#!/usr/bin/perl ########################################### # hd - Diff files side by side in HTML # Derived from 'hdiff' (Bonsai team) # Mike Schilli, 2002 (mschilli1@aol.com) ########################################### use warnings; use strict; use SDiff qw(sdiff); use CGI qw(:all *table *font *pre); use CGI::Carp qw(fatalsToBrowser); use File::Basename; use Set::IntSpan; use Text::Wrap; $| = 1; my $CVS_COMMAND = "/usr/bin/cvs"; my @CVSROOTS = ( ["/home/mschilli/CVSROOT", "My Local CVS"], [":pserver:anonymous\@anoncvs.gimp.org:" . "/cvs/gnome", "Gimp"], [":pserver:mschilli\@east." . "bla.com:/data/cvs/host", "Server"]); my $STABLE_BG_COLOR = "White"; my $ALT_BG_COLOR = "#f0f0f0"; my $SKIPPING_BG_COLOR = "#c0c0c0"; my $HEADER_BG_COLOR = "Orange"; my $CHANGE_BG_COLOR = "LightBlue"; my $ADDITION_BG_COLOR = "LightGreen"; my $DELETION_BG_COLOR = "LightGreen"; my $DIFF_BG_COLOR = "White"; my @MODES = ( [file => "Compare Two Files"], [cvs => "Compare Two CVS revisions"], [chi => "Compare before Check-In"]); my $CONTEXT = 5; my $MAX_LINE_LEN = 80; print header(); if(param("cfg") or !param("mode")) { # Display config page config_page(); exit 0; } my($h1, $h2, $left, $right, $n, @run_list, $nof_lines, $ver); my $cvsroot = (param("aroot") || param("cvsroot")); if(param("mode") eq "file") { # Compare two files $h1 = basename(param("f1")); $h2 = basename(param("f2")); ($left) = readfile(file => param("f1")); ($right) = readfile(file => param("f2")); } elsif(param("mode") eq "chi") { # Compare a local version with CVS ($left, $ver) = readfile( lpath => param('path'), cvsroot => $cvsroot); my $filename = basename(param("path")); $h1 = "$filename (CVS $ver)"; $h2 = "$filename (local copy)"; ($right) = readfile( file => param('path')); } elsif(param("mode") eq "cvs") { # Compare two CVS versions my $f = basename(param("path")); $h1 = "$f (CVS " . param("r1") . ")"; $h2 = "$f (CVS " . param("r2") . ")"; ($left) = readfile( cvsroot => $cvsroot, rpath => param("path"), rev => param("r1")); ($right) = readfile( cvsroot => $cvsroot, rpath => param("path"), rev => param("r2")); } chomp @$left; chomp @$right; my $set = Set::IntSpan->new(); my $diffs = sdiff($left, $right, \$set, $CONTEXT); $nof_lines = @$diffs; if(param("context")) { @run_list = split /,/, $set->run_list(); @run_list = () if $set->empty(); } else { @run_list = ("0-$#$diffs"); } print start_html( {BGCOLOR => $STABLE_BG_COLOR, -title => param("comment")}); param("cfg" => 1); print a({href => self_url}, "Configure"); print start_table( {BGCOLOR => $STABLE_BG_COLOR, RULES => "all", CELLPADDING => 0, CELLSPACING => 0, COLS => 2} ); print TR( {BGCOLOR => $DIFF_BG_COLOR}, th( {colspan => 2}, param("comment")) ); print TR( {BGCOLOR => $HEADER_BG_COLOR}, th($h1), th($h2) ); my $cur_idx = 0; while(@$diffs or @run_list) { my($from, $to); if(@run_list) { ($from, $to) = split /-/, shift @run_list; } if(!defined $from) { # Skip until the end $cur_idx = skip($diffs, $cur_idx, $nof_lines-1); last; } # Just one line? $to = $from unless defined $to; if($cur_idx < $from) { # There are lines to skip $cur_idx = skip($diffs, $cur_idx, $from-1); } for($cur_idx..$to) { my $e = shift @$diffs; my($left, $right, $mod) = @$e; $cur_idx++; my $color = $mod eq "c" ? $CHANGE_BG_COLOR : $mod eq "i" ? $ADDITION_BG_COLOR : $mod eq "d" ? $DELETION_BG_COLOR : $mod eq "u" ? $STABLE_BG_COLOR : "unknown"; $n = sprintf "%2d", $cur_idx; $color = $ALT_BG_COLOR if $mod eq "u" and param("striped") and $n % 2; print TR({BGCOLOR => $color}, td({align => "left"}, type("$n $left")), td({align => "left"}, type("$n $right"))); print "\n"; } } print end_table(); param("cfg", "1"); print a({href => self_url}, "Configure"); print end_html(); ########################################### sub skip { ########################################### my($diffs, $cur_idx, $to) = @_; if($to-$cur_idx > 2*$CONTEXT) { print TR( {BGCOLOR => $SKIPPING_BG_COLOR}, td( { COLSPAN => 2 }, b("Skipping lines ", $cur_idx + 1, "...", $to + 1))); splice @$diffs, 0, $to-$cur_idx+1; $cur_idx = $to+1; } return $cur_idx; } ########################################### sub readfile { ########################################### my (%opts) = @_; my $cmd; # Local file if(exists $opts{file}) { $cmd = "<$opts{file}"; } else { # Get latest CVS version if(!exists $opts{rev}) { cvs_fill_in(\%opts); } # Get file $cmd = "$CVS_COMMAND -Q -d " . "$opts{cvsroot} co -r " . "$opts{rev} -p $opts{rpath} |"; } open F, "$cmd" or die "Cannot open '$cmd'"; my @data = ; close F or die "$cmd failed"; return (\@data, $opts{rev}); } ########################################### sub cvs_fill_in { ########################################### my ($opts) = @_; # Get path within CVS/working path my $rep = dirname($opts->{lpath}) . "/CVS/Repository"; open FILE, "<$rep" or die "Cannot open $rep"; chomp($opts->{rpath} = ); $opts->{rpath} .= "/" . basename($opts->{lpath}); close FILE; # Get cvs version my $wdir = dirname($opts->{lpath}); chdir $wdir or die "Cannot chdir to $wdir"; my $cmd = "$CVS_COMMAND -Q -d " . "$opts->{cvsroot} status " . basename($opts->{lpath}) . " 2>/dev/null"; open PIPE, "$cmd |" or die "Cannot open pipe"; my $data = join '', ; close PIPE or die "$cmd failed"; ($opts->{rev}) = $data =~ /Repository revision:\s*([\d\.]+)/; } ########################################### sub config_page { ########################################### print h1("Configuration"); my $checked = ""; my $current_mode = (param("mode") || "file"); for (@MODES) { my ($mode, $text) = @$_; if($mode eq $current_mode) { print b($text); } else { param("mode", $mode); param("cfg", 1); print a({href => self_url}, $text); } print " \n"; } # Set it back to current mode param("mode", $current_mode); param("cfg", undef); print start_html( {BGCOLOR => $STABLE_BG_COLOR} ); print start_form(-method => "GET"); print start_table({BGCOLOR => $ALT_BG_COLOR}); if($current_mode eq "file") { print TR(td("File 1"), td(textfield(-name => "f1", -size => 50))); print TR(td("File 2"), td(textfield(-name => "f2", -size => 50))); } elsif($current_mode eq "chi") { display_cvs_form(); print TR(td("Local Path and File"), td(textfield(-name => "path", -size => 50))); } elsif($current_mode eq "cvs") { display_cvs_form(); print TR( td("Path to file within CVS"), td(textfield(-name => "path", -size => 50))); print TR(td("Rev 1"), td(textfield(-name => "r1", -size => 10))); print TR(td("Rev 2"), td(textfield(-name => "r2", -size => 10))); } print TR(td("Comment"), td(textfield(-name => "comment", -size => 50))); print end_table(); print hidden(-name => "mode"); print checkbox(-name => "striped", -value => "on"); print checkbox(-name => "context", -value => "on"); print br(); print submit(value => "View Diff"); print end_form(); } ########################################### sub type { ########################################### @_ = map { s/&/&/g; s//>/g; $_ } @_; $Text::Wrap::columns = $MAX_LINE_LEN; $_[0] = join "\n", wrap("", "", $_[0]); return start_pre() . start_font( { FACE => "Lucida Console", SIZE => 1 }) . join('', @_) . end_font(); } ########################################### sub display_cvs_form { ########################################### my %CVSROOTS = map { @$_ } @CVSROOTS; print TR(td("CVS Root"), td(popup_menu( -name => 'cvsroot', -values => [map {$_->[0]} @CVSROOTS], -labels => \%CVSROOTS))); print TR(td("Alternative CVS Root"), td(textfield(-name => "aroot", -size => 50))); }