source: trunk/lib/Configuration.cc @ 1442

Last change on this file since 1442 was 1442, checked in by Jari Häkkinen, 9 years ago

Reverting uninentational change of default image formats

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