A frequent problem I had was wanting to run a graphical (UI) diff between my working copy of a file and the latest (or any particular) revision in the source repository.
Of course one can run the common commands cvs diff (and similar for other repository types) but this gives a textual diff which can be less helpful when a source file is long.
So I present here the source code for the Perl scripts I use before every check-in I make.
Note that I have used tkdiff as the graphical diff utility in these scripts. I find this diff tool very lightweight and it is free. I also have variants of these utilities that use Beyond Compare which is a heavier but very powerful diff utility and one which I thoroughly recommend purchasing by any serious developer.
CVS
#!/usr/bin/perl -w
# -*-CPerl-*-
use Getopt::Long;
use strict;
# configure your utilities here
# ... in particular your favourite graphical diff application
my $diff = "tkdiff";
my $cvs = "cvs";
my $cp = "cp";
my $patch = "patch";
my %tempfile = ();
my $result;
$tempfile{patch} = "temp.patch.$$.temp";
sub patch_copy( $ $ $ $ );
sub clean_tempfiles( $ );
# process options
my @revisions = ();
Getopt::Long::Configure("bundling");
GetOptions( "revision|rev|r=s" => \@revisions );
my $cmpfile = $ARGV[0];
if ( ! $cmpfile )
{
print( STDERR <<EOF );
cvs-tkdiff - compare a file against another version in a CVS repository
Usage:
cvs-tkdiff [-r <revision>] [-r <revision] <filename>
Description:
If no revision is specified then <filename> is compared against the
HEAD in the repository.
If a single revision is provided (e.g. "cvs-tkdiff -r1.1 a.txt")
then the file ("a.txt") in the local working directory will be compared
against the version in the specified commit.
If two revisions are specified (e.g. "cvs-tkdiff -r1.1 -r1.3 a.txt")
then the files from the two specified commits will be compared to each
other.
EOF
exit( 1 );
}
my @filediff = (); # will contain the two files to diff against
if ( @revisions == 2 ) {
# compare revisions mode
my ( $rev_a, $rev_b ) = @revisions;
$tempfile{rev_a} = "temp.file.$$.ver.$rev_a.temp";
$tempfile{rev_b} = "temp.file.$$.ver.$rev_b.temp";
eval {
patch_copy( $cmpfile, $tempfile{rev_a},
$rev_a, $tempfile{patch} );
};
if ( $@ ) {
my $error = $@;
clean_tempfiles( \%tempfile );
die( "Could not retrieve repository version: $error" );
}
eval {
patch_copy( $cmpfile, $tempfile{rev_b},
$rev_b, $tempfile{patch} );
};
if ( $@ ) {
my $error = $@;
clean_tempfiles( \%tempfile );
die( "Could not retrieve repository version: $error" );
}
@filediff = ( $tempfile{rev_a}, $tempfile{rev_b} );
} else {
# compare against CVS version mode
# does $cmpfile exist?
if ( ! -r $cmpfile ) {
print( STDERR "File \"$cmpfile\" is not readable" );
exit( 1 );
}
# get current repository version of file
my $rev_b = undef;
if ( @revisions == 1 ) {
$rev_b = $revisions[0];
} else {
my $cvsstatus = `$cvs status "$cmpfile"`;
my $repositoryversion = undef;
if ( $cvsstatus =~ m/Repository\srevision:\s+([a-zA-Z_0-9.-]+)/i ) {
$rev_b = $1;
} else {
die( "Could not obtain current repository version" );
}
}
$tempfile{rev_b} = "temp.file.$$.ver.$rev_b.temp";
eval {
patch_copy( $cmpfile, $tempfile{rev_b},
$rev_b, $tempfile{patch} );
};
if ( $@ ) {
my $error = $@;
clean_tempfiles( \%tempfile );
die( "Could not retrieve repository version: $error" );
}
@filediff = ( $tempfile{rev_b}, $cmpfile );
}
# run diff
system( $diff, @filediff );
# clean up
clean_tempfiles( \%tempfile );
exit( 0 );
###############################################################################
# subroutines
#
sub clean_tempfiles( $ ) {
my ( $tempfileref ) = @_;
foreach my $key ( keys %{$tempfileref} ) {
unlink( $tempfileref->{$key} ) if ( -f $tempfileref->{$key} );
}
}
sub patch_copy( $ $ $ $ ) {
my ( $fileorig, $filedest, $revision, $patchfile ) = @_;
# copy original file to destination
my $result = `$cp "$fileorig" "$filedest"`;
die ( "Could not copy original file" ) if ( ! -r $filedest );
# create patch file
$result = `$cvs diff -r $revision -u "$cmpfile" >"$patchfile"`;
if ( ! -r $patchfile ) {
die( "Could not create patch file revision $revision from cvs" );
}
$result = `$patch -R -p0 $filedest $patchfile`;
}
SVN
#!/usr/bin/perl -w
# -*-CPerl-*-
use File::Copy;
use Getopt::Long;
use strict;
# configure your utilities here
# ... in particular your favourite graphical diff application
my $diff = "tkdiff";
my $svn = "svn";
my $patch = "patch";
my %tempfile = ();
my $result;
$tempfile{patch} = "temp.patch.$$.temp";
sub normalise_eol( $ $ );
sub patch_copy( $ $ $ $ );
sub clean_tempfiles( $ );
# process options
my @revisions = ();
Getopt::Long::Configure("bundling");
GetOptions( "revision|rev|r=s" => \@revisions );
my $cmpfile = $ARGV[0];
if ( ! $cmpfile )
{
print( STDERR <<EOF );
svn-tkdiff - compare a file against another version in a SVN repository
Usage:
svn-tkdiff [-r <revision>] [-r <revision] <filename>
Description:
If no revision is specified then <filename> is compared against the
HEAD in the repository.
If a single revision is provided (e.g. "svn-tkdiff -r3266 a.txt")
then the file ("a.txt") in the local working directory will be compared
against the version in the specified commit.
If two revisions are specified (e.g. "svn-tkdiff -r3266 -r5230 a.txt")
then the files from the two specified commits will be compared to each
other.
EOF
exit( 1 );
}
my @filediff = (); # will contain the two files to diff against
if ( @revisions == 2 ) {
# compare revisions mode
my ( $rev_a, $rev_b ) = @revisions;
$tempfile{rev_a} = "temp.file.$$.ver.$rev_a.temp";
$tempfile{rev_b} = "temp.file.$$.ver.$rev_b.temp";
eval {
patch_copy( $cmpfile, $tempfile{rev_a},
$rev_a, $tempfile{patch} );
};
if ( $@ ) {
my $error = $@;
clean_tempfiles( \%tempfile );
die( "Could not retrieve repository version: $error" );
}
eval {
patch_copy( $cmpfile, $tempfile{rev_b},
$rev_b, $tempfile{patch} );
};
if ( $@ ) {
my $error = $@;
clean_tempfiles( \%tempfile );
die( "Could not retrieve repository version: $error" );
}
@filediff = ( $tempfile{rev_a}, $tempfile{rev_b} );
} else {
# compare against SVN version mode
# does $cmpfile exist?
if ( ! -r $cmpfile ) {
print( STDERR "File \"$cmpfile\" is not readable" );
exit( 1 );
}
# get current repository version of file
my $rev_b = "HEAD";
if ( @revisions == 1 ) {
$rev_b = $revisions[0];
} else {
my $svnstatus = `$svn status -v "$cmpfile"`;
if ( $svnstatus =~ m/^.{7}\s*(\d+)\s*(\d+)/ ) {
$rev_b = $2;
} else {
die( "Could not obtain current repository version" );
}
}
$tempfile{rev_b} = "temp.file.$$.ver.$rev_b.temp";
eval {
patch_copy( $cmpfile, $tempfile{rev_b},
$rev_b, $tempfile{patch} );
};
if ( $@ ) {
my $error = $@;
clean_tempfiles( \%tempfile );
die( "Could not retrieve repository version: $error" );
}
@filediff = ( $tempfile{rev_b}, $cmpfile );
}
# run diff
system( $diff, @filediff );
# clean up
clean_tempfiles( \%tempfile );
exit( 0 );
###############################################################################
# subroutines
#
sub clean_tempfiles( $ ) {
my ( $tempfileref ) = @_;
foreach my $key ( keys %{$tempfileref} ) {
unlink( $tempfileref->{$key} ) if ( -f $tempfileref->{$key} );
}
}
sub patch_copy( $ $ $ $ ) {
my ( $fileorig, $filedest, $revision, $patchfile ) = @_;
# copy original file to destination
my $result = File::Copy::copy( "$fileorig", "$filedest" );
die ( "Could not copy original file" ) if ( ! -r $filedest );
# create patch file
$result = `$svn diff -r $revision -x -u "$cmpfile" >"$patchfile"`;
if ( ! -r $patchfile ) {
die( "Could not create patch file revision $revision from svn" );
}
# make line endings common
normalise_eol( $filedest, "\n" );
normalise_eol( $patchfile, "\n" );
$result = `$patch -R -p0 $filedest -i $patchfile`;
}
sub normalise_eol( $ $ ) {
my ( $filename, $eol ) = @_;
my $fin = undef;
if ( ! open( $fin, "<$filename" ) ) {
die( "Failed to open file \"$filename\" for reading: $!" );
}
my $fout = undef;
if ( ! open( $fout, ">$filename.$$.tmp" ) ) {
die( "Failed to open file \"$filename.$$.tmp\" for writing: $!" );
}
my $line;
while ( defined( $line = <$fin> ) ) {
$line =~ s/(\r\n|\n\r|\n|\r)/$eol/sg;
print( $fout $line );
}
close( $fout );
close( $fin );
rename( "$filename.$$.tmp", $filename );
}
Git
#!/usr/bin/perl -w
# -*-CPerl-*-
use Getopt::Long;
use strict;
# configure your utilities here
# ... in particular your favourite graphical diff application
my $diff = "tkdiff";
my $git = "GIT_PAGER=cat git";
my $cp = "cp";
my $patch = "patch";
my %tempfile = ();
my $result;
$tempfile{patch} = "temp.patch.$$.temp";
sub patch_copy( $ $ $ $ );
sub clean_tempfiles( $ );
# process options
my @revisions = ();
Getopt::Long::Configure("bundling");
GetOptions( "revision|rev|r=s" => \@revisions );
my $cmpfile = $ARGV[0];
if ( ! $cmpfile )
{
print( STDERR <<EOF );
git-tkdiff - compare a file against another version in a git repository
Usage:
git-tkdiff [-r <revision>] [-r <revision] <filename>
Description:
If no revision is specified then <filename> is compared against the
HEAD in the local repository.
If a single revision is provided (e.g. "git-tkdiff -r0f1bef a.txt")
then the file ("a.txt") in the local working directory will be compared
against the version in the specified commit.
If two revisions are specified (e.g. "git-tkdiff -r0f1b -rba88 a.txt")
then the files from the two specified commits will be compared to each
other.
If you want to compare your local copy against the most current version
in the remote repository then specify "origin/master" as the revision, e.g.
git-tkdiff -rorigin/master a.txt
EOF
exit( 1 );
}
my @filediff = (); # will contain the two files to diff against
sub safe_fname {
my $orig = $_[0];
$orig =~ s/[^a-zA-Z0-9_-]/_/g;
return( $orig );
}
if ( @revisions == 2 ) {
# compare revisions mode
my ( $rev_a, $rev_b ) = @revisions;
my ( $srev_a, $srev_b ) = map { safe_fname($_) } @revisions;
$tempfile{rev_a} = "temp.file.$$.ver.$srev_a.temp";
$tempfile{rev_b} = "temp.file.$$.ver.$srev_b.temp";
eval {
patch_copy( $cmpfile, $tempfile{rev_a},
$rev_a, $tempfile{patch} );
};
if ( $@ ) {
my $error = $@;
clean_tempfiles( \%tempfile );
die( "Could not retrieve repository version: $error" );
}
eval {
patch_copy( $cmpfile, $tempfile{rev_b},
$rev_b, $tempfile{patch} );
};
if ( $@ ) {
my $error = $@;
clean_tempfiles( \%tempfile );
die( "Could not retrieve repository version: $error" );
}
@filediff = ( $tempfile{rev_a}, $tempfile{rev_b} );
} else {
# compare against git HEAD version mode
# does $cmpfile exist?
if ( ! -r $cmpfile ) {
print( STDERR "File \"$cmpfile\" is not readable" );
exit( 1 );
}
# get current repository version of file
my $rev_b = undef;
if ( @revisions == 1 ) {
$rev_b = $revisions[0];
} else {
my $gitlatestlog = `$git log --full-index --summary -1 "$cmpfile"`;
my $repositoryversion = undef;
if ( $gitlatestlog =~ m/^commit\s+([a-fA-F0-9]{40})/m ) {
$rev_b = $1;
} else {
die( "Could not obtain current repository version" );
}
}
my $srev_b = safe_fname( $rev_b );
$tempfile{rev_b} = "temp.file.$$.ver.$srev_b.temp";
eval {
patch_copy( $cmpfile, $tempfile{rev_b},
$rev_b, $tempfile{patch} );
};
if ( $@ ) {
my $error = $@;
clean_tempfiles( \%tempfile );
die( "Could not retrieve repository version: $error" );
}
@filediff = ( $tempfile{rev_b}, $cmpfile );
}
# run diff
system( $diff, @filediff );
# clean up
clean_tempfiles( \%tempfile );
exit( 0 );
###############################################################################
# subroutines
#
sub clean_tempfiles( $ ) {
my ( $tempfileref ) = @_;
foreach my $key ( keys %{$tempfileref} ) {
unlink( $tempfileref->{$key} ) if ( -f $tempfileref->{$key} );
}
}
sub patch_copy( $ $ $ $ ) {
my ( $fileorig, $filedest, $revision, $patchfile ) = @_;
# copy original file to destination
my $result = `$cp "$fileorig" "$filedest"`;
die ( "Could not copy original file" ) if ( ! -r $filedest );
# create patch file
$result = `$git diff -r $revision -u -- "$cmpfile" >"$patchfile"`;
if ( ! -r $patchfile ) {
die( "Could not create patch file revision $revision from cvs" );
}
$result = `$patch -R -p0 $filedest $patchfile`;
}
History
I wrote cvs-tkdiff.pl in 2006 and have subsequently written the variants – git-tkdiff.pl was written in 2009.
Running on Windows
The Windows operating system, at least more modern variants, really take a strong dislike to the GnuWin32 patch utility. Sometimes a work-around is to merely call the executable something that isn’t “patch”. But then sometimes modern versions of git produce diffs that are incompatible with GnuWin32 patch. In the end I decided to download and use the Perl port of patch from PerlPowerTools-1.012.
Thus in my scripts I have configured:
my $patch = "\"C:\\apps\\perl\\bin\\perl.exe\" \"C:\\apps\\PerlPowerTools-1.012\\bin\\patch\"";
Recent Comments