3e4e90b89b718bb9417a1d42c4933e0ab9c0a3b7
   1#!/usr/bin/perl -w
   2
   3# Known limitations:
   4# - cannot add or remove binary files
   5# - cannot add parent directories when needed
   6# - does not propagate permissions
   7# - tells "ready for commit" even when things could not be completed
   8#   (eg addition of a binary file)
   9
  10use strict;
  11use Getopt::Std;
  12use File::Temp qw(tempdir);
  13use Data::Dumper;
  14use File::Basename qw(basename);
  15
  16unless ($ENV{GIT_DIR} && -r $ENV{GIT_DIR}){
  17    die "GIT_DIR is not defined or is unreadable";
  18}
  19
  20our ($opt_h, $opt_p, $opt_v, $opt_c, $opt_f, $opt_m );
  21
  22getopts('hpvcfm:');
  23
  24$opt_h && usage();
  25
  26die "Need at least one commit identifier!" unless @ARGV;
  27
  28# setup a tempdir
  29our ($tmpdir, $tmpdirname) = tempdir('git-cvsapplycommit-XXXXXX',
  30                                     TMPDIR => 1,
  31                                     CLEANUP => 1);
  32
  33print Dumper(@ARGV);
  34# resolve target commit
  35my $commit;
  36$commit = pop @ARGV;
  37$commit = safe_pipe_capture('git-rev-parse', '--verify', "$commit^0");
  38chomp $commit;
  39if ($?) {
  40    die "The commit reference $commit did not resolve!";
  41}
  42
  43# resolve what parent we want
  44my $parent;
  45if (@ARGV) {
  46    $parent = pop @ARGV;
  47    $parent =  safe_pipe_capture('git-rev-parse', '--verify', "$parent^0");
  48    chomp $parent;
  49    if ($?) {
  50        die "The parent reference did not resolve!";
  51    }
  52}
  53
  54# find parents from the commit itself
  55my @commit  = safe_pipe_capture('git-cat-file', 'commit', $commit);
  56my @parents;
  57foreach my $p (@commit) {
  58    if ($p =~ m/^$/) { # end of commit headers, we're done
  59        last;
  60    }
  61    if ($p =~ m/^parent (\w{40})$/) { # found a parent
  62        push @parents, $1;
  63    }
  64}
  65
  66if ($parent) {
  67    # double check that it's a valid parent
  68    foreach my $p (@parents) {
  69        my $found;
  70        if ($p eq $parent) {
  71            $found = 1;
  72            last;
  73        }; # found it
  74        die "Did not find $parent in the parents for this commit!";
  75    }
  76} else { # we don't have a parent from the cmdline...
  77    if (@parents == 1) { # it's safe to get it from the commit
  78        $parent = $parents[0];
  79    } else { # or perhaps not!
  80        die "This commit has more than one parent -- please name the parent you want to use explicitly";
  81    }
  82}
  83
  84$opt_v && print "Applying to CVS commit $commit from parent $parent\n";
  85
  86# grab the commit message
  87open(MSG, ">.msg") or die "Cannot open .msg for writing";
  88print MSG $opt_m;
  89close MSG;
  90
  91`git-cat-file commit $commit | sed -e '1,/^\$/d' >> .msg`;
  92$? && die "Error extracting the commit message";
  93
  94my (@afiles, @dfiles, @mfiles);
  95my @files = safe_pipe_capture('git-diff-tree', '-r', $parent, $commit);
  96#print @files;
  97$? && die "Error in git-diff-tree";
  98foreach my $f (@files) {
  99    chomp $f;
 100    my @fields = split(m!\s+!, $f);
 101    if ($fields[4] eq 'A') {
 102        push @afiles, $fields[5];
 103    }
 104    if ($fields[4] eq 'M') {
 105        push @mfiles, $fields[5];
 106    }
 107    if ($fields[4] eq 'R') {
 108        push @dfiles, $fields[5];
 109    }
 110}
 111$opt_v && print "The commit affects:\n ";
 112$opt_v && print join ("\n ", @afiles,@mfiles,@dfiles) . "\n\n";
 113undef @files; # don't need it anymore
 114
 115# check that the files are clean and up to date according to cvs
 116my $dirty;
 117foreach my $f (@afiles) {
 118    # This should return only one value
 119    my @status = grep(m/^File/,  safe_pipe_capture('cvs', '-q', 'status' ,$f));
 120    if (@status > 1) { warn 'Strange! cvs status returned more than one line?'};
 121    unless ($status[0] =~ m/Status: Unknown$/) {
 122        $dirty = 1;
 123        warn "File $f is already known in your CVS checkout -- perhaps it has been added by another user. Or this may indicate that it exists on a different branch. If this is the case, use -f to force the merge.\n";
 124    }
 125}
 126foreach my $f (@mfiles, @dfiles) {
 127    # TODO:we need to handle removed in cvs
 128    my @status = grep(m/^File/,  safe_pipe_capture('cvs', '-q', 'status' ,$f));
 129    if (@status > 1) { warn 'Strange! cvs status returned more than one line?'};
 130    unless ($status[0] =~ m/Status: Up-to-date$/) {
 131        $dirty = 1;
 132        warn "File $f not up to date in your CVS checkout!\n";
 133    }
 134}
 135if ($dirty) {
 136    if ($opt_f) {       warn "The tree is not clean -- forced merge\n";
 137        $dirty = 0;
 138    } else {
 139        die "Exiting: your CVS tree is not clean for this merge.";
 140    }
 141}
 142
 143###
 144### NOTE: if you are planning to die() past this point
 145###       you MUST call cleanupcvs(@files) before die()
 146###
 147
 148
 149print "'Patching' binary files\n";
 150
 151my @bfiles = grep(m/^Binary/, safe_pipe_capture('git-diff-tree', '-p', $parent, $commit));
 152@bfiles = map { chomp } @bfiles;
 153foreach my $f (@bfiles) {
 154    # check that the file in cvs matches the "old" file
 155    # extract the file to $tmpdir and comparre with cmp
 156    my $tree = safe_pipe_capture('git-rev-parse', "$parent^{tree}");
 157    chomp $tree;
 158    my $blob = `git-ls-tree $tree "$f" | cut -f 1 | cut -d ' ' -f 3`;
 159    chomp $blob;
 160    `git-cat-file blob $blob > $tmpdir/blob`;
 161    if (system('cmp', '-s', $f, "$tmpdir/blob")) {
 162        warn "Binary file $f in CVS does not match parent.\n";
 163        $dirty = 1;
 164        next;
 165    }
 166
 167    # replace with the new file
 168     `git-cat-file blob $blob > $f`;
 169
 170    # TODO: something smart with file modes
 171
 172}
 173if ($dirty) {
 174    cleanupcvs(@files);
 175    die "Exiting: Binary files in CVS do not match parent";
 176}
 177
 178## apply non-binary changes
 179my $fuzz = $opt_p ? 0 : 2;
 180
 181print "Patching non-binary files\n";
 182print `(git-diff-tree -p $parent -p $commit | patch -p1 -F $fuzz ) 2>&1`;
 183
 184my $dirtypatch = 0;
 185if (($? >> 8) == 2) {
 186    cleanupcvs(@files);
 187    die "Exiting: Patch reported serious trouble -- you will have to apply this patch manually";
 188} elsif (($? >> 8) == 1) { # some hunks failed to apply
 189    $dirtypatch = 1;
 190}
 191
 192foreach my $f (@afiles) {
 193    system('cvs', 'add', $f);
 194    if ($?) {
 195        $dirty = 1;
 196        warn "Failed to cvs add $f -- you may need to do it manually";
 197    }
 198}
 199
 200foreach my $f (@dfiles) {
 201    system('cvs', 'rm', '-f', $f);
 202    if ($?) {
 203        $dirty = 1;
 204        warn "Failed to cvs rm -f $f -- you may need to do it manually";
 205    }
 206}
 207
 208print "Commit to CVS\n";
 209my $commitfiles = join(' ', @afiles, @mfiles, @dfiles);
 210my $cmd = "cvs commit -F .msg $commitfiles";
 211
 212if ($dirtypatch) {
 213    print "NOTE: One or more hunks failed to apply cleanly.\n";
 214    print "Resolve the conflicts and then commit using:\n";
 215    print "\n    $cmd\n\n";
 216    exit(1);
 217}
 218
 219
 220if ($opt_c) {
 221    print "Autocommit\n  $cmd\n";
 222    print safe_pipe_capture('cvs', 'commit', '-F', '.msg', @afiles, @mfiles, @dfiles);
 223    if ($?) {
 224        cleanupcvs(@files);
 225        die "Exiting: The commit did not succeed";
 226    }
 227    print "Committed successfully to CVS\n";
 228} else {
 229    print "Ready for you to commit, just run:\n\n   $cmd\n";
 230}
 231sub usage {
 232        print STDERR <<END;
 233Usage: GIT_DIR=/path/to/.git ${\basename $0} [-h] [-p] [-v] [-c] [-f] [-m msgprefix] [ parent ] commit
 234END
 235        exit(1);
 236}
 237
 238# ensure cvs is clean before we die
 239sub cleanupcvs {
 240    my @files = @_;
 241    foreach my $f (@files) {
 242        system('cvs', '-q', 'update', '-C', $f);
 243        if ($?) {
 244            warn "Warning! Failed to cleanup state of $f\n";
 245        }
 246    }
 247}
 248
 249# An alterative to `command` that allows input to be passed as an array
 250# to work around shell problems with weird characters in arguments
 251# if the exec returns non-zero we die
 252sub safe_pipe_capture {
 253    my @output;
 254    if (my $pid = open my $child, '-|') {
 255        @output = (<$child>);
 256        close $child or die join(' ',@_).": $! $?";
 257    } else {
 258        exec(@_) or die "$! $?"; # exec() can fail the executable can't be found
 259    }
 260    return wantarray ? @output : join('',@output);
 261}