blob: 0675e8aa92e598c56c8d9874ebc0cd7043895a93 [file] [log] [blame]
#!/usr/bin/perl -w
# vim:ts=2:sw=2:expandtab
#
# svn-graph.pl - produce a GraphViz .dot graph for the branch history
# of a node
#
# ====================================================================
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ====================================================================
#
# View graphs using a command like:
#
# svn-graph.pl file:///tmp/repos | dotty -
#
# TODO:
# - Calculate the repository root at runtime so the user can pass
# the node of interest as a single URL.
# - (Also?) produce the graphical output ourselves (SVG?) instead
# of writing a GraphViz ".dot" data file. This can be done with
# GraphViz using 'dot'.
# - Display svnmerge.py/Subversion merge history.
#
use strict;
use Getopt::Std;
# Turn off output buffering
$|=1;
require SVN::Core;
require SVN::Client;
# The URL of the Subversion repository we wish to graph
# (e.g. "http://svn.apache.org/repos/asf/subversion").
my $repos_url;
# The revision range we operate on, from $startrev -> $youngest.
my $youngest;
my $startrev;
# This is the node we're interested in
my $startpath;
# Set the variables declared above.
parse_commandline();
# The "interesting" nodes are potential sources for copies. This list
# grows as we move through time.
# The "tracking" nodes are the most recent revisions of paths we're
# following as we move through time. If we hit a delete of a path
# we remove it from the tracking array (i.e. we're no longer interested
# in it).
my %interesting = ("$startpath:$startrev", 1);
my %tracking = ("$startpath", $startrev);
my %codeline_changes_forward = ();
my %codeline_changes_back = ();
my %copysource = ();
my %copydest = ();
write_graph_descriptor();
#print STDERR "\n";
# Validate the command-line arguments, and set the global variables
# $repos_url, $youngest, $startrev, and $startpath.
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)
";
# Defaults.
$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 = SVN::Core::uri_canonicalize($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'};
# Print help info (and exit nicely) if requested.
if ($cmd_opts{'h'})
{
print($usage);
exit 0;
}
}
# This function is a callback which is invoked for every revision as
# we traverse change log messages.
sub process_revision
{
my $changed_paths = shift;
my $revision = shift || '';
my $author = shift || '';
my $date = shift || '';
my $message = shift || '';
my $pool = shift;
#print STDERR "$revision\r";
foreach my $path (keys %$changed_paths)
{
my $copyfrom_path = $$changed_paths{$path}->copyfrom_path;
my $copyfrom_rev = undef;
my $action = $$changed_paths{$path}->action;
# See if we're deleting one of our tracking nodes
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;
}
### TODO: Display a commit which was the result of a merge
### operation with [sytle=dashed,color=blue]
# If this is a copy, work out if it was from somewhere interesting
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;
}
# For each change, we'll walk up the path one component at a time,
# updating any parents that we're tracking (i.e. a change to
# /trunk/asdf/foo updates /trunk). We mark that parent as
# interesting (a potential source for copies), draw a link, and
# update its tracking revision.
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 '');
}
}
# Write a descriptor for the graph in GraphViz .dot format to stdout.
sub write_graph_descriptor
{
my $client = SVN::Client->new;
# Begin writing the 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";
# Retrieve the requested history.
$client->log($repos_url, $startrev, $youngest, 1, 0, \&process_revision);
# Now ensure that everything is linked.
foreach my $codeline_change (keys %codeline_changes_forward)
{
# If this node is not the first in its codeline chain, and it isn't
# the source of a copy, it won't be the source of an edge
if (exists($codeline_changes_back{$codeline_change}) &&
!exists($copysource{$codeline_change}))
{
next;
}
# If this node is the first in its chain, or the source of
# a copy, then we'll print it, and then find the next in
# the chain that needs to be printed too
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";
}
}
# Complete the descriptor (delaying write of font size to avoid
# inheritance by any subgraphs).
#my $title = "Family Tree\n$startpath, $startrev through $youngest";
#print "\tgraph [label=\"$title\", fontsize=18];\n";
print "}\n";
}