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}){ 17die"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($?) { 40die"The commit reference$commitdid 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"); 48chomp$parent; 49if($?) { 50die"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; 57foreachmy$p(@commit) { 58if($p=~m/^$/) {# end of commit headers, we're done 59last; 60} 61if($p=~m/^parent (\w{40})$/) {# found a parent 62push@parents,$1; 63} 64} 65 66if($parent) { 67# double check that it's a valid parent 68foreachmy$p(@parents) { 69my$found; 70if($peq$parent) { 71$found=1; 72last; 73};# found it 74die"Did not find$parentin the parents for this commit!"; 75} 76}else{# we don't have a parent from the cmdline... 77if(@parents==1) {# it's safe to get it from the commit 78$parent=$parents[0]; 79}else{# or perhaps not! 80die"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$commitfrom 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"; 98foreachmy$f(@files) { 99chomp$f; 100my@fields=split(m!\s+!,$f); 101if($fields[4]eq'A') { 102push@afiles,$fields[5]; 103} 104if($fields[4]eq'M') { 105push@mfiles,$fields[5]; 106} 107if($fields[4]eq'R') { 108push@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; 117foreachmy$f(@afiles) { 118# This should return only one value 119my@status=grep(m/^File/, safe_pipe_capture('cvs','-q','status',$f)); 120if(@status>1) {warn'Strange! cvs status returned more than one line?'}; 121unless($status[0] =~m/Status: Unknown$/) { 122$dirty=1; 123warn"File$fis 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} 126foreachmy$f(@mfiles,@dfiles) { 127# TODO:we need to handle removed in cvs 128my@status=grep(m/^File/, safe_pipe_capture('cvs','-q','status',$f)); 129if(@status>1) {warn'Strange! cvs status returned more than one line?'}; 130unless($status[0] =~m/Status: Up-to-date$/) { 131$dirty=1; 132warn"File$fnot up to date in your CVS checkout!\n"; 133} 134} 135if($dirty) { 136if($opt_f) {warn"The tree is not clean -- forced merge\n"; 137$dirty=0; 138}else{ 139die"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; 153foreachmy$f(@bfiles) { 154# check that the file in cvs matches the "old" file 155# extract the file to $tmpdir and comparre with cmp 156my$tree= safe_pipe_capture('git-rev-parse',"$parent^{tree}"); 157chomp$tree; 158my$blob=`git-ls-tree$tree"$f" | cut -f 1 | cut -d ' ' -f 3`; 159chomp$blob; 160`git-cat-file blob$blob>$tmpdir/blob`; 161 if (system('cmp', '-s',$f, "$tmpdir/blob")) { 162 warn "Binary file$fin 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); 175die"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); 187die"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 192foreachmy$f(@afiles) { 193system('cvs','add',$f); 194if($?) { 195$dirty=1; 196warn"Failed to cvs add$f-- you may need to do it manually"; 197} 198} 199 200foreachmy$f(@dfiles) { 201system('cvs','rm','-f',$f); 202if($?) { 203$dirty=1; 204warn"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) { 213print"NOTE: One or more hunks failed to apply cleanly.\n"; 214print"Resolve the conflicts and then commit using:\n"; 215print"\n$cmd\n\n"; 216exit(1); 217} 218 219 220if($opt_c) { 221print"Autocommit\n$cmd\n"; 222print safe_pipe_capture('cvs','commit','-F','.msg',@afiles,@mfiles,@dfiles); 223if($?) { 224 cleanupcvs(@files); 225die"Exiting: The commit did not succeed"; 226} 227print"Committed successfully to CVS\n"; 228}else{ 229print"Ready for you to commit, just run:\n\n$cmd\n"; 230} 231sub usage { 232print STDERR <<END; 233Usage: GIT_DIR=/path/to/.git ${\basename$0} [-h] [-p] [-v] [-c] [-f] [-m msgprefix] [ parent ] commit 234END 235exit(1); 236} 237 238# ensure cvs is clean before we die 239sub cleanupcvs { 240my@files=@_; 241foreachmy$f(@files) { 242system('cvs','-q','update','-C',$f); 243if($?) { 244warn"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 { 253my@output; 254if(my$pid=open my$child,'-|') { 255@output= (<$child>); 256close$childor die join(' ',@_).":$!$?"; 257}else{ 258exec(@_)or die"$!$?";# exec() can fail the executable can't be found 259} 260returnwantarray?@output:join('',@output); 261}