use strict;
use Getopt::Std;
$|=1;
require SVN::Core;
require SVN::Ra;
require SVN::Client;
my $repos_url;
my $youngest;
my $startrev;
my $startpath;
parse_commandline();
my $auth = (new SVN::Client())->auth;
my $ra = SVN::Ra->new(url => $repos_url, auth => $auth);
if ($youngest eq 'HEAD')
{
$youngest = $ra->get_latest_revnum();
}
my %interesting = ("$startpath:$startrev", 1);
my %tracking = ("$startpath", $startrev);
my %codeline_changes_forward = ();
my %codeline_changes_back = ();
my %copysource = ();
my %copydest = ();
write_graph_descriptor();
sub parse_commandline
{
my %cmd_opts;
my $usage = "
usage: svn-graph.pl [-r START_REV:END_REV] [-p PATH] REPOS_URL
-r the revision range (defaults to 0 through HEAD)
-p the repository-relative path (defaults to /trunk)
-h show this help information (other options will be ignored)
";
$cmd_opts{'r'} = '1:HEAD';
$cmd_opts{'p'} = '/trunk';
getopts('r:p:h', \%cmd_opts) or die $usage;
die $usage if scalar(@ARGV) < 1;
$repos_url = $ARGV[0];
$cmd_opts{'r'} =~ m/(\d+)(:(.+))?/;
if ($3)
{
$youngest = ($3 eq 'HEAD' ? $3 : int($3));
$startrev = int($1);
}
else
{
$youngest = ($3 eq 'HEAD' ? $1 : int($1));
$startrev = 1;
}
$startpath = $cmd_opts{'p'};
if ($cmd_opts{'h'})
{
print($usage);
exit 0;
}
}
sub process_revision
{
my $changed_paths = shift;
my $revision = shift || '';
my $author = shift || '';
my $date = shift || '';
my $message = shift || '';
my $pool = shift;
foreach my $path (keys %$changed_paths)
{
my $copyfrom_path = $$changed_paths{$path}->copyfrom_path;
my $copyfrom_rev = undef;
my $action = $$changed_paths{$path}->action;
if ($action eq 'D' and exists($tracking{$path}))
{
print "\t\"$path:$tracking{$path}\" ";
print "[label=\"$path:$tracking{$path}\\nDeleted in r$revision\",color=red];\n";
delete($tracking{$path});
next;
}
if (defined($copyfrom_path))
{
$copyfrom_rev = $tracking{$copyfrom_path};
}
if (defined($copyfrom_rev) &&
exists($interesting{$copyfrom_path . ':' . $copyfrom_rev}))
{
$interesting{$path . ':' . $revision} = 1;
$tracking{$path} = $revision;
print "\t\"$copyfrom_path:$copyfrom_rev\" -> ";
print " \"$path:$revision\"";
print " [label=\"copy at r$revision\",color=green];\n";
$copysource{"$copyfrom_path:$copyfrom_rev"} = 1;
$copydest{"$path:$revision"} = 1;
}
do
{
if (exists($tracking{$path}) && $tracking{$path} != $revision)
{
$codeline_changes_forward{"$path:$tracking{$path}"} =
"$path:$revision";
$codeline_changes_back{"$path:$revision"} =
"$path:$tracking{$path}";
$interesting{$path . ':' . $revision} = 1;
$tracking{$path} = $revision;
}
$path =~ s:/[^/]*$::;
} until ($path eq '');
}
}
sub write_graph_descriptor
{
print "digraph tree {\n";
print "\tgraph [bgcolor=white];\n";
print "\tnode [color=lightblue2, style=filled];\n";
print "\tedge [color=black, labeljust=r];\n";
print "\n";
$ra->get_log(['/'], $startrev, $youngest, 0, 1, 0, \&process_revision);
foreach my $codeline_change (keys %codeline_changes_forward)
{
if (exists($codeline_changes_back{$codeline_change}) &&
!exists($copysource{$codeline_change}))
{
next;
}
if (!exists($codeline_changes_back{$codeline_change}) or
exists($copysource{$codeline_change}) )
{
print "\t\"$codeline_change\" -> ";
my $nextchange = $codeline_changes_forward{$codeline_change};
my $changecount = 1;
while (defined($nextchange))
{
if (exists($copysource{$nextchange}) or
!exists($codeline_changes_forward{$nextchange}) )
{
print "\"$nextchange\" [label=\"$changecount change";
if ($changecount > 1)
{
print 's';
}
print '"];';
last;
}
$changecount++;
$nextchange = $codeline_changes_forward{$nextchange};
}
print "\n";
}
}
print "}\n";
}