#!/usr/bin/perl # svn2html2.pl - Convert the XML output of "svn log" to (X)HTML # Copyright (C) 2004 Anderson Lizardo # Kevin P. Fleming # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA use strict; use warnings; use Getopt::Long; use Pod::Usage; use POSIX qw(strftime); use XML::Parser; # Commit messages may be in Unicode, so we need to tell Perl about it binmode(STDOUT, ":utf8"); # Output only the latest $entries_limit author(s) commits my $entries_limit = 10; my $help = 0; my $man = 0; my $infile = ""; my $project = ""; my $with_filename = 0; my $with_branchname = 0; GetOptions( "help" => \$help, "man" => \$man, "infile=s" => \$infile, "with-filename", \$with_filename, "with-branchname", \$with_branchname, "project=s" => \$project, ) or pod2usage(1); pod2usage(1) if $help; pod2usage(1) unless $project; pod2usage(-exitstatus => 0, -verbose => 2) if $man; my $svnproject = "svn://svn.linuxfromscratch.org/$project"; my $date = ""; # Current date my $buffer = ""; # Current text in buffer my $author = ""; # Current author my %messages; # Commit messages, hashed by project/date/author my %files; # Files affected by commit, hashed by branch/tag my $entry_count = 0; my %users = load_mapfile(); # user <-> Full Name conversion map my %projects = load_projectmap(); my $parser = new XML::Parser( Handlers => { Start => \&handle_StartTag, End => \&handle_EndTag, Char => \&handle_Text, }, ); if ($infile) { eval { $parser->parsefile($infile) } or pod2usage("$0: $@"); } else { open(LOG, "svn log --verbose --xml $svnproject |"); $parser->parse(\*LOG); close LOG; } sub handle_StartTag { $buffer = ""; } sub handle_EndTag { my (undef, $tag) = @_; if ($tag eq "author") { $author = $users{$buffer} ? $users{$buffer} : $buffer; } elsif ($tag eq "date" and $buffer =~ /^(\d{4}-\d{2}-\d{2})T/) { # Flush buffer if date has changed # print_log() if ($date and $1 ne $date); $date = $1; } elsif ($tag eq "path") { my $project; # sort the project keys in descending order by length, # to ensure that subproject entries are not assigned # to their parent project foreach (sort { length $b <=> length $a } (keys %projects)) { if ($buffer =~ /^\/$_/) { $project = $_; last; } } if ($project) { $buffer =~ s/^\/$project\/*//; my @path = split('/', $buffer); my $fileskey = "root"; my $subdir = shift @path; if ($subdir) { if (($subdir eq "branches") or ($subdir eq "tags")) { # check if this path is for an item within the branch/tag, or # for the branch/tag itself if ($#path >= 1) { $fileskey = $subdir . "/" . shift @path; } else { unshift @path, $subdir; $fileskey = "root"; } } elsif ($subdir eq "trunk") { # check if this path is for an item within the subdir, or # for the subdir itself if ($#path >= 0) { $fileskey = $subdir; } else { unshift @path, $subdir; $fileskey = "root"; } } else { unshift @path, $subdir; } } unshift @{$files{$project}{$fileskey}}, File::Spec->catdir(join('/', @path)); } } elsif ($tag eq "msg") { my $message; # Remove ASCII "bullets" ("* like this") from commit messages $buffer =~ s/^\s*\*\s+//; chomp $buffer; foreach my $project (keys %files) { $message = $buffer; if ($with_filename and $with_branchname) { my $files; my $msg; foreach my $branch (keys %{$files{$project}}) { $msg = $branch . ": "; $files = join(", ", @{$files{$project}{$branch}}); $files =~ s/, $//; $msg .= "(" . $files . ") "; $msg .= $message; chomp $message; unshift @{$messages{$project}{$date}{$author}}, $msg; } } elsif ($with_branchname) { my $branches; foreach (keys %{$files{$project}}) { $branches .= $_ . ", "; } $branches =~ s/, $/: /; $message = $branches . $message; unshift @{$messages{$project}{$date}{$author}}, $message; } elsif ($with_filename) { my $files; foreach my $parent (keys %{$files{$project}}) { foreach my $file (@{$files{$project}{$parent}}) { if ($parent eq "root") { $files .= $file; } else { $files .= $parent . "/" . $file; } $files .= ", "; } } $files =~ s/, $/: /; $message = $files . $message; unshift @{$messages{$project}{$date}{$author}}, $message; } else { unshift @{$messages{$project}{$date}{$author}}, $message; } } undef %files; } elsif ($tag eq "log") { print_log(); } } sub handle_Text { my (undef , $text) = @_; # Encode "special" entities $text =~ s/\&/\&/g; $text =~ s//\>/g; #$text =~ s/\"/\"/g; #$text =~ s/\'/\'/g; # Add current text to the buffer $buffer .= $text; } # Convert ISO 8601 date (yyyy-mm-dd) to the specified format sub isodate2any { my ($date, $format) = @_; if ($date =~ /(\d{4})-(\d{2})-(\d{2})/) { return strftime($format, 0, 0, 0, $3, $2 - 1, $1 - 1900); } else { return undef; } } sub print_log { print "\n"; undef %messages; } sub load_mapfile { my %map; my $map_file = '/etc/passwd'; open(PASSWD, $map_file) || die "Could not open $map_file\: $!"; while () { chomp; my @user = split ':'; my $login = $user[0]; my ($fn) = split(',', $user[4]); $map{$login} = $fn; } close PASSWD; return %map; } sub load_projectmap { my %map; my $parent; my $current; my $desc; open(LEVEL1, "svn list $svnproject |"); while () { chomp; chop; $desc = qx!svn propget project_desc $svnproject/$_!; chomp $desc; if ($desc) { $map{$_} = $desc; } else { my $level1 = $_; open(LEVEL2, "svn list $svnproject/$level1 |"); while () { chomp; chop; $desc = qx!svn propget project_desc $svnproject/$level1/$_!; chomp $desc; if ($desc) { $map{$level1 . "/" . $_} = $desc; } } close LEVEL2; } } close LEVEL1; return %map; } __END__ =head1 NAME svn2html2.pl - convert the XML output of "svn log" to (X)HTML =head1 SYNOPSIS svn2html2.pl --project project_name [--help|--man] [--with-filename] [--with-branchname] [--infile xml_file] Options: --project Specifies the top-level project name to process --infile Parse XML from a file --with-branchname Prepend branch names to commit messages --with-filename Prepend filenames to commit messages --help Show brief help message --man Full documentation =head1 DESCRIPTION B converts the XML code produced by "svn log --xml" to HTML or XHTML code. =head1 OPTIONS =over =item B<--project project_name> Specifies the top-level project to process from the Subversion repository. This program will scan all first- and second-level directories in that project for svn properties called "project_desc"; any directory found with that property will have its log messages processed and grouped together, and the value of that property will be used the as description of the project in the resulting HTML. =item B<--infile xml_file> Specify which XML file to parse. This file must be the output of "svn log --xml". By default, B reads XML code from standard input. =item B<--with-branchname> This option prepends branch names to each commit message. =item B<--with-filename> This option prepends filenames to each commit message. =item B<--help> Print a brief help message and exits. =item B<--man> Print the manual page and exits. =back =head1 AUTHOR Copyright (C) 2004 Anderson Lizardo Kevin P. Fleming This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. =cut