#!/usr/bin/perl
###########################################
# hd - Diff files side by side in HTML
# Derived from 'hdiff' (Bonsai team)
# Mike Schilli, 2002 ([email protected])
###########################################
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 = <F>;
   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} = <FILE>);
   $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 '', <PIPE>;
   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 "&nbsp;\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/&/&amp;/g;
              s/</&lt;/g; s/>/&gt;/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)));
}