#!/usr/bin/perl -w
# -*- perl -*-
use GD;
use CGI; # replace with P4CGI, if using P4CGI/P4DB
use strict;
use Math::Trig;
#
#################################################################
#
# P4 Branch Graphics
# Original by tshort@cisco.com/tshort@ma.ultranet.com
#
#################################################################
# configurables (P4DB/P4CGI compatible names)
my $FONT; # defaults to gdTinyFont if undefined
# URLS for for doing diffs and such.
# e.g. href="chv.vgi?CH=1234"
my $CHANGELIST_URL = "chv.cgi"; # name of the CGI URL that views changes
my $CHANGELIST_PARAM = "CH"; # name of the changelist parameter for the CGI script that views changes
# e.g. href="fdv.vgi?FSCP=//depot/filename.c&REV=2&ACT=edit"
my $DIFF_URL = "fdv.cgi"; # name of the CGI URL that does diffs
my $DIFF_PARAM = "FSPC"; # name of the file parameter for the CGI script that does diffs
my $DIFF_REV = "REV"; # name of the revision parameter for the CGI script that does diffs
my $DIFF_ACTION = "ACT"; # name of the action parameter for the CGI script that does diffs
# borrowed from P4CGI, if using P4DB/P4CGI, this can be cut out.
$ENV{P4PORT} = "perforce:1666";
$ENV{P4USER} = "perforce";
my $P4 = "/usr/bin/p4";
my $CGI = new CGI;
sub bail {
my $message = shift @_ ;
my $text = shift @_ ;
print
"",
$CGI->header(),
$CGI->start_html(-title => "Error in script",
-bgcolor => "white");
$message = &fixSpecChar($message) ;
print
"
An error has occurred
Sorry!
Message:
$message
" ;
if(defined $text) {
$text = &fixSpecChar($text) ;
print "$text
\n" ;
} ;
print
"Parameters to script:
",
$CGI->dump() ;
print "
",$CGI->end_html() ;
exit 1 ;
}
sub fixSpecChar($ )
{
my $d = shift @_ ;
return "" unless defined $d;
$d =~ s/&/&/g ; # & -> &
$d =~ s/\"/"/g;# " -> "
$d =~ s/</g ; # < -> <
$d =~ s/>/>/g ; # > -> >
return $d ;
}
sub p4call {
my ( $par, @command ) = @_;
my $partype = ref $par ;
die("Called with illegal parameter ref: $partype") if $partype ne "ARRAY";
@$par = ();
open( P4, "$P4 @command|" ) || bail( "p4 @command failed" );
while() {
chomp;
push @$par,$_;
}
close P4;
return;
}
# end from P4CGI, the functions can be replaced with those equivalents
# If using P4CGI/P4DB, add &P4CGI:: if needed to p4call and bail and fixSpecChar
#######
# Parameters:
#
######
$| = 1 ;
#
# Get parameter(s)
#
my $FSPC = $CGI->param("FSPC");
#P4CGI: my $FSPC = &P4CGI::cgi()->param("FSPC");
bail("No file specified") unless defined $FSPC ;
my $TYPE = $CGI->param("TYPE");
#P4CGI: my $TYPE = &P4CGI::cgi()->param("TYPE");
$TYPE = "html" unless defined $TYPE;
my @filelog;
my %filerev;
my %filename;
my %fileuser;
my %filechange;
my %filefromfile;
my %fileboxid;
my %filecol;
my %fileaction;
my %filefromaction;
my %otherfiles;
my @boxids = (0);
p4call(\@filelog,"filelog \"$FSPC\"") ;
bail("No data for file \"$FSPC\"") if @filelog == 0;
my $log;
my $idx;
my $boxid = 1;
my $col = 1;
$filecol{$FSPC} = $col;
$col++;
for ($log = shift @filelog; defined $log; $log = shift @filelog)
{
$_ = fixSpecChar($log) ;
if (/^\.\.\. \#(\d+) \S+ (\d+) (\S+) on (\S+) by (\S*)@(\S*) (\S*)\s*'(.*)'/ )
{
$idx = $FSPC . "\#" . $1;
$filerev{$idx} = $1;
$filename{$idx} = $FSPC;
$fileuser{$idx} = $5;
$filechange{$idx} = $2;
$fileaction{$idx} = $3;
$fileboxid{$idx} = $boxid;
push(@boxids, $idx);
$boxid++;
}
elsif (/^\.\.\. \.\.\. (copy|merge|delete|branch|edit) from ([^#]+)\#(\d+),\#(\d+)$/)
{
bail("no file?!?") unless defined $idx;
$filefromfile{$idx} = $2 . "\#" . $4;
$filefromaction{$idx} = $1;
$otherfiles{$2} = 1;
}
elsif (/^\.\.\. \.\.\. (copy|merge|delete|branch|edit) from ([^#]+)\#(\d+)$/)
{
bail("no file?!?") unless defined $idx;
$filefromfile{$idx} = $2 . "\#" . $3;
$filefromaction{$idx} = $1;
$otherfiles{$2} = 1;
}
elsif (/^\.\.\. \.\.\. (add|copy|merge|delete|branch|edit) into ([^#]+)\#(\d+)$/)
{
bail("no file?!?") unless defined $idx;
$otherfiles{$2} = 1;
}
}
my $file;
my @otherfiles = keys(%otherfiles);
for ($file = shift @otherfiles; defined $file; $file = shift @otherfiles)
{
p4call(\@filelog,"filelog \"$file\"") ;
next if @filelog == 0;
$filecol{$file} = $col;
$col++;
undef $idx;
for ($log = shift @filelog; defined $log; $log = shift @filelog)
{
$_ = fixSpecChar($log) ;
if (/^\.\.\. \#(\d+) \S+ (\d+) (\S+) on (\S+) by (\S*)@(\S*) (\S*)\s*'(.*)'/ )
{
$idx = $file . "\#" . $1;
$filerev{$idx} = $1;
$filename{$idx} = $file;
$fileuser{$idx} = $5;
$filechange{$idx} = $2;
$fileaction{$idx} = $3;
$fileboxid{$idx} = $boxid;
push(@boxids, $idx);
$boxid++;
}
elsif (/^\.\.\. \.\.\. (copy|merge|delete|branch|edit) from ([^#]+)\#(\d+),\#(\d+)/)
{
bail("no file?!?") unless defined $idx;
$filefromfile{$idx} = $2 . "\#" . $4;
$filefromaction{$idx} = $1;
}
elsif (/^\.\.\. \.\.\. (copy|merge|delete|branch|edit) from ([^#]+)\#(\d+)/)
{
bail("no file?!?") unless defined $idx;
$filefromfile{$idx} = $2 . "\#" . $3;
$filefromaction{$idx} = $1;
}
}
}
# Now that we have the history of the main file, and any close files that are one branch away,
# we need to figure out where the arrows are...
my @arrows;
my $i;
for ($i = 1; defined $boxids[$i]; $i++)
{
$idx = $boxids[$i];
if (defined $filefromfile{$idx})
{
my $file = $filefromfile{$idx};
my $action = $filefromaction{$idx} if defined $file;
my $from = $fileboxid{$file} if defined $action;
push(@arrows, "$action $from->$i") if defined $from;
}
}
my @revarrows;
for ($i = 1; defined $boxids[$i]; $i++)
{
$idx = $boxids[$i];
my $rev = $filerev{$idx} + 1;
$file = $filename{$idx};
my $filespec = $file . "\#" . $rev;
if (defined $fileboxid{$filespec})
{
my $to = $fileboxid{$filespec};
push(@revarrows, "$i->$to") if defined $to;
}
}
# determine height of each box based on change
my @changes = sort { $a <=> $b } values %filechange;
my $last = 0;
my $height = 1;
my %changetoheight;
foreach (@changes)
{
next if ($_ == $last);
$last = $_;
$changetoheight{$last} = $height;
$height++;
}
unless (defined $FONT)
{
if ($col < 8) # arbitrary cutoff
{
$FONT = gdMediumBoldFont;
}
elsif ($col < 12) # arbitrary cutoff
{
$FONT = gdSmallFont;
}
else
{
$FONT = gdTinyFont;
}
}
# THESE SHOULD BE EVEN NUMBERS!
my $BOXHEIGHT = $FONT->height * 2 + 6;
my $BOXWIDTH = $FONT->width * 10; # for clXXXXXX with two spaces
my $BOXVSPACE = $FONT->height * 2;
my $BOXHSPACE = $BOXWIDTH / 2;
# returns the centerpoint of the box
sub boxtoxy
{
my $box = $_[0];
my $idx = $boxids[$box];
my $h = $changetoheight{$filechange{$idx}};
my $c = $filecol{$filename{$idx}};
my $x = (($BOXHSPACE + $BOXWIDTH) * $c) - ($BOXWIDTH / 2);
my $y = (($BOXVSPACE + $BOXHEIGHT) * $h) - ($BOXHEIGHT / 2);
return ($x, $y);
}
sub boxrect
{
my ($x, $y) = boxtoxy($_[0]);
$x -= $BOXWIDTH / 2;
$y -= $BOXHEIGHT / 2;
my $x1 = $x + $BOXWIDTH;
my $y1 = $y + $BOXHEIGHT;
return ($x, $y, $x1, $y1);
}
sub nwcorner
{
my ($x, $y) = boxtoxy($_[0]);
$x -= $BOXWIDTH / 2;
$y -= $BOXHEIGHT / 2;
return ($x, $y);
}
sub necorner
{
my ($x, $y) = boxtoxy($_[0]);
$x += $BOXWIDTH / 2;
$y -= $BOXHEIGHT / 2;
return ($x, $y);
}
sub swcorner
{
my ($x, $y) = boxtoxy($_[0]);
$x -= $BOXWIDTH / 2;
$y += $BOXHEIGHT / 2;
return ($x, $y);
}
sub secorner
{
my ($x, $y) = boxtoxy($_[0]);
$x += $BOXWIDTH / 2;
$y += $BOXHEIGHT / 2;
return ($x, $y);
}
sub centertop
{
my ($x, $y) = boxtoxy($_[0]);
$y -= $BOXHEIGHT / 2;
return ($x, $y);
}
sub centerbottom
{
my ($x, $y) = boxtoxy($_[0]);
$y += $BOXHEIGHT / 2;
return ($x, $y);
}
sub centerright
{
my ($x, $y) = boxtoxy($_[0]);
$x += $BOXWIDTH / 2;
return ($x, $y);
}
sub centerleft
{
my ($x, $y) = boxtoxy($_[0]);
$x -= $BOXWIDTH / 2;
return ($x, $y);
}
sub imagesize
{
my ($x, $y);
# $col and $height are one more than the number of columns/height
$x = ($BOXWIDTH + $BOXHSPACE) * $col - $BOXWIDTH;
$y = ($BOXHEIGHT + $BOXVSPACE) * $height - $BOXHEIGHT;
return ($x, $y);
}
# not sure where I got this from... by I didn't write it... P4DB?/p4pr?
sub find_branch_part {
# Strips identical substrings from the beginning and end of
# $origname and $branchname and then returns what remains of
# $branchname.
my($origname, $branchname) = @_;
my(@origname) = split('/', $origname);
my(@branchname) = split('/', $branchname);
while (@origname && ($origname[0] eq $branchname[0])) {
shift @origname;
shift @branchname;
}
while (@origname &&
($origname[@origname-1] eq $branchname[@branchname-1])) {
pop @origname;
pop @branchname;
}
join('/', @branchname);
}
#create image or do the HTML page
my ($x, $y);
my $image;
my $white;
my $blue;
my $green;
my $black;
my $red;
if ($TYPE eq "html")
{
# start the HTML page
#print "",&P4CGI::start_page("Branch Relationships
$FSPC", "BRA", "");
print "Content-type: text/html\n\n";
print "\n\nBranch Relationships $FSPC\n\n";
print "\n";
#end P4CGI
print "
Branch relationships for $FSPC:
\n";
print "
Key:
\n";
print "Each box represents a revision (\#n) and changelist (cln) of a file. Click a box to view the changelist description. A box with a red X indicates the file was deleted.
\n";
print "The title over each column represents the branch name. Click on the title to view the graph from the point of view of that file.
\n";
print "Solid green arrows indicate a revision change. Click on a green arrow to view the diffs between revisions.
\n";
print "Solid red arrows indicate a branch.
\n";
print "Dashed red arrows indicate a merge.
\n";
print "\n";
print "\n\n\n";
#print "\n", &P4CGI::end_page() ;
}
else
{
foreach (@arrows)
{
my ($from, $to);
my ($x1, $y1);
my ($x2, $y2);
if (/(add|branch) (\d+)->(\d+)/)
{
($from, $to) = ($2, $3);
($x1, $y1) = boxtoxy($from);
($x2, $y2) = boxtoxy($to);
if ($x1 < $x2)
{
($x1, $y1) = centerright($from);
($x2, $y2) = nwcorner($to);
}
else
{
($x1, $y1) = centerleft($from);
($x2, $y2) = necorner($to);
}
$image->line($x1, $y1, $x2, $y2, $red);
}
elsif (/(\S+) (\d+)->(\d+)/)
{
($from, $to) = ($2, $3);
($x1, $y1) = boxtoxy($from);
($x2, $y2) = boxtoxy($to);
if ($x1 < $x2)
{
($x1, $y1) = centerright($from);
($x2, $y2) = centerleft($to);
}
else
{
($x1, $y1) = centerleft($from);
($x2, $y2) = centerright($to);
}
$image->dashedLine($x1, $y1, $x2, $y2, $red);
}
if (defined $from)
{
my $h = $FONT->width;
my $poly = new GD::Polygon;
my $angle = atan2 ($y2 - $y1, $x2 - $x1) - pi() / 2;
$poly->addPt(0, 0);
my $c = cos($angle);
my $s = sin($angle);
$x1 = $c * (-$h) - $s * (-$h * 2);
$y1 = $s * (-$h) + $c * (-$h * 2);
$poly->addPt($x1, $y1);
$x1 = $c * $h - $s * (-$h * 2);
$y1 = $s * $h + $c * (-$h * 2);
$poly->addPt($x1, $y1);
$poly->offset($x2, $y2);
$image->filledPolygon($poly, $red);
}
}
print "Content-type: image/png\n\n";
binmode STDOUT;
print $image->png();
}
## tada