source: trunk/lib/Configuration.cc @ 1237

Last change on this file since 1237 was 1237, checked in by Peter Johansson, 12 years ago

remove unneeded include in Alias.h and missing includes (found after this removal).

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
File size: 20.8 KB
Line 
1// $Id: Configuration.cc 1237 2010-10-23 21:41:40Z peter $
2
3/*
4  Copyright (C) 2007, 2008, 2009, 2010 Jari Häkkinen, Peter Johansson
5
6  This file is part of svndigest, http://dev.thep.lu.se/svndigest
7
8  svndigest is free software; you can redistribute it and/or modify it
9  under the terms of the GNU General Public License as published by
10  the Free Software Foundation; either version 3 of the License, or
11  (at your option) any later version.
12
13  svndigest is distributed in the hope that it will be useful, but
14  WITHOUT ANY WARRANTY; without even the implied warranty of
15  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16  General Public License for more details.
17
18  You should have received a copy of the GNU General Public License
19  along with svndigest. If not, see <http://www.gnu.org/licenses/>.
20*/
21
22#include <config.h>
23
24#include "Configuration.h"
25
26#include "Colors.h"
27#include "Functor.h"
28#include "utility.h"
29
30#include "yat/split.h"
31
32#include <algorithm>
33#include <cassert>
34#include <cstdlib>
35#include <fstream>
36#include <map>
37#include <string>
38#include <sstream>
39#include <stdexcept>
40#include <utility>
41
42namespace theplu{
43namespace svndigest{
44
45  Configuration* Configuration::instance_=NULL;
46
47
48  Configuration::Configuration(void)
49  {
50  }
51
52
53  void Configuration::add_codon(std::string key, std::string start, 
54                                std::string end)
55  {
56    std::pair<std::string, std::string> p(start,end);
57    String2Codons::iterator iter = string2codons_.end();
58    for (String2Codons::iterator i=string2codons_.begin();
59         i!=string2codons_.end(); ++i)
60      if (i->first == key)
61        iter = i;
62   
63    if (iter==string2codons_.end())
64      string2codons_.push_back(std::make_pair(key, VectorPair(1,p)));
65    else
66      iter->second.push_back(p);
67  }
68
69
70  const std::map<std::string, std::string>&
71  Configuration::author_colors(void) const
72  {
73    return author_color_;
74  }
75
76  std::string Configuration::author_str_color(const std::string& author) const
77  {
78    std::string res;
79    std::map<std::string, std::string>::const_iterator iterator;
80    if ( (iterator=author_color_.find(author)) != author_color_.end())
81      res = iterator->second;
82    return res;
83  }
84
85
86  const std::vector<std::pair<std::string, std::string> >* 
87  Configuration::codon(std::string file_name) const 
88  {
89    if (const std::pair<std::string,std::string>* dict=dictionary(file_name))
90      file_name = translate(file_name, *dict);
91    for (String2Codons::const_iterator i(string2codons_.begin());
92         i!=string2codons_.end(); ++i) {
93      if (fnmatch(i->first.c_str(), file_name.c_str()))
94        return &i->second;
95    }
96    return NULL;
97  }
98
99
100  const std::map<std::string,Alias>& Configuration::copyright_alias(void) const
101  {
102    return copyright_alias_;
103  }
104
105
106  const std::string&  Configuration::copyright_string(void) const
107  {
108    return copyright_string_;
109  }
110
111  const std::pair<std::string,std::string>* 
112  Configuration::dictionary(std::string lhs) const
113  {
114    for (size_t i=0; i<dictionary_.size(); ++i)
115      if (fnmatch(lhs.c_str(), dictionary_[i].first.c_str()))
116        return &dictionary_[i];
117    return NULL;
118  }
119
120
121  bool Configuration::equal_false(std::string str) const
122  {
123    transform(str.begin(), str.end(), str.begin(), tolower);
124    return str=="false" || str=="no" || str=="off" || str=="0";
125  }
126
127
128  bool Configuration::equal_true(std::string str) const
129  {
130    transform(str.begin(), str.end(), str.begin(), tolower);
131    return str=="true" || str=="yes" || str=="on" || str=="1";
132  }
133
134
135  const std::string& Configuration::image_anchor_format(void) const
136  {
137    return image_anchor_format_;
138  }
139
140
141  const std::string& Configuration::image_format(void) const
142  {
143    return image_format_;
144  }
145
146
147  void Configuration::load(void)
148  {
149    set_default();
150    validate_dictionary();
151  }
152
153
154  void Configuration::load(std::istream& is)
155  {
156    assert(is.good());
157
158    bool parsing_found=false;
159    bool dictionary_found=false;
160    std::string line;
161    std::string section;
162    std::string tmp;
163    while (getline(is, line)) {
164      line = ltrim(line);
165      if (line.empty() || line[0]=='#')
166        continue;
167      std::stringstream ss(line);
168      if (line[0] == '[') {
169        getline(ss, tmp, '[');
170        getline(ss, section, ']');
171        continue;
172      }
173      std::string lhs;
174      getline(ss, lhs, '=');
175      lhs = trim(lhs);
176      std::string rhs;
177      getline(ss, rhs);
178      rhs = trim(rhs);
179      if (rhs.empty()){
180        throw Config_error(line, "expected format: <lhs> = <rhs>");
181      }
182      if (section == "copyright-alias"){
183        std::map<std::string,Alias>::iterator iter = 
184          copyright_alias_.lower_bound(lhs);
185        if (iter!=copyright_alias_.end() && iter->first==lhs){
186          std::stringstream mess;
187          mess << "in copright-alias section " << lhs << " defined twice.";
188          throw Config_error(line, mess.str());
189        }
190       
191        // insert alias
192        copyright_alias_.insert(iter,std::make_pair(lhs, Alias(rhs,copyright_alias_.size())));
193      }
194      else if (section == "trac"){
195        if (lhs=="trac-root")
196          trac_root_=rhs;
197        else {
198          std::stringstream mess;
199          mess << "in trac section" << lhs + " is invalid option.";
200          throw Config_error(line, mess.str());
201        }
202      }
203      else if (section == "output") {
204        if (lhs=="blame-information") {
205          if (equal_false(rhs))
206            output_blame_information_ = false;
207          else if (equal_true(rhs))
208            output_blame_information_ = true;
209          else {
210            throw Config_error(line, "");
211          }
212        }
213        else if (lhs=="file") {
214          if (equal_false(rhs))
215            output_file_ = false;
216          else if (equal_true(rhs))
217            output_file_ = true;
218          else {
219            throw Config_error(line, "");
220          }
221        }
222        else if (lhs=="tab-size") {
223          tab_size_ = strtoul(rhs.c_str(), NULL, 10);
224          if (tab_size_==0) {
225            throw Config_error(line, 
226                       "invalid value: tab-size must be a positive integer");
227          }
228          else if (tab_size_ == ULONG_MAX) {
229            throw Config_error(line, "invalid value: out of range");
230          }
231        }
232      }
233      else if (section == "copyright") {
234        if (lhs=="missing-copyright-warning") {
235          if (equal_false(rhs))
236            missing_copyright_warning_ = false;
237          else if (equal_true(rhs))
238            missing_copyright_warning_ = true;
239          else {
240            throw Config_error(line, "");
241          }
242        }
243        else if (lhs=="copyright-string") {
244          copyright_string_ = rhs;
245        }
246      }
247      else if (section == "author-color") {
248        unsigned char r,g,b;
249        try {
250          str2rgb(rhs, r,g,b);
251        }
252        catch (std::runtime_error& e) {
253          throw Config_error(line, e.what());
254        }
255        author_color_[lhs] = rhs;
256      }     
257      else if (section == "parsing-codons") {
258        if (!parsing_found) {
259          parsing_found=true;
260          // clearing the default setting
261          string2codons_.clear();
262        }
263       
264        if (codon(lhs)) {
265          std::stringstream mess;
266          mess << "clashes with previous given file name pattern: ";
267          // find previous file-name-pattern
268          for (String2Codons::const_iterator i(string2codons_.begin());
269               i!=string2codons_.end(); ++i) {
270            if (fnmatch(lhs.c_str(), i->first.c_str())) {
271              mess << "`" << i->first << "'";
272              break;
273            }
274          }
275          throw Config_error(line, mess.str());
276        }
277        std::stringstream ss(rhs);
278        std::string start;
279        while (getline(ss, start, ':')) {
280          start = trim(start);
281          std::string end;
282          getline(ss, end, ';');
283          end = trim(end);
284          if (start.empty() && end.empty())
285            continue;
286          try {
287            if (start.empty() || start=="\"\"") {
288              throw std::runtime_error("start-code is empty");
289            }
290            else if (start.size()<3) {
291              std::stringstream mess;
292              mess << "start-code `" << start << "' is invalid";
293              throw std::runtime_error(mess.str());
294            }
295            start = trim(start, '"');
296            if (end.empty() || end=="\"\"") {
297              throw std::runtime_error("end-code is empty");
298            }
299            else if (end.size()<3) {
300              std::stringstream mess;
301              mess << "end-code `" << end << "' is invalid";
302              throw std::runtime_error(mess.str());
303            }
304            end = trim(end, '"');
305          }
306          catch (std::runtime_error& e){
307            throw Config_error(line, e.what());
308          }
309          replace(start, "\\n", "\n");
310          replace(end, "\\n", "\n");
311          add_codon(lhs, start, end);
312        }
313      } 
314      else if (section == "file-name-dictionary") {
315        if (!dictionary_found) {
316          dictionary_found=true;
317          // clearing the default setting
318          dictionary_.clear();
319        }
320       
321        if (const std::pair<std::string, std::string>* entry=dictionary(lhs)) {
322          std::stringstream mess;
323          mess << "clashes with previous given file name pattern: "
324               << "`" << entry->first << "'";
325          throw Config_error(line, mess.str());
326        }
327        lhs = trim(lhs);
328        rhs = trim(rhs);
329        if (!lhs.empty() && !rhs.empty()) 
330          dictionary_.push_back(std::make_pair(lhs, rhs));
331        else if (!lhs.empty() || !rhs.empty()) {
332          throw Config_error(line, "");
333        }
334      } 
335      else if (section == "svn_props") {
336        svn_props_.push_back(std::make_pair(lhs, empty_str_map_));
337        std::vector<std::string> vec;
338        yat::utility::split(vec, rhs, ';');
339        for (size_t i=0; i<vec.size(); ++i) {
340          std::vector<std::string> vec2;
341          yat::utility::split(vec2, vec[i], '=');
342          std::string key = trim(vec2[0]);
343          std::string value("");
344          if (vec2.size() >= 2)
345            value = trim(vec2[1]);
346          svn_props_.back().second[key] = value;
347        }
348      }
349      else if (section == "image") {
350        if (lhs == "format") {
351          try {
352            image_format(rhs);
353          }
354          catch (std::runtime_error e) {
355            throw Config_error(line, 
356                               "unknown format: " + rhs + "\n" + e.what());
357          }
358        }
359        else if (lhs == "image_format") {
360          try {
361            image_anchor_format(rhs);
362          }
363          catch (std::runtime_error e) {
364            throw Config_error(line, 
365                               "unknown format: " + rhs + "\n" + e.what());
366          }
367        }
368      }
369    }
370    validate_dictionary();
371  }
372
373
374  void Configuration::image_anchor_format(const std::string& format)
375  {
376    if (format!="none" && format!="pdf" && format!="png" && format!="svg") {
377      std::ostringstream oss;
378      oss << "Valid arguments are:\n"
379          << "  - `none'\n"
380          << "  - `pdf'\n"
381          << "  - `png'\n"
382          << "  - `svg'";
383      throw std::runtime_error(oss.str());
384    }
385    image_anchor_format_ = format;
386  }
387
388
389  void Configuration::image_format(const std::string& format)
390  {
391    if (format!="none" && format!="png" && format!="svg") {
392      std::ostringstream oss;
393      oss << "Valid arguments are:\n"
394          << "  - `none'\n"
395          << "  - `png'\n"
396          << "  - `svg'";
397      throw std::runtime_error(oss.str());
398    }
399    image_format_ = format;
400  }
401
402
403  Configuration& Configuration::instance(void)
404  {
405    if (!instance_){
406      instance_ = new Configuration;
407      instance_->load();
408    }
409    return *instance_;
410  }
411
412
413  bool Configuration::missing_copyright_warning(void) const
414  {
415    return missing_copyright_warning_;
416  }
417
418
419  std::string
420  Configuration::translate(const std::string& str,
421                           const std::pair<std::string, std::string>& dic) const
422  {
423    std::string res;
424    std::vector<std::string> vec;
425    if (!regexp(dic.first, str, vec)) {
426      std::stringstream mess;
427      mess << "invalid config file: "
428           << "expression " << dic.first << " is invalid";
429      throw std::runtime_error(mess.str());       
430    }
431    for (std::string::const_iterator i(dic.second.begin()); 
432         i!=dic.second.end(); ++i) {
433      if (*i == '$') {
434        std::stringstream ss(std::string(i+1, dic.second.end()));
435        size_t n = 0;
436        ss >> n;
437        if (n>vec.size() || n==0){
438          std::stringstream mess;
439          mess << "invalid config file: "
440               << "expression " << dic.second << " is invalid";
441          if (n)
442            mess << "because " << n << " is a too large.";
443          throw std::runtime_error(mess.str());       
444        }
445        res += vec[n-1];
446        ++i;
447        if (n>9){
448          ++i;
449          if (n>99)
450            ++i;
451
452        }
453      }
454      else
455        res += *i;
456    }
457
458    return res;
459  }
460
461
462  std::string trans_end_code(std::string str)
463  {
464    if (str.size()>0 && str[str.size()-1]=='\n')
465      return str.substr(0, str.size()-1) + std::string("\\n");
466    return str;
467  }
468
469
470  std::string trans_beg_code(std::string str)
471  {
472    if (str.size()>0 && str[0]=='\n')
473      return std::string("\\n") + str.substr(1); 
474    return str;
475  }
476
477
478  std::string trim(std::string str, char c)
479  {
480    if (str.size()<2 || str[0]!=c || str[str.size()-1]!=c){
481      std::stringstream mess;
482      mess << "expected `" << str << "' to be surrounded by `" << c << "'";
483      throw std::runtime_error(mess.str());
484    }
485    return str.substr(1, str.size()-2);
486  }
487
488
489  void Configuration::set_default(void)
490  {
491    copyright_alias_.clear();
492    copyright_string_="Copyright (C)";
493    missing_copyright_warning_=false;
494    trac_root_ = "";
495    tab_size_ = 2;
496
497    add_codon("*.ac", "#", "\n");
498    add_codon("*.ac", "dnl", "\n");
499    add_codon("*.am", "#", "\n");
500    add_codon("*.as", "#", "\n");
501    add_codon("*.as", "dnl", "\n");
502    add_codon("*.bat", "\nREM", "\n");
503    add_codon("*.bat", "\nrem", "\n");
504    add_codon("*.c", "//", "\n");
505    add_codon("*.c", "/*", "*/");
506    add_codon("*.cc", "//", "\n");
507    add_codon("*.cc", "/*", "*/");
508    add_codon("*.cpp", "//", "\n");
509    add_codon("*.cpp", "/*", "*/");
510    add_codon("*.css", "<!--", "-->");
511    add_codon("*.cxx", "//", "\n");
512    add_codon("*.cxx", "/*", "*/");
513    add_codon("*.h", "//", "\n");
514    add_codon("*.h", "/*", "*/");
515    add_codon("*.hh", "//", "\n");
516    add_codon("*.hh", "/*", "*/");
517    add_codon("*.hpp", "//", "\n");
518    add_codon("*.hpp", "/*", "*/");
519    add_codon("*.html", "<%--", "--%>");
520    add_codon("*.java", "//", "\n");
521    add_codon("*.java", "/*", "*/");
522    add_codon("*.jsp", "<!--", "-->");
523    add_codon("*.m", "%", "\n");
524    add_codon("*.m4", "#", "\n");
525    add_codon("*.m4", "dnl", "\n");
526    add_codon("*.pl", "#", "\n");
527    add_codon("*.pm", "#", "\n");
528    add_codon("*.R", "#", "\n");
529    add_codon("*.rss", "<!--", "-->");
530    add_codon("*.sgml", "<!--", "-->");
531    add_codon("*.sh", "#", "\n");
532    add_codon("*.shtml", "<!--", "-->");
533    add_codon("*.tex", "%", "\n");
534    add_codon("*.xhtml", "<!--", "-->");
535    add_codon("*.xml", "<!--", "-->");
536    add_codon("*.xsd", "<!--", "-->");
537    add_codon("*.xsl", "<!--", "-->");
538    add_codon("*config", "#", "\n");
539    add_codon("bootstrap", "#", "\n");
540    add_codon("Makefile", "#", "\n");
541
542    dictionary_ = VectorPair(1, std::make_pair("*.in", "$1"));
543    image_format_ = "png";
544    image_anchor_format_ = "png";
545    output_blame_information_ = true;
546    output_file_ = true;
547
548  }
549
550
551  bool Configuration::output_blame_information(void) const
552  {
553    return output_blame_information_;
554  }
555
556
557  bool Configuration::output_file(void) const
558  {
559    return output_file_;
560  }
561
562
563  const std::map<std::string, std::string>&
564  Configuration::svn_properties(const std::string& filename) const
565  {
566    // reverse backwards as we prefer to to pick properties defined later
567    std::vector<props>::const_reverse_iterator iter = 
568      find_fn(svn_props_.rbegin(), svn_props_.rend(), filename);
569    if (iter==svn_props_.rend())
570      return empty_str_map_;
571    return iter->second;
572  }
573
574
575  size_t Configuration::tab_size(void) const
576  {
577    return tab_size_;
578  }
579
580
581  std::string Configuration::trac_root(void) const
582  {
583    return trac_root_;
584  }
585
586
587  void Configuration::validate_dictionary(void) const
588  {
589    VectorPair::const_iterator end(dictionary_.end());
590    for (VectorPair::const_iterator iter(dictionary_.begin());iter!=end;++iter){
591      std::string word(iter->first);
592      replace(word, "*", "");
593      replace(word, "?", "");
594      // throws if dictionary is invalid
595      translate(word, *iter);
596    }
597  }
598
599
600  std::ostream& operator<<(std::ostream& os, const Configuration& conf)
601  {
602    os << "### This file configures various behaviors for svndigest\n"
603       << "### The commented-out below are intended to demonstrate how to use\n"
604       << "### this file.\n"
605       << "\n";
606
607    os << "### Section for setting output\n"
608       << "[output]\n"
609       << "# if true svndigest will output blame information for each file.\n"
610       << "blame-information = ";
611    if (conf.output_blame_information())
612      os << "yes\n";
613    else
614      os << "no\n";
615    os << "# if true report will have pages for files and not only "
616       << "directories.\n"
617       << "file = ";
618    if (conf.output_file())
619      os << "yes\n";
620    else
621      os << "no\n";
622    os << "# svndigest uses this value to replace tabs "
623       << "with spaces in blame output\n"
624       << "tab-size = " << conf.tab_size() << "\n";
625
626
627    os << "\n### Section for setting behaviour of copyright update\n"
628       << "[copyright]\n"
629       << "# if true svndigest will warn if file has no copyright statement.\n"
630       << "missing-copyright-warning = ";
631    if (conf.missing_copyright_warning())
632      os << "yes\n";
633    else
634      os << "no\n";
635    os << "copyright-string = " << conf.copyright_string_ << "\n"; 
636
637    os << "\n"
638       << "### Section for setting aliases used in copyright update\n"
639       << "[copyright-alias]\n"
640       << "# jdoe = John Doe\n";
641
642    typedef std::vector<std::pair<std::string, Alias> > vector;
643    vector vec;
644    std::back_insert_iterator<vector> back_insert_iterator(vec);
645    vec.reserve(conf.copyright_alias().size());
646    std::copy(conf.copyright_alias().begin(), conf.copyright_alias().end(),
647              back_insert_iterator);
648    // sort with respect to Alias.id
649    IdCompare id;
650    PairSecondCompare<const std::string, Alias, IdCompare> comp(id);
651    std::sort(vec.begin(),vec.end(), comp);
652             
653
654    for (vector::const_iterator i(vec.begin()); i!=vec.end(); ++i) {
655      os << i->first << " = " << i->second.name() << "\n";
656    }
657
658    os << "\n"
659       << "### Section for images\n"
660       << "[image]\n"
661       << "format = " << conf.image_format() << "\n";
662    os << "anchor_format = " << conf.image_anchor_format() << "\n";
663
664
665    os << "\n"
666       << "### Section for author color in plots and blame output.\n"
667       << "[author-color]\n"
668       << "# jdoe = 000000\n";
669    typedef Configuration::str_map str_map;
670    for (str_map::const_iterator i(conf.author_color_.begin());
671         i!=conf.author_color_.end(); ++i) {
672      os << i->first << " = " << i->second << "\n";
673    }
674
675    typedef Configuration::props props;
676    os << "\n"
677       << "### Section for overriding svn properties.\n"
678       << "### The format is the same as for section auto-props in subversion\n"
679       << "### config file\n"
680       << "[svn-props]\n";
681    os << "# COPYING = svndigest:ignore\n";
682    std::vector<props>::const_iterator p=conf.svn_props_.begin(); 
683    for ( ; p!=conf.svn_props_.end(); ++p) {
684      os << p->first << " = ";
685      const str_map& map = p->second;
686      str_map::const_iterator end = map.end();
687      for (str_map::const_iterator i=map.begin(); i!=end; ++i) {
688        if (i != map.begin())
689          os << ";";
690        os << i->first << "=" << i->second;
691      }
692      os << "\n";
693    }
694
695
696    os << "\n"
697       << "### Section for setting trac environment.\n"
698       << "[trac]\n"
699       << "# If trac-root is set, svndigest will create anchors to "
700       << "the Trac page.\n"
701       << "# trac-root = http://dev.thep.lu.se/svndigest/\n";
702    if (!conf.trac_root().empty())
703      os << "trac-root = " << conf.trac_root() << "\n";
704
705    os << "\n"
706       << "### Section for setting dictionary for file names.\n"
707       << "### Prior looking for file name pattern in section " 
708       << "[parsing-codons],\n"
709       << "### the file name may be translated according to the rules \n"
710       << "### in this section. In default setting there is, for example,\n"
711       << "### a rule to translate `<FILENAME>.in' to `<FILENAME>'.\n"
712       << "### The format of the entries is:\n"
713       << "###    file-name-pattern = new-name\n"
714       << "### Left hand side may contain wildcards (such as '*' and '?').\n"
715       << "### Right hand side may contain \"$i\", which will be replaced \n"
716       << "### with the ith wild card in lhs string.\n";
717
718    if (!conf.dictionary_.empty()) {
719      os << "\n"
720         << "### Section for setting dictionary for file names.\n"
721         << "### Prior looking for file name pattern in section " 
722         << "[parsing-codons],\n"
723         << "### the file name may be translated according to the rules \n"
724         << "### in this section. In default setting there is, for example,\n"
725         << "### a rule to translate `<FILENAME>.in' to `<FILENAME>'.\n"
726         << "### The format of the entries is:\n"
727         << "###    file-name-pattern = new-name\n"
728         << "### Left hand side may contain wildcards (such as '*' and '?').\n"
729         << "### Right hand side may contain \"$i\", which will be replaced \n"
730         << "### with the ith wild card in lhs string.\n"
731         << "[file-name-dictionary]\n";
732      for (size_t i=0; i<conf.dictionary_.size(); ++i)
733        os << conf.dictionary_[i].first << " = " 
734           << conf.dictionary_[i].second << "\n"; 
735    }
736    if (!conf.string2codons_.empty()) {
737      os << "\n"
738         << "### Section for setting parsing modes\n"
739         << "### The format of the entries is:\n"
740         << "###   file-name-pattern = \"start-code\" : \"end-code\"\n"
741         << "### The file-name-pattern may contain wildcards (such as '*' "
742         << "and '?').\n"
743         << "### String \"\\n\" can be used for codons containing newline"
744         << "\n### character.\n"
745         << "[parsing-codons]\n";
746      for (size_t i=0; i<conf.string2codons_.size(); ++i) {
747        os << conf.string2codons_[i].first << " = "; 
748        for (size_t j=0; j<conf.string2codons_[i].second.size(); ++j) {
749          if (j)
750            os << "  ;  ";
751          os << "\"" << trans_beg_code(conf.string2codons_[i].second[j].first) 
752             << "\":\"" 
753             << trans_end_code(conf.string2codons_[i].second[j].second) 
754             << "\""; 
755        }
756        os << "\n";
757      }
758    }
759    return os;
760  }
761
762 
763  Config_error::Config_error(const std::string& line,const std::string& message)
764    : std::runtime_error(std::string("line: `") + line + 
765                         std::string("' is invalid.\n") + message)
766  {}
767
768}} // end of namespace svndigest and namespace theplu
Note: See TracBrowser for help on using the repository browser.