File cvs-exp.pl of Package gource
#!/usr/bin/perl -w
# "cvs log" is reasonably good for tracking changes to a single file, but
# for an entire project with lots of branches and tags, it provides _no_
# idea of what's happening on the whole. This cvs-exp.pl works on the output
# of cvs log and arranges events chronologically, merging entries as needed.
# by keeping track of lists of files and revisions, it figures when in the
# sequence of events were various tags and branches created. It prints out a
# cute-looking tree at the end to summarize activity in the repository.
#
# cvs-exp.pl: a global-picture chronological "cvs log" postprocessor
# author: Sitaram Iyer <ssiyer@cs.rice.edu> 11jan00, one afternoon's work
# Copyright (C) 2000 Rice University
# available at http://www.cs.rice.edu/~ssiyer/code/cvs-exp/
#
# cvs-exp.pl, a wrapper around (and a spoof of) the "cvs log" command, is a
# way of chronologically tracking events in a cvs repository, when you're
# lost among lots of branches and tags and little idea of what's going on.
#
# CHANGELOG
#
# Bugfix contributed by Adam Bregenzer <adam@bregenzer.net> on Oct 24, 2002
# to allow for spaces in filenames.
#
# david.fleck@ugs.com (July 2004):
# added -before, -changeset, -search, -info, -last, and -remote functions.
#
# Bugfix from Peter Conrad <conrad@tivano.de> on Nov 30, 2004
# to remove a superfluous line with a "," if a file has a long pathname
# so two reasons for writing this:
# * I managed a CVS repository for a tolerably largeish project recently,
# and had lots of trouble keeping up with the overall state of the
# project, especially when people kept making mistakes of committing to
# the wrong branches etc. This would've proved quite useful then.
# * I like writing _long_ cvs commit descriptions, so a significant part of
# documentation in my code happens to be scattered among a gazillion
# files in a CVS repository, and I wouldn't mind merging them nicely and
# chronologically.
# status:
# sorting and merging text - sortof.
# list of files changed - yes
# tag support - yes: 'twas hard
# vendor tag support - yes
# branch support - yes
# NOTE: a newly created branch with no commits inside will not show up.
# this is because CVS works that way, and nothing can be done.
#
# NOTE: tagging files in a new branch will really tag the parent. weird CVS.
#
# NOTE: create branch, it says CREATE BRANCH, then add some files onto the
# trunk, and it says CREATE BRANCH again for *those* files. this is
# really what happens, since the branch is created for those files
# only at that point. so this isn't a bug either.
#
# BUGS: infinite nesting in e.g.413.
#
# BUGS: sorting is not polished up yet, so sometimes entries with almost the
# same timestamp are repeated - e.g. with Initial revision and Import
use strict;
my $opt_files = 1;
my $opt_log = 1;
my $opt_tree = 1;
my $cset = 0;
my $log = "log";
my $before = 0;
my $lastonly = 0;
my $searchstring = 0;
my $searchdate = 0;
my $case = "";
my @dirs;
while ($#ARGV >= 0) {
for ($ARGV[0]) {
/-nofiles/ && do {
shift; $opt_files = 0;
last;
};
/-nolog/ && do {
shift; $opt_log = 0;
last;
};
/-notree/ && do {
shift; $opt_tree = 0;
last;
};
/-c.*/ && do {
shift; $cset = shift;
last;
};
/-s.*/ && do {
shift; $searchstring = shift;
last;
};
/-i.*/ && do {
shift; $searchdate = shift;
last;
};
/-l.*/ && do {
shift; $lastonly = 1;
last;
};
/-h.*/ && do {
usage();
exit();
};
/-b.*/ && do {
shift; $before = 1;
last;
};
/-r.*/ && do {
shift; $log = "rlog";
last;
};
/.*/ && do {
push(@dirs, (shift));
last;
};
}
}
if (-t) {
open(CVS, "cvs $log ".join(' ',@dirs)." 2> /dev/null |") || die "can't execute cvs: $!\n";
} else {
*CVS = *STDIN;
}
my $dash = "-"x28;
my $ddash = "="x77;
my $inheader = 1;
my %symbtag = ();
my %tagnfiles = ();
my ($file, $branch, %d, $txt, $rev, $date, %tagnfiles1, @commitTable);
# create an array of hashes (@commitTable) containing entries for each commit.
fileentry:
while (<CVS>) {
chomp;
if (!$inheader && ($_ eq $dash || $_ eq $ddash)) {
my $k = join(' ', map { s/\s*;$//; "$file:$_" } (split(/\s+/,$branch)));
%d = ( 'file' => $file,
'frev' => "$file:$rev",
'txt' => $txt,
'rev' => $rev,
'date' => $date,
'branch' => $k,
);
push @commitTable, { %d };
}
if ($_ eq $dash) {
$inheader = 0;
$_ = <CVS>; chomp; s/revision\s*//; $rev = $_;
$_ = <CVS>; chomp; $date = $_;
$_ = <CVS>; chomp;
$txt = "";
$branch = (/^branches:\s*(.*)$/) ? $1 : do { $txt .= "$_\n"; ""; };
} elsif ($_ eq $ddash) {
$inheader = 1; undef $file;
next fileentry;
} else {
if ($inheader) {
# $file = $1 if (/^Working file: (.*)$/);
$file = $1 if (/^RCS file: (.*)(,v)?$/);
if (/^\t([^\s]*)\s*:\s*([^\s]*)\s*$/) {
die if (!defined($file));
my ($tag,$ver) = ($1,$2);
# if $ver has an even number of dot-separateds and the
# second-to-last is zero, then wipe it. this is a branch.
my @ver = split(/\./,$ver);
$ver = join('.',(@ver[0..$#ver-2],$ver[$#ver]))
if ($#ver >= 1 && ($#ver % 2) && $ver[$#ver-1] eq "0");
defined($tagnfiles{$tag}) ?
do { $tagnfiles{$tag}++ } :
do { $tagnfiles{$tag} = 1 };
my @t = (defined($symbtag{$file}{$ver}) ?
@{ $symbtag{$file}{$ver} } : ());
push @t, $tag;
$symbtag{$file}{$ver} = [ @t ];
}
} else {
$txt .= "$_\n";
}
}
}
foreach (keys(%tagnfiles)) { $tagnfiles1{$_} = $tagnfiles{$_}; } # backup
@commitTable = sort { cmpval($a) cmp cmpval($b) } @commitTable;
# merge consecutive entries with the same text - not all entries, note.
my $m = "garbage";
my @mtable = ();
foreach (@commitTable) {
my %entry = %{$_};
if ($m eq $entry{txt}) { # then skip
$mtable[$#mtable]{frev} .= " " . $entry{frev};
$mtable[$#mtable]{file} .= " " . $entry{file};
foreach my $tag (@{ $symbtag{$entry{file}}{$entry{rev}} }) {
$tagnfiles{$tag}--;
unshift (@{$mtable[$#mtable]{tags}},$tag) if ($tagnfiles{$tag} <= 0);
}
} else {
$m = $entry{txt};
$entry{tags} = ();
foreach my $tag (@{ $symbtag{$entry{file}}{$entry{rev}} }) {
$tagnfiles{$tag}--;
unshift (@{$entry{tags}},$tag) if ($tagnfiles{$tag} <= 0);
}
push @mtable, { %entry };
}
}
# Having slurped in all the info. from the 'cvs log' command, now figure out
# how to output it.
my %child = ();
my $iter = 0;
# return revision numbers of *all* files as they were prior to commit ($cset).
if ($before) {
unless ($cset) {
print "A -changeset value must be provided for this option.\n";
}
my (%entry, %latest) = ();
xprint("State of repository before change ",sprintf("%06u:\n", $cset));
for (my $i = 0; $i<$cset; $i++) {
%entry = %{$mtable[$i]};
foreach (split(/ /,$entry{frev})) {
my ($file, $rev) = split(/:/);
$latest{$file} =$rev;
}
}
%entry = %{$mtable[$cset]};
my $b = brname($entry{frev});
($b ne "") && xprint("BRANCH [$b]\n");
xprint("$entry{txt}\n");
xprint("($entry{date})\n");
foreach (sort keys %latest) {
xprint($_,":",$latest{$_},"\n");
}
foreach (@{$entry{tags}}) {
xprint("*** CREATE TAG [$_]" . nfiles($_));
push @{$child{$b}}, "t $_";
}
foreach (split(/\s+/,brname($entry{branch}))) {
xprint("*** CREATE BRANCH [$_]" . nfiles($_));
push @{$child{$b}}, "b $_";
}
xprint("" . ("="x78) . "\n");
exit(0);
}
# return data only on a single specific change, then exit.
# re-arrange the output format to make life easy for cvs-undo.pl
if ($cset) {
xprint(sprintf("\n%06u:\n", $cset));
my %entry = %{$mtable[$cset]};
my $b = brname($entry{frev});
($b ne "") && xprint("BRANCH [$b]\n");
xprint("$entry{txt}\n");
xprint("($entry{date})\n");
xprint(join("\n",split(/ /,$entry{frev})) . "\n");
foreach (@{$entry{tags}}) {
xprint("*** CREATE TAG [$_]" . nfiles($_));
push @{$child{$b}}, "t $_";
}
foreach (split(/\s+/,brname($entry{branch}))) {
xprint("*** CREATE BRANCH [$_]" . nfiles($_));
push @{$child{$b}}, "b $_";
}
xprint("" . ("="x78) . "\n");
exit(0);
}
# return data on all changes whose date component matches '$searchdate', then
# exit.
if ($searchdate) {
my $nseen = 0;
my $laststring = "";
($lastonly) && ($laststring = "last instance of ");
print "Searching for $laststring\'$searchdate\'\n";
for (my $i = 0; $i <= $#mtable; $i++) {
my %entry = %{$mtable[$i]};
if ($entry{'date'} =~ /$searchdate/) {
$nseen = $i;
unless ($lastonly) {
xprint(sprintf("\n%06u:", $i));
my $b = brname($entry{frev});
($b ne "") && xprint("\nBRANCH [$b]\n");
xprint("\n($entry{date})\n");
xprint(wraprint($entry{frev},77," | ",",") . "\n `" . ("-"x40) . "\n");
xprint("\n$entry{txt}\n");
foreach (@{$entry{tags}}) {
xprint("*** CREATE TAG [$_]" . nfiles($_));
push @{$child{$b}}, "t $_";
}
foreach (split(/\s+/,brname($entry{branch}))) {
xprint("*** CREATE BRANCH [$_]" . nfiles($_));
push @{$child{$b}}, "b $_";
}
xprint("" . ("="x78) . "\n");
}
}
}
if (($lastonly) && ($nseen)) {
my %entry = %{$mtable[$nseen]};
xprint(sprintf("\n%06u:", $nseen));
my $b = brname($entry{frev});
($b ne "") && xprint("\nBRANCH [$b]\n");
xprint("\n($entry{date})\n");
xprint(wraprint($entry{frev},77," | ",",") . "\n `" . ("-"x40) . "\n");
xprint("\n$entry{txt}\n");
foreach (@{$entry{tags}}) {
xprint("*** CREATE TAG [$_]" . nfiles($_));
push @{$child{$b}}, "t $_";
}
foreach (split(/\s+/,brname($entry{branch}))) {
xprint("*** CREATE BRANCH [$_]" . nfiles($_));
push @{$child{$b}}, "b $_";
}
xprint("" . ("="x78) . "\n");
}
print "No relevant entries found\n" if not ($nseen);
exit(0);
}
# return data on all changes whose text component matches '$searchstring', then
# exit.
if ($searchstring) {
my $nseen = 0;
print "Searching for \'$searchstring\'\n";
foreach (@mtable) {
my %entry = %{$_};
if ($entry{'txt'} =~ /$searchstring/) {
$nseen++;
xprint(sprintf("\n%06u:", $iter));
my $b = brname($entry{frev});
($b ne "") && xprint("\nBRANCH [$b]\n");
xprint("\n($entry{date})\n");
xprint(wraprint($entry{frev},77," | ",",") . "\n `" . ("-"x40) . "\n");
xprint("\n$entry{txt}\n");
foreach (@{$entry{tags}}) {
xprint("*** CREATE TAG [$_]" . nfiles($_));
push @{$child{$b}}, "t $_";
}
foreach (split(/\s+/,brname($entry{branch}))) {
xprint("*** CREATE BRANCH [$_]" . nfiles($_));
push @{$child{$b}}, "b $_";
}
xprint("" . ("="x78) . "\n");
}
$iter++;
}
print "No relevant entries found\n" if not ($nseen);
exit(0);
}
# return all data, with changeset number prepended.
foreach (@mtable) {
xprint(sprintf("\n%06u:", $iter));
my %entry = %{$_};
my $b = brname($entry{frev});
($b ne "") && xprint("\nBRANCH [$b]\n");
xprint("\n($entry{date})\n");
($opt_files) &&
xprint(wraprint($entry{frev},77," | ",",") . "\n `" . ("-"x40) . "\n");
xprint("\n$entry{txt}\n");
foreach (@{$entry{tags}}) {
xprint("*** CREATE TAG [$_]" . nfiles($_));
push @{$child{$b}}, "t $_";
}
foreach (split(/\s+/,brname($entry{branch}))) {
xprint("*** CREATE BRANCH [$_]" . nfiles($_));
push @{$child{$b}}, "b $_";
}
xprint("" . ("="x78) . "\n");
$iter++;
}
my %seen = ();
do { print "HEAD\n"; print_branch("", ()); } if ($opt_tree);
### End main ###
sub nfiles { " <$tagnfiles1{$_[0]} file".($tagnfiles1{$_[0]}==1?"":"s").">\n" }
sub xprint { print @_ if ($opt_log); }
sub brname {
my %b = ();
my $line = $_[0];
while ($line =~ s/(.+?):([0-9.]+)\s*//) {
my $file = $1;
my $ver = $2;
$ver =~ s/;$//;
my @ver = split(/\./,$ver);
pop @ver if ($#ver % 2);
$ver = join('.',@ver);
my $x = $symbtag{$file}{$ver};
$b{@{$x}[0]} = 1 if (defined($x));
}
return join(' ',keys(%b));
}
# complicated sort/merge:
# sort on timestamp, largely.
# however, if two entries have the same hours and minutes, then *merge* on
# text and sort on timestamp of the first one anyway.
# XXX ABOVE NOT YET FULLY IMPLEMENTED.
sub cmpval {
my %a = %{$_[0]}; my $s = $a{date};
$s =~ s/^([^:]*:[^:]*:[^:]*):.*/$1/; $s.$a{rev}.$a{txt}.$a{frev};
}
sub wraprint {
my $n = $_[1];
my $pad = $_[2];
my $join = $_[3];
my $len = 0;
my $res = "";
my $line = $_[0];
while ($line =~ s/(.+?:[0-9.]+)\s*//) {
if ($len + length($1) + 2 < $n) {
do { $res .= "$join "; $len += 1+length($join); } if ($res ne "");
} elsif ($len > 0) {
$res .= "$join\n"; $len = 0;
}
do { $res .= $pad; $len += length($pad); } if ($len == 0);
$res .= $1; $len += length($1);
}
return $res;
}
sub print_branch {
$seen{$_[0]} = 1;
my $t = $child{$_[0]};
my @t = (defined($t)?@{$t}:());
shift;
my @were_last = @_;
my $am_last = 0;
foreach (@t) {
$am_last = 1 if ($_ eq $t[$#t]);
/^(.) (.*)/; my $tb = $1; $_ = $2;
print(join('',map {($_?" ":"|")." "} @were_last).($am_last?"`":"|")."- ".($tb eq "b"?"[":"")."$_".($tb eq "b"?"]":"").nfiles($_))
if ($tb ne "b" || !defined($seen{$_}));
print_branch($_, (@were_last,$am_last))
if ($tb eq "b" && !defined($seen{$_}));
}
}
sub usage {
print "Usage:\n",
" cvs-exp.pl [opts] [arguments to cvs log] [dir]\n",
"or cvs log | cvs-exp.pl [opts] \n",
"\n",
"cvs-exp.pl creates a chronological view of the commits, tags, and branching\n",
"events within a CVS module or set of CVS modules. cvs-exp.pl uses the 'cvs log'\n",
"(or 'cvs rlog') command to gather raw materials for formatted output.\n",
"\n",
"Options:\n",
" -nolog don't do the log; only show the tag tree at the end\n",
" -notree vice versa - the default is to print both.\n",
" -nofiles don't list files in the log\n",
"\n",
" The previous three options are overridden by the\n",
" following options:\n",
"\n",
" -before return version information for all files as they were\n",
" before a given changeset. (Requires -changeset .)\n",
" -changeset nn return information on specific checkin nn\n",
" -search str search for all commits whose comments match the regular\n",
" expression 'str'\n",
" -info str search for all commits whose date or author match the\n",
" regular expression 'str'\n",
" -last return only the most recent commit matching a search\n",
" (only meaningful with the '-info' option)\n",
" -remote use 'cvs rlog' rather than 'cvs log'\n",
" -help when all else fails\n";
}