| #!/usr/bin/perl -w |
| # |
| # svn-graph.pl - produce a GraphViz .dot graph for the branch history |
| # of a node |
| # |
| # ==================================================================== |
| # Copyright (c) 2000-2004 CollabNet. All rights reserved. |
| # |
| # This software is licensed as described in the file COPYING, which |
| # you should have received as part of this distribution. The terms |
| # are also available at http://subversion.tigris.org/license-1.html. |
| # If newer versions of this license are posted there, you may use a |
| # newer version instead, at your option. |
| # |
| # This software consists of voluntary contributions made by many |
| # individuals. For exact contribution history, see the revision |
| # history and logs, available at http://subversion.tigris.org/. |
| # ==================================================================== |
| # |
| # TODO: |
| # - take some command line parameters (url, start & end revs, |
| # node we're tracking, etc) |
| # - calculate the repository root at runtime so the user can pass |
| # the node of interest as a single URL |
| # - produce the graphical output ourselves (SVG?) instead |
| # of using .dot? |
| # |
| |
| use strict; |
| |
| # Turn off output buffering |
| $|=1; |
| |
| require SVN::Core; |
| require SVN::Ra; |
| |
| # CONFIGURE ME: The URL of the Subversion repository we wish to graph. |
| # See TODO. |
| my $REPOS_URL = 'file:///some/repository'; |
| #my $REPOS_URL = 'http://svn.collab.net/repos/svn'; |
| |
| # Point at the root of a repository so we get can look at |
| # every revision. |
| my $ra = SVN::Ra->new($REPOS_URL); |
| |
| # We're going to look at all revisions |
| my $youngest = $ra->get_latest_revnum(); |
| my $startrev = 1; |
| |
| # This is the node we're interested in |
| my $startpath = "/trunk"; |
| |
| # 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 = (); |
| |
| # This function is a callback which is called for every revision |
| # as we traverse |
| 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 = $$changed_paths{$path}->copyfrom_rev; |
| 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; |
| } |
| |
| # If this is a copy, work out if it was from somewhere interesting |
| if (defined($copyfrom_path) && |
| exists($interesting{$copyfrom_path.":".$copyfrom_rev})) |
| { |
| $interesting{$path.":".$revision} = 1; |
| $tracking{$path} = $revision; |
| print "\t\"$copyfrom_path:$copyfrom_rev\" -> "; |
| print " \"$path:$revision\" [label=\"copy at r$revision\",weight=1,color=green];\n"; |
| $copysource{"$copyfrom_path:$copyfrom_rev"} = 1; |
| $copydest{"$path:$revision"} = 1; |
| } |
| |
| # For each change, we'll move up the path, 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 it's tracking revision. |
| while ($path =~ m:/:) { |
| 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:/[^/]+$::; |
| } |
| } |
| |
| } |
| |
| # And we can do it all with just one call to SVN :) |
| print "digraph tree {\n"; |
| |
| $ra->get_log(['/'], $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 it's 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\" [weight=100,label=\"$changecount change(s)\",style=bold];"; |
| last; |
| } |
| $changecount++; |
| $nextchange = $codeline_changes_forward{$nextchange}; |
| } |
| print "\n"; |
| } |
| } |
| |
| print "}\n"; |
| print STDERR "\n"; |