source: trunk/lib/Configuration.cc @ 1158

Last change on this file since 1158 was 1158, checked in by Peter Johansson, 11 years ago

remove debug code

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