Menu

[766a1d]: / diff  Maximize  Restore  History

Download this file

431 lines (349 with data), 11.4 kB

#!/usr/bin/perl -T
######################################################################
#
# diff --	Display diff output with markup.
#
#	Arne Georg Gleditsch <argggh@ifi.uio.no>
#	Per Kristian Gjermshus <pergj@ifi.uio.no>
#
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
#
######################################################################

use strict;
use lib do { $0 =~ m{(.*)/} ? "$1/lib" : "lib" }; # if LXR modules are in ./lib

=head1 diff script

This script manages display of code differences between
two versions of a source file.

=cut

use LXR::Common;
use LXR::Markup;
use LXR::Template;
use Local;
use FileHandle;


=head2 C<fflush ()>

Function C<fflush> sets STDOUT in autoflush mode.

B<Note:>

=over

=item

I<The reason for using this function is not clear.
It has been commented out without adverse effect.>

I<Being very short, it could be inlined (only one usage!)
if it needs to be reenabled.>

=back

=cut

# sub fflush {
# 	$| = 1;
# 	print('');
# }


=head2 C<htmljust ($s, $w)>

Function C<htmljust> returns an HTML string justified to exactly
a fixed number of screen positions.

=over

=item 1

C<$s>

a I<string> containing an HTML sequence

=item 2

C<$w>

an I<integer> defining the justification width

=back

The string argument is truncated or expanded to show exactly
C<$w> "characters" on screen.

Atomic units must not be split, otherwise HTML integrity is broken.
HTML tags and entity references are copied without truncation.

When checking overflow, HTML tags are considered as zero-width "characters"
and HTML entity references as one screen position glyphs
(which is not always the case: combining diacritic marks,
zero-width spacers, ...).

When the desired width is met, opening tags may not have been matched
by their closing tags. To return a synctactically correct HTML
sequence, HTML tags are still copied but without their content.
This results in a sequence longer than necessary, but it is safe.

=cut

sub htmljust {
	my ($s, $w) = @_;
	my @s = split(/(<.*?>|&[\#\w\d]+;)/, $s);
	$s = '';

	while (@s){
		my $f = shift(@s);
		next if $f eq '';
		if ('<' eq substr($f, 0, 1)) {
		# HTML tag element: no screen position, copy it
			$s .= $f
		} elsif ('&' eq substr($f, 0, 1)) {
		# HTML entity reference: one screen position usually
		# Copy it space permitting
			if ($w > 0) {
				$s .= $f;
				$w--;
			}
		} else {
		# Ordinary text, check for truncation
			$f = substr($f, 0, $w);
			$w -= length($f);
			$s .= $f;
		}
	}
	# Add spaces up to the requested width
	$s .= ' ' x $w;
	return $s;
}


=head2 C<printdiff (@dargs)>

Procedure C<printdiff> is the main driver for difference display
(two passes).

=over

=item 1

C<@dargs>

an I<array> containing the C<'variables'> values for the reference version

=back

When entered for the first time, query arguments only offer current
C<'variables'> values.
This is detected by the absence of any C<~>I<var_name>C<=>... argument.
Current values are then transfered into these so-called I<remembered>
values and user is requested to choose another version.

On second entry, both current values (I<var_name>C<=>...) and
remembered values (C<~>I<var_name>C<=>...) are present in the
query arguments.
The latter values designate the reference version (in the right pane);
the former values the "new" version (in the left pane).
With these two file descriptions, processing can be done.

The file name in C<$pathname> has been nominally transformed by the
C<'maps'> rules.
But to get the other name, we must first reverse the effects of these
rules (in the remembered environment) et re-apply them (in the current
environment).
Once this is done, both file names correctly point to the desired
versions.

Next, physical (real) files are obtained so that I<rcs B<diff>> can
build the patch directives..

Both files are highlighted by C<markupfile>.
The resulting HTML streams are kept in memory.
I<This could cause a serious strain on memory and degrade performance
(because of swapping for instance).>

Then it is relatively simple to merge both streams line by line
under control of the patch directives.


=cut

sub printdiff {
	my (@dargs) = @_;

	if ($#dargs < 0) {
	# First pass through the script
	# Request second version
		my @vars;
		foreach ($config->allvariables) {
			if	(!exists($config->{'variables'}{$_}{'when'})
				|| eval($config->varexpand($config->{'variables'}{$_}{'when'}))
				) {
				push(@vars, $config->vardescription($_));
			}
		}

		$vars[ $#vars - 1 ] .= ' or ' . pop(@vars) if $#vars > 0;

		print	( "<p align=\"center\">\n"
				, "Please indicate the version of the file you wish to\n"
				, "compare to by clicking on the appropriate\n"
				, join(', ', @vars)
				, " button.\n"
				, "</p>\n"
				);
		return;
	}

	# Second pass - both versions are known
	if ('/' eq substr($pathname, -1)) {
		print("<h3 align=\"center\">Diff not yet supported for directories.</h3>\n");
		return;
	}
	my $origname = $pathname;
	# Tentatively reverse the effect of mappath on $pathname to get an "early bird"
	# skeleton path on which to apply the mapping rules in the current environment.
	my $diffname = $config->mappath($config->unmappath($pathname, @dargs));
	my ($diffv) = grep(m/v=/, @dargs);
	$diffv =~ s/v=//;

	unless ($files->isfile($origname, $diffv)) {
		print("<p class='error'>*** $origname does not exist in version $diffv ***</p>\n");
		return;
	}
	unless ($files->isfile($diffname, $releaseid)) {
		print("<p class='error'>*** $diffname does not exist in version $releaseid ***</p>\n");
		return;
	}

# 	fflush;
	# realfilename may create a temporary file
	# which should be released when no longer needed
	my $origtemp = $files->realfilename($origname, $diffv);
	my $difftemp = $files->realfilename($diffname, $releaseid);
	$ENV{'PATH'} = '/usr/local/bin:/usr/bin:/bin:/usr/sbin';
	unless (open(DIFF, '-|')) {
		open(STDERR, '>&STDOUT');
		exec('diff', '-U0', $difftemp, $origtemp);
		die "*** Diff subprocess died unexpectedly: $!\n";
	}

	my ($leftstart, $leftlen);		# What is replaced in left file
	my ($rightstart, $rightlen);	# What replaces in tight file
	my $facing;						# Number of facing lines
	my $rightxcess;					# Running count of lines in excess at right
	my $leftorg;					# Final real line number at left
	my $dir;						# Change indicator
	my %chg;						# All change indicators
	my $blanks;						# Number of blanks lines to keep abreast
	my (%leftblanks, %rightblanks);

	while (<DIFF>) {
		if	(($leftstart, $leftlen, $rightstart, $rightlen)
				= m/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/
			) {
			$leftstart++  if $leftlen eq '0';
			$rightstart++ if $rightlen eq '0';
			$leftlen  = 1 unless defined($leftlen);
			$rightlen = 1 unless defined($rightlen);

			$leftorg = $leftstart + $rightxcess;
			if ($leftlen < $rightlen) {
				$rightxcess += $rightlen - $leftlen;

				$dir    = '&gt;&gt;';
				$blanks = $rightlen - $leftlen;
				$facing = $leftlen;
				$leftblanks{$leftstart + $leftlen} = $blanks;
			} else {
				$dir    = '&lt;&lt;';
				$blanks = $leftlen - $rightlen;
				$facing = $rightlen;
				$rightblanks{$rightstart + $rightlen} = $blanks;
			}
			foreach (0 .. $facing - 1) {
				$chg{ $leftorg + $_ } = '!!';
			}
			foreach (0 .. $blanks - 1) {
				$chg{ $leftorg + $facing + $_ } = $dir;
			}

		}
	}
	close(DIFF);

	#	Print a descriptive title and tell exactly what versions
	#	are compared (dump the variable value sets)
	my @linkargs = grep {m/(.*?)=(.*)/; $config->variable($1) ne $2;} @dargs;
	map (s/(.*?)=/!$1=/, @linkargs);
	print	( "<h1>Diff markup</h1>\n"
			, '<h2>between '
			, fileref	( $diffname
						, 'diff-fref'
						, $diffname
						, undef
						, @linkargs
						)
			, ' <small>('
			);
	my @fctx;
	for my $var ($config->allvariables) {
		next if	exists($config->{'variables'}{$var}{'when'})
				&& !eval($config->varexpand($config->{'variables'}{$var}{'when'}));
		push (@fctx, $config->vardescription($var).': '.$config->variable($var));
	}
	print	( join(', ', @fctx)
			, ')</small><br>'
			, ' and '
			, fileref	( $origname
						, 'diff-fref'
						, $origname
						)
			, ' <small>('
			);
	@fctx = ();
	for my $var ($config->allvariables) {
		next if	exists($config->{'variables'}{$var}{'when'})
				&& !eval($config->varexpand($config->{'variables'}{$var}{'when'}));
		my ($varval) = grep(m/$var=/, @dargs);
		$varval =~ s/$var=//;
		push (@fctx, $config->vardescription($var).': '.$varval);
	}
	print	( join(', ', @fctx)
			, ")</small></h2><hr>\n"
			);

	#	Highlight both files
	my $origh = FileHandle->new($origtemp);
	#	Save current environment before switching to @dargs environment
	my %oldvars;
	foreach my $arg (@dargs) {
		if ($arg =~ m/(.*?)=(.*)/) {
			$oldvars{$1} = $config->variable($1);
			$config->variable($1, $2);
		}
	}
	my $rightfile;
	markupfile($origh, sub { $rightfile .= shift });
	#	Restore original environment
	while ((my $var, my $val) = each %oldvars) {
		$config->variable($var, $val);
	}
	%oldvars = {};
	$origh->close;
	$files->releaserealfilename($origtemp);

	$pathname = $diffname;

	my $diffh = FileHandle->new($difftemp);
	my $leftfile;
	markupfile($diffh, sub { $leftfile .= shift });
	my $len = $. + $rightxcess;	# Total lines displayed
	$diffh->close;
	$files->releaserealfilename($difftemp);

	$pathname = $origname;

	#	Output both versions side by side
	my $i;
	$i = 0;
	$leftfile  =~ s/^/"\n" x ($leftblanks{$i++})/mge;
	$i = 0;
	$rightfile =~ s/^/"\n" x ($rightblanks{$i++})/mge;

	my @leftlines  = split(/\n/, $leftfile);
	my @rightlines = split(/\n/, $rightfile);

	my $leftwidth = $$HTTP{'param'}{'_diffleftwidth'}
					|| $config->{'diffleftwidth'}
					|| 50;
	print("<pre class=\"filecontent\">\n");
	foreach $i (1 .. $len) {
		my $l = htmljust($leftlines[$i], $leftwidth);
		my $r = $rightlines[$i];

		my $diffmark = '  ';
		if ($chg{$i}) {
			$diffmark = '<span class="diff-mark">' . $chg{$i} . "</span>";
			if ('&lt;&lt;' eq $chg{$i}) {
				$l =~ s|</a> |</a> <span class="diff-left">|;
			}
			if ('&gt;&gt;' eq $chg{$i}) {
				$r =~ s|</a> |</a> <span class="diff-right">|;
			}
			if ('!!' eq $chg{$i}) {
				$l =~ s|</a> |</a> <span class="diff-both">|;
				$r =~ s|</a> |</a> <span class="diff-both">|;
			}
			$l .= '</span>';
			$r .= '</span>';
		}

		print "$l $diffmark $r\n";
	}
	print("</pre>\n");

}


=head2 Script entry point

Builds the header and footer and launches C<printdiff>
for the real job.

=cut

httpinit();
std_http_headers('diff');

makeheader('diff');
my @dargs;
foreach my $param (keys %{$HTTP->{'param'}}) {
	my $var = $param;
	next unless $var =~ s/^~//;
	if (exists($config->{'variables'}{$var})) {
			push @dargs, "$var=" . $HTTP->{'param'}{$param};
	}
}
printdiff(@dargs);
makefooter('diff');

httpclean;

Want the latest updates on software, tech news, and AI?
Get latest updates about software, tech news, and AI from SourceForge directly in your inbox once a month.