1#!/usr/bin/perl -w 2 3# Known limitations: 4# - does not propagate permissions 5# - tells "ready for commit" even when things could not be completed 6# (not sure this is true anymore, more testing is needed) 7# - does not handle whitespace in pathnames at all. 8 9use strict; 10use Getopt::Std; 11use File::Temp qw(tempdir); 12use Data::Dumper; 13use File::Basename qw(basename dirname); 14 15unless($ENV{GIT_DIR} && -r $ENV{GIT_DIR}){ 16die"GIT_DIR is not defined or is unreadable"; 17} 18 19our($opt_h,$opt_p,$opt_v,$opt_c,$opt_f,$opt_a,$opt_m); 20 21getopts('hpvcfam:'); 22 23$opt_h&& usage(); 24 25die"Need at least one commit identifier!"unless@ARGV; 26 27# setup a tempdir 28our($tmpdir,$tmpdirname) = tempdir('git-cvsapplycommit-XXXXXX', 29 TMPDIR =>1, 30 CLEANUP =>1); 31 32# resolve target commit 33my$commit; 34$commit=pop@ARGV; 35$commit= safe_pipe_capture('git-rev-parse','--verify',"$commit^0"); 36chomp$commit; 37if($?) { 38die"The commit reference$commitdid not resolve!"; 39} 40 41# resolve what parent we want 42my$parent; 43if(@ARGV) { 44$parent=pop@ARGV; 45$parent= safe_pipe_capture('git-rev-parse','--verify',"$parent^0"); 46chomp$parent; 47if($?) { 48die"The parent reference did not resolve!"; 49} 50} 51 52# find parents from the commit itself 53my@commit= safe_pipe_capture('git-cat-file','commit',$commit); 54my@parents; 55my$committer; 56my$author; 57my$stage='headers';# headers, msg 58my$title; 59my$msg=''; 60 61foreachmy$line(@commit) { 62chomp$line; 63if($stageeq'headers'&&$lineeq'') { 64$stage='msg'; 65next; 66} 67 68if($stageeq'headers') { 69if($line=~m/^parent (\w{40})$/) {# found a parent 70push@parents,$1; 71}elsif($line=~m/^author (.+) \d+ [-+]\d+$/) { 72$author=$1; 73}elsif($line=~m/^committer (.+) \d+ [-+]\d+$/) { 74$committer=$1; 75} 76}else{ 77$msg.=$line."\n"; 78unless($title) { 79$title=$line; 80} 81} 82} 83 84if($parent) { 85my$found; 86# double check that it's a valid parent 87foreachmy$p(@parents) { 88if($peq$parent) { 89$found=1; 90last; 91};# found it 92} 93die"Did not find$parentin the parents for this commit!"if!$found; 94}else{# we don't have a parent from the cmdline... 95if(@parents==1) {# it's safe to get it from the commit 96$parent=$parents[0]; 97}else{# or perhaps not! 98die"This commit has more than one parent -- please name the parent you want to use explicitly"; 99} 100} 101 102$opt_v&&print"Applying to CVS commit$commitfrom parent$parent\n"; 103 104# grab the commit message 105open(MSG,">.msg")or die"Cannot open .msg for writing"; 106if($opt_m) { 107print MSG $opt_m; 108} 109print MSG $msg; 110if($opt_a) { 111print MSG "\n\nAuthor:$author\n"; 112if($authorne$committer) { 113print MSG "Committer:$committer\n"; 114} 115} 116close MSG; 117 118my(@afiles,@dfiles,@mfiles,@dirs); 119my%amodes; 120my@files= safe_pipe_capture('git-diff-tree','-r',$parent,$commit); 121#print @files; 122$?&&die"Error in git-diff-tree"; 123foreachmy$f(@files) { 124chomp$f; 125my@fields=split(m!\s+!,$f); 126if($fields[4]eq'A') { 127my$path=$fields[5]; 128$amodes{$path} =$fields[1]; 129push@afiles,$path; 130# add any needed parent directories 131$path= dirname $path; 132while(!-d $pathand!grep{$_eq$path}@dirs) { 133unshift@dirs,$path; 134$path= dirname $path; 135} 136} 137if($fields[4]eq'M') { 138push@mfiles,$fields[5]; 139} 140if($fields[4]eq'D') { 141push@dfiles,$fields[5]; 142} 143} 144my(@binfiles,@abfiles,@dbfiles,@bfiles,@mbfiles); 145@binfiles=grepm/^Binary files/, safe_pipe_capture('git-diff-tree','-p',$parent,$commit); 146map{chomp}@binfiles; 147@abfiles=greps/^Binary files \/dev\/null and b\/(.*) differ$/$1/,@binfiles; 148@dbfiles=greps/^Binary files a\/(.*) and \/dev\/null differ$/$1/,@binfiles; 149@mbfiles=greps/^Binary files a\/(.*) and b\/(.*) differ$/$1/,@binfiles; 150push@bfiles,@abfiles; 151push@bfiles,@dbfiles; 152push@bfiles,@mbfiles; 153push@mfiles,@mbfiles; 154 155$opt_v&&print"The commit affects:\n"; 156$opt_v&&print join("\n",@afiles,@mfiles,@dfiles) ."\n\n"; 157undef@files;# don't need it anymore 158 159# check that the files are clean and up to date according to cvs 160my$dirty; 161foreachmy$d(@dirs) { 162if(-e $d) { 163$dirty=1; 164warn"$dexists and is not a directory!\n"; 165} 166} 167foreachmy$f(@afiles) { 168# This should return only one value 169if($f=~ m,(.*)/[^/]*$,) { 170my$p=$1; 171next if(grep{$_eq$p}@dirs); 172} 173my@status=grep(m/^File/, safe_pipe_capture('cvs','-q','status',$f)); 174if(@status>1) {warn'Strange! cvs status returned more than one line?'}; 175if(-d dirname $fand$status[0] !~m/Status: Unknown$/ 176and$status[0] !~m/^File: no file /) { 177$dirty=1; 178warn"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"; 179warn"Status was:$status[0]\n"; 180} 181} 182 183foreachmy$f(@mfiles,@dfiles) { 184# TODO:we need to handle removed in cvs 185my@status=grep(m/^File/, safe_pipe_capture('cvs','-q','status',$f)); 186if(@status>1) {warn'Strange! cvs status returned more than one line?'}; 187unless($status[0] =~m/Status: Up-to-date$/) { 188$dirty=1; 189warn"File$fnot up to date in your CVS checkout!\n"; 190} 191} 192if($dirty) { 193if($opt_f) {warn"The tree is not clean -- forced merge\n"; 194$dirty=0; 195}else{ 196die"Exiting: your CVS tree is not clean for this merge."; 197} 198} 199 200### 201### NOTE: if you are planning to die() past this point 202### you MUST call cleanupcvs(@files) before die() 203### 204 205 206print"Creating new directories\n"; 207foreachmy$d(@dirs) { 208unless(mkdir$d) { 209warn"Could not mkdir$d:$!"; 210$dirty=1; 211} 212`cvs add$d`; 213 if ($?) { 214$dirty= 1; 215 warn "Failed to cvs add directory$d-- you may need to do it manually"; 216 } 217} 218 219print "'Patching' binary files\n"; 220 221foreach my$f(@bfiles) { 222 # check that the file in cvs matches the "old" file 223 # extract the file to$tmpdirand compare with cmp 224 if (not(grep {$_eq$f}@afiles)) { 225 my$tree= safe_pipe_capture('git-rev-parse', "$parent^{tree}"); 226 chomp$tree; 227 my$blob= `git-ls-tree $tree"$f"| cut -f 1| cut -d ' '-f 3`; 228 chomp$blob; 229 `git-cat-file blob $blob>$tmpdir/blob`; 230if(system('cmp','-s',$f,"$tmpdir/blob")) { 231warn"Binary file$fin CVS does not match parent.\n"; 232if(not$opt_f) { 233$dirty=1; 234next; 235} 236} 237} 238if(not(grep{$_eq$f}@dfiles)) { 239my$tree= safe_pipe_capture('git-rev-parse',"$commit^{tree}"); 240chomp$tree; 241my$blob=`git-ls-tree$tree"$f" | cut -f 1 | cut -d ' ' -f 3`; 242chomp$blob; 243# replace with the new file 244`git-cat-file blob$blob>$f`; 245 } 246 247 # TODO: something smart with file modes 248 249} 250if ($dirty) { 251 cleanupcvs(@files); 252 die "Exiting: Binary files in CVS do not match parent"; 253} 254 255## apply non-binary changes 256my$fuzz=$opt_p? 0 : 2; 257 258print "Patching non-binary files\n"; 259 260if (scalar(@afiles)+scalar(@dfiles)+scalar(@mfiles) != scalar(@bfiles)) { 261 print `(git-diff-tree -p $parent-p $commit| patch -p1 -F $fuzz)2>&1`; 262} 263 264my$dirtypatch= 0; 265if (($?>> 8) == 2) { 266 cleanupcvs(@files); 267 die "Exiting: Patch reported serious trouble -- you will have to apply this patch manually"; 268} elsif (($?>> 8) == 1) { # some hunks failed to apply 269$dirtypatch= 1; 270} 271 272foreach my$f(@afiles) { 273 set_new_file_permissions($f,$amodes{$f}); 274 if (grep {$_eq$f}@bfiles) { 275 system('cvs', 'add','-kb',$f); 276 } else { 277 system('cvs', 'add',$f); 278 } 279 if ($?) { 280$dirty= 1; 281 warn "Failed to cvs add$f-- you may need to do it manually"; 282 } 283} 284 285foreach my$f(@dfiles) { 286 system('cvs', 'rm', '-f',$f); 287 if ($?) { 288$dirty= 1; 289 warn "Failed to cvs rm -f$f-- you may need to do it manually"; 290 } 291} 292 293print "Commit to CVS\n"; 294print "Patch:$title\n"; 295my$commitfiles= join(' ',@afiles,@mfiles,@dfiles); 296my$cmd= "cvs commit -F .msg$commitfiles"; 297 298if ($dirtypatch) { 299 print "NOTE: One or more hunks failed to apply cleanly.\n"; 300 print "Resolve the conflicts and then commit using:\n"; 301 print "\n$cmd\n\n"; 302 exit(1); 303} 304 305 306if ($opt_c) { 307 print "Autocommit\n$cmd\n"; 308 print safe_pipe_capture('cvs', 'commit', '-F', '.msg',@afiles,@mfiles,@dfiles); 309 if ($?) { 310 cleanupcvs(@files); 311 die "Exiting: The commit did not succeed"; 312 } 313 print "Committed successfully to CVS\n"; 314} else { 315 print "Ready for you to commit, just run:\n\n$cmd\n"; 316} 317sub usage { 318 print STDERR <<END; 319Usage: GIT_DIR=/path/to/.git ${\basename$0} [-h] [-p] [-v] [-c] [-f] [-m msgprefix] [ parent ] commit 320END 321exit(1); 322} 323 324# ensure cvs is clean before we die 325sub cleanupcvs { 326my@files=@_; 327foreachmy$f(@files) { 328system('cvs','-q','update','-C',$f); 329if($?) { 330warn"Warning! Failed to cleanup state of$f\n"; 331} 332} 333} 334 335# An alternative to `command` that allows input to be passed as an array 336# to work around shell problems with weird characters in arguments 337# if the exec returns non-zero we die 338sub safe_pipe_capture { 339my@output; 340if(my$pid=open my$child,'-|') { 341@output= (<$child>); 342close$childor die join(' ',@_).":$!$?"; 343}else{ 344exec(@_)or die"$!$?";# exec() can fail the executable can't be found 345} 346returnwantarray?@output:join('',@output); 347} 348 349# For any file we want to add to cvs, we must first set its permissions 350# properly, *before* the "cvs add ..." command. Otherwise, it is impossible 351# to change the permission of the file in the CVS repository using only cvs 352# commands. This should be fixed in cvs-1.12.14. 353sub set_new_file_permissions { 354my($file,$perm) =@_; 355chmod oct($perm),$file 356or die"failed to set permissions of\"$file\":$!\n"; 357}