source: trunk/third/perl/pod/buildtoc @ 20075

Revision 20075, 17.6 KB checked in by zacheiss, 21 years ago (diff)
This commit was generated by cvs2svn to compensate for changes in r20074, which included commits to RCS files with non-trunk default branches.
Line 
1#!/usr/bin/perl -w
2
3use strict;
4use vars qw($masterpodfile %Build %Targets $Verbose $Up %Ignore
5            @Master %Readmes %Pods %Aux %Readmepods %Pragmata %Modules
6            %Copies);
7use File::Spec;
8use File::Find;
9use FindBin;
10use Text::Tabs;
11use Text::Wrap;
12use Getopt::Long;
13
14no locale;
15
16$Up = File::Spec->updir;
17$masterpodfile = File::Spec->catdir($Up, "pod.lst");
18
19# Generate any/all of these files
20# --verbose gives slightly more output
21# --build-all tries to build everything
22# --build-foo updates foo as follows
23# --showfiles shows the files to be changed
24
25%Targets
26  = (
27     toc => "perltoc.pod",
28     manifest => File::Spec->catdir($Up, "MANIFEST"),
29     perlpod => "perl.pod",
30     vms => File::Spec->catdir($Up, "vms", "descrip_mms.template"),
31     nmake => File::Spec->catdir($Up, "win32", "Makefile"),
32     dmake => File::Spec->catdir($Up, "win32", "makefile.mk"),
33     podmak => File::Spec->catdir($Up, "win32", "pod.mak"),
34     # plan9 =>  File::Spec->catdir($Up, "plan9", "mkfile"),
35    );
36
37{
38  my @files = keys %Targets;
39  my $filesopts = join(" | ", map { "--build-$_" } "all", sort @files);
40  my $showfiles;
41  die <<__USAGE__
42$0: Usage: $0 [--verbose] [--showfiles] $filesopts
43__USAGE__
44  unless @ARGV
45        && GetOptions (verbose => \$Verbose,
46                       showfiles => \$showfiles,
47                       map {+"build-$_", \$Build{$_}} @files, 'all');
48  # Set them all to true
49  @Build{@files} = @files if ($Build{all});
50  if ($showfiles) {
51      print
52          join(" ",
53               sort { lc $a cmp lc $b }
54               map {
55                   my ($v, $d, $f) = File::Spec->splitpath($_);
56                   my @d;
57                   @d = defined $d ? File::Spec->splitdir($d) : ();
58                   shift @d if @d;
59                   File::Spec->catfile(@d ?
60                                       (@d == 1 && $d[0] eq '' ? () : @d)
61                                       : "pod", $f);
62               } @Targets{grep { $_ ne 'all' && $Build{$_} } keys %Build}),
63          "\n";
64      exit(0);
65  }
66}
67
68# Don't copy these top level READMEs
69%Ignore
70  = (
71     Y2K => 1,
72     micro => 1,
73#     vms => 1,
74     );
75
76if ($Verbose) {
77  print "I'm building $_\n" foreach grep {$Build{$_}} keys %Build;
78}
79
80chdir $FindBin::Bin or die "$0: Can't chdir $FindBin::Bin: $!";
81
82open MASTER, $masterpodfile or die "$0: Can't open $masterpodfile: $!";
83
84my ($delta_source, $delta_target);
85
86foreach (<MASTER>) {
87  next if /^\#/;
88
89  # At least one upper case letter somewhere in the first group
90  if (/^(\S+)\s(.*)/ && $1 =~ tr/h//) {
91    # it's a heading
92    my $flags = $1;
93    $flags =~ tr/h//d;
94    my %flags = (header => 1);
95    $flags{toc_omit} = 1 if $flags =~ tr/o//d;
96    $flags{aux} = 1 if $flags =~ tr/a//d;
97    die "$0: Unknown flag found in heading line: $_" if length $flags;
98    push @Master, [\%flags, $2];
99
100  } elsif (/^(\S*)\s+(\S+)\s+(.*)/) {
101    # it's a section
102    my ($flags, $filename, $desc) = ($1, $2, $3);
103
104    my %flags = (indent => 0);
105    $flags{indent} = $1 if $flags =~ s/(\d+)//;
106    $flags{toc_omit} = 1 if $flags =~ tr/o//d;
107    $flags{aux} = 1 if $flags =~ tr/a//d;
108
109    if ($flags =~ tr/D//d) {
110      $flags{perlpod_omit} = 1;
111      $delta_source = "$filename.pod";
112    }
113    if ($flags =~ tr/d//d) {
114      $flags{manifest_omit} = 1;
115      $delta_target = "$filename.pod";
116    }
117
118    if ($flags =~ tr/r//d) {
119      my $readme = $filename;
120      $readme =~ s/^perl//;
121      $Readmepods{$filename} = $Readmes{$readme} = $desc;
122      $flags{readme} = 1;
123    } elsif ($flags{aux}) {
124      $Aux{$filename} = $desc;
125    } else {
126      $Pods{$filename} = $desc;
127    }
128    die "$0: Unknown flag found in section line: $_" if length $flags;
129    push @Master, [\%flags, $filename, $desc];
130  } elsif (/^$/) {
131    push @Master, undef;
132  } else {
133    die "$0: Malformed line: $_" if $1 =~ tr/A-Z//;
134  }
135}
136if (defined $delta_source) {
137  if (defined $delta_target) {
138    # This way round so that keys can act as a MANIFEST skip list
139    # Targets will aways be in the pod directory. Currently we can only cope
140    # with sources being in the same directory. Fix this and do perlvms.pod
141    # with this?
142    $Copies{$delta_target} = $delta_source;
143  } else {
144    die "$0: delta source defined but not target";
145  }
146} elsif (defined $delta_target) {
147  die "$0: delta target defined but not target";
148}
149
150close MASTER;
151
152# Sanity cross check
153{
154  my (%disk_pods, @disk_pods);
155  my (@manipods, %manipods);
156  my (@manireadmes, %manireadmes);
157  my (@perlpods, %perlpods);
158  my (%our_pods);
159  my (%sources);
160
161  # Convert these to a list of filenames.
162  foreach (keys %Pods, keys %Readmepods) {
163    $our_pods{"$_.pod"}++;
164  }
165
166  # None of these filenames will be boolean false
167  @disk_pods = glob("*.pod");
168  @disk_pods{@disk_pods} = @disk_pods;
169
170  # Things we copy from won't be in perl.pod
171  # Things we copy to won't be in MANIFEST
172  @sources{values %Copies} = ();
173
174  open(MANI, "../MANIFEST") || die "$0: opening ../MANIFEST failed: $!";
175  while (<MANI>) {
176    if (m!^pod/([^.]+\.pod)\s+!i) {
177      push @manipods, $1;
178    } elsif (m!^README\.(\S+)\s+!i) {
179      next if $Ignore{$1};
180      push @manireadmes, "perl$1.pod";
181    }
182  }
183  close(MANI);
184  @manipods{@manipods} = @manipods;
185  @manireadmes{@manireadmes} = @manireadmes;
186
187  open(PERLPOD, "perl.pod") || die "$0: opening perl.pod failed: $!\n";
188  while (<PERLPOD>) {
189    if (/^For ease of access, /../^\(If you're intending /) {
190      if (/^\s+(perl\S*)\s+\w/) {
191        push @perlpods, "$1.pod";
192      }
193    }
194  }
195  close(PERLPOD);
196  die "$0: could not find the pod listing of perl.pod\n"
197    unless @perlpods;
198  @perlpods{@perlpods} = @perlpods;
199
200  foreach my $i (sort keys %disk_pods) {
201    warn "$0: $i exists but is unknown by buildtoc\n"
202      unless $our_pods{$i};
203    warn "$0: $i exists but is unknown by ../MANIFEST\n"
204      if !$manipods{$i} && !$manireadmes{$i} && !$Copies{$i};
205    warn "$0: $i exists but is unknown by perl.pod\n"
206        if !$perlpods{$i} && !exists $sources{$i};
207  }
208  foreach my $i (sort keys %our_pods) {
209    warn "$0: $i is known by buildtoc but does not exist\n"
210      unless $disk_pods{$i};
211  }
212  foreach my $i (sort keys %manipods) {
213    warn "$0: $i is known by ../MANIFEST but does not exist\n"
214      unless $disk_pods{$i};
215  }
216  foreach my $i (sort keys %perlpods) {
217    warn "$0: $i is known by perl.pod but does not exist\n"
218      unless $disk_pods{$i};
219  }
220}
221
222# Find all the mdoules
223{
224  my @modpods;
225  find \&getpods => qw(../lib ../ext);
226
227  sub getpods {
228    if (/\.p(od|m)$/) {
229      my $file = $File::Find::name;
230      return if $file eq '../lib/Pod/Functions.pm'; # Used only by pod itself
231      return if $file =~ m!(?:^|/)t/!;
232      return if $file =~ m!lib/Attribute/Handlers/demo/!;
233      return if $file =~ m!lib/Net/FTP/.+\.pm!; # Hi, Graham! :-)
234      return if $file =~ m!lib/Math/BigInt/t/!;
235      return if $file =~ m!/Devel/PPPort/[Hh]arness|lib/Devel/Harness!i;
236      return if $file =~ m!XS/(?:APItest|Typemap)!;
237      my $pod = $file;
238      return if $pod =~ s/pm$/pod/ && -e $pod;
239      die "$0: tut $File::Find::name" if $file =~ /TUT/;
240      unless (open (F, "< $_\0")) {
241        warn "$0: bogus <$file>: $!";
242        system "ls", "-l", $file;
243      }
244      else {
245        my $line;
246        while ($line = <F>) {
247          if ($line =~ /^=head1\s+NAME\b/) {
248            push @modpods, $file;
249            #warn "GOOD $file\n";
250            return;
251          }
252        }
253        warn "$0: $file: cannot find =head1 NAME\n";
254      }
255    }
256  }
257
258  die "$0: no pods" unless @modpods;
259
260  my %done;
261  for (@modpods) {
262    #($name) = /(\w+)\.p(m|od)$/;
263    my $name = path2modname($_);
264    if ($name =~ /^[a-z]/) {
265      $Pragmata{$name} = $_;
266    } else {
267      if ($done{$name}++) {
268        # warn "already did $_\n";
269        next;
270      }
271      $Modules{$name} = $_;
272    }
273  }
274}
275
276# OK. Now a lot of ancillay function definitions follow
277# Main program returns at "Do stuff"
278
279sub path2modname {
280    local $_ = shift;
281    s/\.p(m|od)$//;
282    s-.*?/(lib|ext)/--;
283    s-/-::-g;
284    s/(\w+)::\1/$1/;
285    return $_;
286}
287
288sub output ($);
289
290sub output_perltoc {
291  open(OUT, ">perltoc.pod") || die "$0: creating perltoc.pod failed: $!";
292
293  local $/ = '';
294
295  ($_= <<"EOPOD2B") =~ s/^\t//gm && output($_);
296
297        # !!!!!!!   DO NOT EDIT THIS FILE   !!!!!!!
298        # This file is autogenerated by buildtoc from all the other pods.
299        # Edit those files and run buildtoc --build-toc to effect changes.
300
301        =head1 NAME
302
303        perltoc - perl documentation table of contents
304
305        =head1 DESCRIPTION
306
307        This page provides a brief table of contents for the rest of the Perl
308        documentation set.  It is meant to be scanned quickly or grepped
309        through to locate the proper section you're looking for.
310
311        =head1 BASIC DOCUMENTATION
312
313EOPOD2B
314#' make emacs happy
315
316  # All the things in the master list that happen to be pod filenames
317  podset(map {"$_->[1].pod"} grep {defined $_ && @$_ == 3 && !$_->[0]{toc_omit}} @Master);
318
319
320  ($_= <<"EOPOD2B") =~ s/^\t//gm && output($_);
321
322
323
324        =head1 PRAGMA DOCUMENTATION
325
326EOPOD2B
327
328  podset(sort values %Pragmata);
329
330  ($_= <<"EOPOD2B") =~ s/^\t//gm && output($_);
331
332
333
334        =head1 MODULE DOCUMENTATION
335
336EOPOD2B
337
338  podset( @Modules{ sort keys %Modules } );
339
340  $_= <<"EOPOD2B";
341
342
343        =head1 AUXILIARY DOCUMENTATION
344
345        Here should be listed all the extra programs' documentation, but they
346        don't all have manual pages yet:
347
348        =over 4
349
350EOPOD2B
351
352  $_ .=  join "\n", map {"\t=item $_\n"} sort keys %Aux;
353  $_ .= <<"EOPOD2B" ;
354
355        =back
356
357        =head1 AUTHOR
358
359        Larry Wall <F<larry\@wall.org>>, with the help of oodles
360        of other folks.
361
362
363EOPOD2B
364
365  s/^\t//gm;
366  output $_;
367  output "\n";                    # flush $LINE
368}
369
370# Below are all the auxiliary routines for generating perltoc.pod
371
372my ($inhead1, $inhead2, $initem);
373
374sub podset {
375    local @ARGV = @_;
376    my $pod;
377
378    while(<>) {
379        tr/\015//d;
380        if (s/^=head1 (NAME)\s*/=head2 /) {
381            $pod = path2modname($ARGV);
382            unhead1();
383            output "\n \n\n=head2 ";
384            $_ = <>;
385            if ( /^\s*$pod\b/ ) {
386                s/$pod\.pm/$pod/;       # '.pm' in NAME !?
387                output $_;
388            } else {
389                s/^/$pod, /;
390                output $_;
391            }
392            next;
393        }
394        if (s/^=head1 (.*)/=item $1/) {
395            unhead2();
396            output "=over 4\n\n" unless $inhead1;
397            $inhead1 = 1;
398            output $_; nl(); next;
399        }
400        if (s/^=head2 (.*)/=item $1/) {
401            unitem();
402            output "=over 4\n\n" unless $inhead2;
403            $inhead2 = 1;
404            output $_; nl(); next;
405        }
406        if (s/^=item ([^=].*)/$1/) {
407            next if $pod eq 'perldiag';
408            s/^\s*\*\s*$// && next;
409            s/^\s*\*\s*//;
410            s/\n/ /g;
411            s/\s+$//;
412            next if /^[\d.]+$/;
413            next if $pod eq 'perlmodlib' && /^ftp:/;
414            ##print "=over 4\n\n" unless $initem;
415            output ", " if $initem;
416            $initem = 1;
417            s/\.$//;
418            s/^-X\b/-I<X>/;
419            output $_; next;
420        }
421        if (s/^=cut\s*\n//) {
422            unhead1();
423            next;
424        }
425    }
426}
427
428sub unhead1 {
429    unhead2();
430    if ($inhead1) {
431        output "\n\n=back\n\n";
432    }
433    $inhead1 = 0;
434}
435
436sub unhead2 {
437    unitem();
438    if ($inhead2) {
439        output "\n\n=back\n\n";
440    }
441    $inhead2 = 0;
442}
443
444sub unitem {
445    if ($initem) {
446        output "\n\n";
447        ##print "\n\n=back\n\n";
448    }
449    $initem = 0;
450}
451
452sub nl {
453    output "\n";
454}
455
456my $NEWLINE = 0;        # how many newlines have we seen recently
457my $LINE;               # what remains to be printed
458
459sub output ($) {
460    for (split /(\n)/, shift) {
461        if ($_ eq "\n") {
462            if ($LINE) {
463                print OUT wrap('', '', $LINE);
464                $LINE = '';
465            }
466            if (($NEWLINE) < 2) {
467                print OUT;
468                $NEWLINE++;
469            }
470        }
471        elsif (/\S/ && length) {
472            $LINE .= $_;
473            $NEWLINE = 0;
474        }
475    }
476}
477
478# End of original buildtoc. From here on are routines to generate new sections
479# for and inplace edit other files
480
481sub generate_perlpod {
482  my @output;
483  my $maxlength = 0;
484  foreach (@Master) {
485    my $flags = $_->[0];
486    next if $flags->{aux};
487    next if $flags->{perlpod_omit};
488
489    if (@$_ == 2) {
490      # Heading
491      push @output, "=head2 $_->[1]\n";
492    } elsif (@$_ == 3) {
493      # Section
494      my $start = " " x (4 + $flags->{indent}) . $_->[1];
495      $maxlength = length $start if length ($start) > $maxlength;
496      push @output, [$start, $_->[2]];
497    } elsif (@$_ == 0) {
498      # blank line
499      push @output, "\n";
500    } else {
501      die "$0: Illegal length " . scalar @$_;
502    }
503  }
504  # want at least 2 spaces padding
505  $maxlength += 2;
506  $maxlength = ($maxlength + 3) & ~3;
507  # sprintf gives $1.....$2 where ... are spaces:
508  return unexpand (map {ref $_ ? sprintf "%-${maxlength}s%s\n", @$_ : $_}
509                   @output);
510}
511
512
513sub generate_manifest {
514  # Annyoingly unexpand doesn't consider it good form to replace a single
515  # space before a tab with a tab
516  # Annoyingly (2) it returns read only values.
517  my @temp = unexpand (map {sprintf "%-32s%s\n", @$_} @_);
518  map {s/ \t/\t\t/g; $_} @temp;
519}
520sub generate_manifest_pod {
521  generate_manifest map {["pod/$_.pod", $Pods{$_}]}
522    grep {!$Copies{"$_.pod"}} sort keys %Pods;
523}
524sub generate_manifest_readme {
525  generate_manifest map {["README.$_", $Readmes{$_}]} sort keys %Readmes;
526}
527
528sub generate_roffitall {
529  (map ({"\t\$maindir/$_.1\t\\"}sort keys %Pods),
530   "\t\t\\",
531   map ({"\t\$maindir/$_.1\t\\"}sort keys %Aux),
532   "\t\t\\",
533   map ({"\t\$libdir/$_.3\t\\"}sort keys %Pragmata),
534   "\t\t\\",
535   map ({"\t\$libdir/$_.3\t\\"}sort keys %Modules),
536  )
537}
538
539sub generate_descrip_mms_1 {
540  local $Text::Wrap::columns = 150;
541  my $count = 0;
542  my @lines = map {"pod" . $count++ . " = $_"}
543    split /\n/, wrap('', '', join " ", map "[.lib.pod]$_.pod",
544                     sort keys %Pods, keys %Readmepods);
545  @lines, "pod = " . join ' ', map {"\$(pod$_)"} 0 .. $count - 1;
546}
547
548sub generate_descrip_mms_2 {
549  map {sprintf <<'SNIP', $_, $_ eq 'perlvms' ? 'vms' : 'pod', $_}
550[.lib.pod]%s.pod : [.%s]%s.pod
551        @ If F$Search("[.lib]pod.dir").eqs."" Then Create/Directory [.lib.pod]
552        Copy/NoConfirm/Log $(MMS$SOURCE) [.lib.pod]
553SNIP
554   sort keys %Pods, keys %Readmepods;
555}
556
557sub generate_nmake_1 {
558  # XXX Fix this with File::Spec
559  (map {sprintf "\tcopy ..\\README.%-8s ..\\pod\\perl$_.pod\n", $_}
560    sort keys %Readmes),
561      (map {"\tcopy ..\\pod\\$Copies{$_} ..\\pod\\$_\n"} sort keys %Copies);
562}
563
564# This doesn't have a trailing newline
565sub generate_nmake_2 {
566  # Spot the special case
567  local $Text::Wrap::columns = 76;
568  my $line = wrap ("\t    ", "\t    ",
569                   join " ", sort keys %Copies,
570                                  map {"perl$_.pod"} "vms", keys %Readmes);
571  $line =~ s/$/ \\/mg;
572  $line;
573}
574
575sub generate_pod_mak {
576  my $variable = shift;
577  my @lines;
578  my $line = join "\\\n", "\U$variable = ",
579    map {"\t$_.$variable\t"} sort keys %Pods;
580  # Special case
581  $line =~ s/.*perltoc.html.*\n//m;
582  $line;
583}
584
585sub do_manifest {
586  my $name = shift;
587  my @manifest =
588    grep {! m!^pod/[^.]+\.pod.*\n!}
589      grep {! m!^README\.(\S+)! || $Ignore{$1}} @_;
590  # Dictionary order - fold and handle non-word chars as nothing
591  map  { $_->[0] }
592  sort { $a->[1] cmp $b->[1] || $a->[0] cmp $b->[0] }
593  map  { my $f = lc $_; $f =~ s/[^a-z0-9\s]//g; [ $_, $f ] }
594    @manifest,
595      &generate_manifest_pod(),
596        &generate_manifest_readme();
597}
598
599sub do_nmake {
600  my $name = shift;
601  my $makefile = join '', @_;
602  die "$0: $name contains NUL bytes" if $makefile =~ /\0/;
603  $makefile =~ s/^\tcopy \.\.\\README.*\n/\0/gm;
604  my $sections = () = $makefile =~ m/\0+/g;
605  die "$0: $name contains no README copies" if $sections < 1;
606  die "$0: $name contains discontiguous README copies" if $sections > 1;
607  # Now remove the other copies that follow
608  1 while $makefile =~ s/\0\tcopy .*\n/\0/gm;
609  $makefile =~ s/\0+/join ("", &generate_nmake_1)/se;
610
611  $makefile =~ s{(del /f [^\n]+checkpods[^\n]+).*?(pod2html)}
612    {"$1\n" . &generate_nmake_2."\n\t    $2"}se;
613  $makefile;
614}
615
616# shut up used only once warning
617*do_dmake = *do_dmake = \&do_nmake;
618
619sub do_perlpod {
620  my $name = shift;
621  my $pod = join '', @_;
622
623  unless ($pod =~ s{(For\ ease\ of\ access,\ .*\n)
624                    (?:\s+[a-z]{4,}.*\n #   fooo
625                    |=head.*\n          # =head foo
626                    |\s*\n              # blank line
627                   )+
628                  }
629          {$1 . join "", &generate_perlpod}mxe) {
630    die "$0: Failed to insert ammendments in do_perlpod";
631  }
632  $pod;
633}
634
635sub do_podmak {
636  my $name = shift;
637  my $body = join '', @_;
638  foreach my $variable (qw(pod man html tex)) {
639    die "$0: could not find $variable in $name"
640      unless $body =~ s{\n\U$variable\E = (?:[^\n]*\\\n)*[^\n]*}
641        {"\n" . generate_pod_mak ($variable)}se;
642  }
643  $body;
644}
645
646sub do_vms {
647  my $name = shift;
648  my $makefile = join '', @_;
649  die "$0: $name contains NUL bytes" if $makefile =~ /\0/;
650  $makefile =~ s/\npod\d* =[^\n]*/\0/gs;
651  my $sections = () = $makefile =~ m/\0+/g;
652  die "$0: $name contains no pod assignments" if $sections < 1;
653  die "$0: $name contains $sections discontigous pod assignments"
654    if $sections > 1;
655  $makefile =~ s/\0+/join "\n", '', &generate_descrip_mms_1/se;
656
657  die "$0: $name contains NUL bytes" if $makefile =~ /\0/;
658
659# Looking for rules like this
660# [.lib.pod]perl.pod : [.pod]perl.pod
661#       @ If F$Search("[.lib]pod.dir").eqs."" Then Create/Directory [.lib.pod]
662#       Copy/NoConfirm/Log $(MMS$SOURCE) [.lib.pod]
663
664  $makefile =~ s/\n\Q[.lib.pod]\Eperl[^\n\.]*\.pod[^\n]+\n
665                 [^\n]+\n       # Another line
666                 [^\n]+\Q[.lib.pod]\E\n         # ends [.lib.pod]
667                    /\0/gsx;
668  $sections = () = $makefile =~ m/\0+/g;
669  die "$0: $name contains no copy rules" if $sections < 1;
670  die "$0: $name contains $sections discontigous copy rules"
671    if $sections > 1;
672  $makefile =~ s/\0+/join "\n", '', &generate_descrip_mms_2/se;
673  $makefile;
674}
675
676# Do stuff
677
678my $built;
679while (my ($target, $name) = each %Targets) {
680  next unless $Build{$target};
681  $built++;
682  if ($target eq "toc") {
683    print "Now processing $name\n" if $Verbose;
684    &output_perltoc;
685    print "Finished\n" if $Verbose;
686    next;
687  }
688  print "Now processing $name\n" if $Verbose;
689  open THING, $name or die "Can't open $name: $!";
690  my @orig = <THING>;
691  my $orig = join '', @orig;
692  close THING;
693  my @new = do {
694    no strict 'refs';
695    &{"do_$target"}($target, @orig);
696  };
697  my $new = join '', @new;
698  if ($new eq $orig) {
699    print "Was not modified\n" if $Verbose;
700    next;
701  }
702  rename $name, "$name.old" or die "$0: Can't rename $name to $name.old: $!";
703  open THING, ">$name" or die "$0: Can't open $name for writing: $!";
704  print THING $new or die "$0: print to $name failed: $!";
705  close THING or die die "$0: close $name failed: $!";
706}
707
708warn "$0: was not instructed to build anything\n" unless $built;
Note: See TracBrowser for help on using the repository browser.