newspaint

Documenting Problems That Were Difficult To Find The Answer To

Quickly Comparing Working Copies With CVS, SVN, and Git Repositories

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.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: