git-cvsexportcommit.perlon commit show-branch -g: default to HEAD (df373ea)
   1#!/usr/bin/perl -w
   2
   3# Known limitations:
   4# - does not propagate permissions
   5# - error handling has not been extensively tested
   6#
   7
   8use strict;
   9use Getopt::Std;
  10use File::Temp qw(tempdir);
  11use Data::Dumper;
  12use File::Basename qw(basename dirname);
  13
  14unless ($ENV{GIT_DIR} && -r $ENV{GIT_DIR}){
  15    die "GIT_DIR is not defined or is unreadable";
  16}
  17
  18our ($opt_h, $opt_p, $opt_v, $opt_c, $opt_f, $opt_a, $opt_m );
  19
  20getopts('hpvcfam:');
  21
  22$opt_h && usage();
  23
  24die "Need at least one commit identifier!" unless @ARGV;
  25
  26# setup a tempdir
  27our ($tmpdir, $tmpdirname) = tempdir('git-cvsapplycommit-XXXXXX',
  28                                     TMPDIR => 1,
  29                                     CLEANUP => 1);
  30
  31# resolve target commit
  32my $commit;
  33$commit = pop @ARGV;
  34$commit = safe_pipe_capture('git-rev-parse', '--verify', "$commit^0");
  35chomp $commit;
  36if ($?) {
  37    die "The commit reference $commit did not resolve!";
  38}
  39
  40# resolve what parent we want
  41my $parent;
  42if (@ARGV) {
  43    $parent = pop @ARGV;
  44    $parent =  safe_pipe_capture('git-rev-parse', '--verify', "$parent^0");
  45    chomp $parent;
  46    if ($?) {
  47        die "The parent reference did not resolve!";
  48    }
  49}
  50
  51# find parents from the commit itself
  52my @commit  = safe_pipe_capture('git-cat-file', 'commit', $commit);
  53my @parents;
  54my $committer;
  55my $author;
  56my $stage = 'headers'; # headers, msg
  57my $title;
  58my $msg = '';
  59
  60foreach my $line (@commit) {
  61    chomp $line;
  62    if ($stage eq 'headers' && $line eq '') {
  63        $stage = 'msg';
  64        next;
  65    }
  66
  67    if ($stage eq 'headers') {
  68        if ($line =~ m/^parent (\w{40})$/) { # found a parent
  69            push @parents, $1;
  70        } elsif ($line =~ m/^author (.+) \d+ [-+]\d+$/) {
  71            $author = $1;
  72        } elsif ($line =~ m/^committer (.+) \d+ [-+]\d+$/) {
  73            $committer = $1;
  74        }
  75    } else {
  76        $msg .= $line . "\n";
  77        unless ($title) {
  78            $title = $line;
  79        }
  80    }
  81}
  82
  83if ($parent) {
  84    my $found;
  85    # double check that it's a valid parent
  86    foreach my $p (@parents) {
  87        if ($p eq $parent) {
  88            $found = 1;
  89            last;
  90        }; # found it
  91    }
  92    die "Did not find $parent in the parents for this commit!" if !$found;
  93} else { # we don't have a parent from the cmdline...
  94    if (@parents == 1) { # it's safe to get it from the commit
  95        $parent = $parents[0];
  96    } else { # or perhaps not!
  97        die "This commit has more than one parent -- please name the parent you want to use explicitly";
  98    }
  99}
 100
 101$opt_v && print "Applying to CVS commit $commit from parent $parent\n";
 102
 103# grab the commit message
 104open(MSG, ">.msg") or die "Cannot open .msg for writing";
 105if ($opt_m) {
 106    print MSG $opt_m;
 107}
 108print MSG $msg;
 109if ($opt_a) {
 110    print MSG "\n\nAuthor: $author\n";
 111    if ($author ne $committer) {
 112        print MSG "Committer: $committer\n";
 113    }
 114}
 115close MSG;
 116
 117`git-diff-tree --binary -p $parent $commit >.cvsexportcommit.diff`;# || die "Cannot diff";
 118
 119## apply non-binary changes
 120my $fuzz = $opt_p ? 0 : 2;
 121
 122print "Checking if patch will apply\n";
 123
 124my @stat;
 125open APPLY, "GIT_DIR= git-apply -C$fuzz --binary --summary --numstat<.cvsexportcommit.diff|" || die "cannot patch";
 126@stat=<APPLY>;
 127close APPLY || die "Cannot patch";
 128my (@bfiles,@files,@afiles,@dfiles);
 129chomp @stat;
 130foreach (@stat) {
 131        push (@bfiles,$1) if m/^-\t-\t(.*)$/;
 132        push (@files, $1) if m/^-\t-\t(.*)$/;
 133        push (@files, $1) if m/^\d+\t\d+\t(.*)$/;
 134        push (@afiles,$1) if m/^ create mode [0-7]+ (.*)$/;
 135        push (@dfiles,$1) if m/^ delete mode [0-7]+ (.*)$/;
 136}
 137map { s/^"(.*)"$/$1/g } @bfiles,@files;
 138map { s/\\([0-7]{3})/sprintf('%c',oct $1)/eg } @bfiles,@files;
 139
 140# check that the files are clean and up to date according to cvs
 141my $dirty;
 142my @dirs;
 143foreach my $p (@afiles) {
 144    my $path = dirname $p;
 145    while (!-d $path and ! grep { $_ eq $path } @dirs) {
 146        unshift @dirs, $path;
 147        $path = dirname $path;
 148    }
 149}
 150
 151foreach my $d (@dirs) {
 152    if (-e $d) {
 153        $dirty = 1;
 154        warn "$d exists and is not a directory!\n";
 155    }
 156}
 157foreach my $f (@afiles) {
 158    # This should return only one value
 159    if ($f =~ m,(.*)/[^/]*$,) {
 160        my $p = $1;
 161        next if (grep { $_ eq $p } @dirs);
 162    }
 163    my @status = grep(m/^File/,  safe_pipe_capture('cvs', '-q', 'status' ,$f));
 164    if (@status > 1) { warn 'Strange! cvs status returned more than one line?'};
 165    if (-d dirname $f and $status[0] !~ m/Status: Unknown$/
 166        and $status[0] !~ m/^File: no file /) {
 167        $dirty = 1;
 168        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";
 169        warn "Status was: $status[0]\n";
 170    }
 171}
 172
 173foreach my $f (@files) {
 174    next if grep { $_ eq $f } @afiles;
 175    # TODO:we need to handle removed in cvs
 176    my @status = grep(m/^File/,  safe_pipe_capture('cvs', '-q', 'status' ,$f));
 177    if (@status > 1) { warn 'Strange! cvs status returned more than one line?'};
 178    unless ($status[0] =~ m/Status: Up-to-date$/) {
 179        $dirty = 1;
 180        warn "File $f not up to date in your CVS checkout!\n";
 181    }
 182}
 183if ($dirty) {
 184    if ($opt_f) {       warn "The tree is not clean -- forced merge\n";
 185        $dirty = 0;
 186    } else {
 187        die "Exiting: your CVS tree is not clean for this merge.";
 188    }
 189}
 190
 191print "Applying\n";
 192`GIT_DIR= git-apply -C$fuzz --binary --summary --numstat --apply <.cvsexportcommit.diff` || die "cannot patch";
 193
 194print "Patch applied successfully. Adding new files and directories to CVS\n";
 195my $dirtypatch = 0;
 196foreach my $d (@dirs) {
 197    if (system('cvs','add',$d)) {
 198        $dirtypatch = 1;
 199        warn "Failed to cvs add directory $d -- you may need to do it manually";
 200    }
 201}
 202
 203foreach my $f (@afiles) {
 204    if (grep { $_ eq $f } @bfiles) {
 205      system('cvs', 'add','-kb',$f);
 206    } else {
 207      system('cvs', 'add', $f);
 208    }
 209    if ($?) {
 210        $dirtypatch = 1;
 211        warn "Failed to cvs add $f -- you may need to do it manually";
 212    }
 213}
 214
 215foreach my $f (@dfiles) {
 216    system('cvs', 'rm', '-f', $f);
 217    if ($?) {
 218        $dirtypatch = 1;
 219        warn "Failed to cvs rm -f $f -- you may need to do it manually";
 220    }
 221}
 222
 223print "Commit to CVS\n";
 224print "Patch title (first comment line): $title\n";
 225my @commitfiles = map { unless (m/\s/) { '\''.$_.'\''; } else { $_; }; } (@files);
 226my $cmd = "cvs commit -F .msg @commitfiles";
 227
 228if ($dirtypatch) {
 229    print "NOTE: One or more hunks failed to apply cleanly.\n";
 230    print "You'll need to apply the patch in .cvsexportcommit.diff manually\n";
 231    print "using a patch program. After applying the patch and resolving the\n";
 232    print "problems you may commit using:";
 233    print "\n    $cmd\n\n";
 234    exit(1);
 235}
 236
 237if ($opt_c) {
 238    print "Autocommit\n  $cmd\n";
 239    print safe_pipe_capture('cvs', 'commit', '-F', '.msg', @files);
 240    if ($?) {
 241        die "Exiting: The commit did not succeed";
 242    }
 243    print "Committed successfully to CVS\n";
 244} else {
 245    print "Ready for you to commit, just run:\n\n   $cmd\n";
 246}
 247
 248# clean up
 249unlink(".cvsexportcommit.diff");
 250unlink(".msg");
 251
 252sub usage {
 253        print STDERR <<END;
 254Usage: GIT_DIR=/path/to/.git ${\basename $0} [-h] [-p] [-v] [-c] [-f] [-m msgprefix] [ parent ] commit
 255END
 256        exit(1);
 257}
 258
 259# An alternative to `command` that allows input to be passed as an array
 260# to work around shell problems with weird characters in arguments
 261# if the exec returns non-zero we die
 262sub safe_pipe_capture {
 263    my @output;
 264    if (my $pid = open my $child, '-|') {
 265        @output = (<$child>);
 266        close $child or die join(' ',@_).": $! $?";
 267    } else {
 268        exec(@_) or die "$! $?"; # exec() can fail the executable can't be found
 269    }
 270    return wantarray ? @output : join('',@output);
 271}
 272
 273sub safe_pipe_capture_blob {
 274    my $output;
 275    if (my $pid = open my $child, '-|') {
 276        local $/;
 277        undef $/;
 278        $output = (<$child>);
 279        close $child or die join(' ',@_).": $! $?";
 280    } else {
 281        exec(@_) or die "$! $?"; # exec() can fail the executable can't be found
 282    }
 283    return $output;
 284}