newspaint

Documenting Problems That Were Difficult To Find The Answer To

Category Archives: Version Control

Summarising Local Changes Against A CVS Repository

Say you have a large project checked out from CVS with many subdirectories. And you want to know what files you have changed that need checking in.

You could use the cvs status command but the output can get unwieldy on the screen. The following script presents a coloured text summary.

#!/usr/bin/perl -w # -*-CPerl-*-

use constant DEBUG => 0;

use strict;

my $cvs = "cvs";

# using ANSI?
my $ansicolor = 1;
eval "use Term::ANSIColor;";
if ( $@ ) {
    $ansicolor = undef;
}

# can we detect column width?
my $columns = undef;
if ( ! $columns ) {
    eval {
        my ( $fin, $line );
        if ( open( $fin, "resize |" ) ) {
            while ( defined( $line = <$fin> ) ) {
                if ( $line =~ m/COLUMNS=(\d+)/ ) {
                    $columns = $1;
                    last;
                }
            }
        }
        close( $fin );
    };
    if ( DEBUG() && $@ ) {
        my $err = $@; warn( $err );
    }
}

if ( ! $columns ) {
    eval {
        my ( $fin, $line );
        if ( open( $fin, "stty -a |" ) ) {
	    while ( defined( $line = <$fin> ) ) {
	        if ( $line =~ m/columns\s+(\d+)/ ) {
	            $columns = $1;
		    last;
	        }
            }
	}
	close( $fin );
    };
    if ( DEBUG() && $@ ) {
        my $err = $@; warn( $err );
    }
}
#$columns-- if ( $columns && ( $columns > 1 ) );
if ( DEBUG() ) {
    print( "Using columns of \"$columns\"\n" ) if ( $columns );
    print( "Could not determine column width\n" ) if ( ! $columns );
}

# autoflush on
select( (select(STDERR), $|=1)[0] );
select( (select(STDOUT), $|=1)[0] );

# start the cvs status command
my $arg = join( " ", map { "\"$_\"" } @ARGV );

if ( ! open( FIN, "$cvs status $arg 2>&1 |" ) ) {
    die( "Could not start cvs: $!" );
}

# cycle through each line of text returned by cvs status
my $lastdir = undef;
my $lastlength = 0;
while ( defined( my $line = <FIN> ) ) {
    if ( $line =~ m/^(cvs \S+: )(Examining )(.*)/s ) {
        my $dispdir;

        my $dir = $3;
        if ( ( $columns ) && ( $columns <= length( $1 . $2 . $3 ) ) ) {
            $dir = substr( $3, 0, ( $columns - length( $1 . $2 ) ) - 1 );
        }

        if ( $ansicolor ) {
            eval '$lastdir = $1 .color("yellow") .$2 .$3 .color("reset");';
            eval '$dispdir = $1 .color("yellow") .$2 .$dir .color("reset");';
        } else {
            $lastdir = $1 . $2 . $3;
            $dispdir = $1 . $2 . $dir;
        }

        my $length12dir = length( $1 . $2 . $dir );

        $lastdir =~ s/[\r\n]+//gs;
        $dispdir =~ s/[\r\n]+//gs;

        print( ( ' ' x $lastlength ) . "\r" );
        $lastlength = $length12dir;
        print( $dispdir . "\r" );
    }

    if ( $line =~ m/^(File: )(.*?)(\s+)(Status: )(.*)/s ) {
        my $fileline = undef;
        if ( $ansicolor ) {
            eval '$fileline = $1 . color("cyan") . $2 . color("reset") . $3 . $4 . color("magenta") . $5 . color("reset")';
        } else {
            $fileline = $1 . $2. $3. $4. $5;
        }

        if ( $line !~ m/Status: Up-to-date/ ) {
            if ( $lastdir ) {
                print( ( ' ' x length($lastdir) ) . "\r" );
                print( $lastdir . "\n" );
                $lastdir = undef;
            }

            print( $fileline );
        }
    }
}

# clean up screen
if ( $lastdir ) {
    print( ' ' x length($lastdir) . "\r" );
}

# close cvs
close( FIN );

It first attempts to discover the terminal width so that directory names don’t spill off the edge when displaying what folder is currently being analysed.

Example output would be:

host@server:/tmp/cvs# cvschange.pl
cvs status: Examining mytest
File: verifymsg         Status: Locally Modified
cvs status: Examining nagiosconf
File: nsca.pl           Status: Locally Modified

This script was written in 2006.

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.

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\"";