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@files= safe_pipe_capture('git-diff-tree','-r',$parent,$commit); 120#print @files; 121$?&&die"Error in git-diff-tree"; 122foreachmy$f(@files) { 123chomp$f; 124my@fields=split(m!\s+!,$f); 125if($fields[4]eq'A') { 126my$path=$fields[5]; 127push@afiles,$path; 128# add any needed parent directories 129$path= dirname $path; 130while(!-d $pathand!grep{$_eq$path}@dirs) { 131unshift@dirs,$path; 132$path= dirname $path; 133} 134} 135if($fields[4]eq'M') { 136push@mfiles,$fields[5]; 137} 138if($fields[4]eq'D') { 139push@dfiles,$fields[5]; 140} 141} 142my(@binfiles,@abfiles,@dbfiles,@bfiles,@mbfiles); 143@binfiles=grepm/^Binary files/, safe_pipe_capture('git-diff-tree','-p',$parent,$commit); 144map{chomp}@binfiles; 145@abfiles=greps/^Binary files \/dev\/null and b\/(.*) differ$/$1/,@binfiles; 146@dbfiles=greps/^Binary files a\/(.*) and \/dev\/null differ$/$1/,@binfiles; 147@mbfiles=greps/^Binary files a\/(.*) and b\/(.*) differ$/$1/,@binfiles; 148push@bfiles,@abfiles; 149push@bfiles,@dbfiles; 150push@bfiles,@mbfiles; 151push@mfiles,@mbfiles; 152 153$opt_v&&print"The commit affects:\n"; 154$opt_v&&print join("\n",@afiles,@mfiles,@dfiles) ."\n\n"; 155undef@files;# don't need it anymore 156 157# check that the files are clean and up to date according to cvs 158my$dirty; 159foreachmy$d(@dirs) { 160if(-e $d) { 161$dirty=1; 162warn"$dexists and is not a directory!\n"; 163} 164} 165foreachmy$f(@afiles) { 166# This should return only one value 167if($f=~ m,(.*)/[^/]*$,) { 168my$p=$1; 169next if(grep{$_eq$p}@dirs); 170} 171my@status=grep(m/^File/, safe_pipe_capture('cvs','-q','status',$f)); 172if(@status>1) {warn'Strange! cvs status returned more than one line?'}; 173if(-d dirname $fand$status[0] !~m/Status: Unknown$/ 174and$status[0] !~m/^File: no file /) { 175$dirty=1; 176warn"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"; 177warn"Status was:$status[0]\n"; 178} 179} 180 181foreachmy$f(@mfiles,@dfiles) { 182# TODO:we need to handle removed in cvs 183my@status=grep(m/^File/, safe_pipe_capture('cvs','-q','status',$f)); 184if(@status>1) {warn'Strange! cvs status returned more than one line?'}; 185unless($status[0] =~m/Status: Up-to-date$/) { 186$dirty=1; 187warn"File$fnot up to date in your CVS checkout!\n"; 188} 189} 190if($dirty) { 191if($opt_f) {warn"The tree is not clean -- forced merge\n"; 192$dirty=0; 193}else{ 194die"Exiting: your CVS tree is not clean for this merge."; 195} 196} 197 198### 199### NOTE: if you are planning to die() past this point 200### you MUST call cleanupcvs(@files) before die() 201### 202 203 204print"Creating new directories\n"; 205foreachmy$d(@dirs) { 206unless(mkdir$d) { 207warn"Could not mkdir$d:$!"; 208$dirty=1; 209} 210`cvs add$d`; 211 if ($?) { 212$dirty= 1; 213 warn "Failed to cvs add directory$d-- you may need to do it manually"; 214 } 215} 216 217print "'Patching' binary files\n"; 218 219foreach my$f(@bfiles) { 220 # check that the file in cvs matches the "old" file 221 # extract the file to$tmpdirand compare with cmp 222 if (not(grep {$_eq$f}@afiles)) { 223 my$tree= safe_pipe_capture('git-rev-parse', "$parent^{tree}"); 224 chomp$tree; 225 my$blob= `git-ls-tree $tree"$f"| cut -f 1| cut -d ' '-f 3`; 226 chomp$blob; 227 `git-cat-file blob $blob>$tmpdir/blob`; 228if(system('cmp','-s',$f,"$tmpdir/blob")) { 229warn"Binary file$fin CVS does not match parent.\n"; 230if(not$opt_f) { 231$dirty=1; 232next; 233} 234} 235} 236if(not(grep{$_eq$f}@dfiles)) { 237my$tree= safe_pipe_capture('git-rev-parse',"$commit^{tree}"); 238chomp$tree; 239my$blob=`git-ls-tree$tree"$f" | cut -f 1 | cut -d ' ' -f 3`; 240chomp$blob; 241# replace with the new file 242`git-cat-file blob$blob>$f`; 243 } 244 245 # TODO: something smart with file modes 246 247} 248if ($dirty) { 249 cleanupcvs(@files); 250 die "Exiting: Binary files in CVS do not match parent"; 251} 252 253## apply non-binary changes 254my$fuzz=$opt_p? 0 : 2; 255 256print "Patching non-binary files\n"; 257 258if (scalar(@afiles)+scalar(@dfiles)+scalar(@mfiles) != scalar(@bfiles)) { 259 print `(git-diff-tree -p $parent-p $commit| patch -p1 -F $fuzz)2>&1`; 260} 261 262my$dirtypatch= 0; 263if (($?>> 8) == 2) { 264 cleanupcvs(@files); 265 die "Exiting: Patch reported serious trouble -- you will have to apply this patch manually"; 266} elsif (($?>> 8) == 1) { # some hunks failed to apply 267$dirtypatch= 1; 268} 269 270foreach my$f(@afiles) { 271 if (grep {$_eq$f}@bfiles) { 272 system('cvs', 'add','-kb',$f); 273 } else { 274 system('cvs', 'add',$f); 275 } 276 if ($?) { 277$dirty= 1; 278 warn "Failed to cvs add$f-- you may need to do it manually"; 279 } 280} 281 282foreach my$f(@dfiles) { 283 system('cvs', 'rm', '-f',$f); 284 if ($?) { 285$dirty= 1; 286 warn "Failed to cvs rm -f$f-- you may need to do it manually"; 287 } 288} 289 290print "Commit to CVS\n"; 291print "Patch:$title\n"; 292my$commitfiles= join(' ',@afiles,@mfiles,@dfiles); 293my$cmd= "cvs commit -F .msg$commitfiles"; 294 295if ($dirtypatch) { 296 print "NOTE: One or more hunks failed to apply cleanly.\n"; 297 print "Resolve the conflicts and then commit using:\n"; 298 print "\n$cmd\n\n"; 299 exit(1); 300} 301 302 303if ($opt_c) { 304 print "Autocommit\n$cmd\n"; 305 print safe_pipe_capture('cvs', 'commit', '-F', '.msg',@afiles,@mfiles,@dfiles); 306 if ($?) { 307 cleanupcvs(@files); 308 die "Exiting: The commit did not succeed"; 309 } 310 print "Committed successfully to CVS\n"; 311} else { 312 print "Ready for you to commit, just run:\n\n$cmd\n"; 313} 314sub usage { 315 print STDERR <<END; 316Usage: GIT_DIR=/path/to/.git ${\basename$0} [-h] [-p] [-v] [-c] [-f] [-m msgprefix] [ parent ] commit 317END 318exit(1); 319} 320 321# ensure cvs is clean before we die 322sub cleanupcvs { 323my@files=@_; 324foreachmy$f(@files) { 325system('cvs','-q','update','-C',$f); 326if($?) { 327warn"Warning! Failed to cleanup state of$f\n"; 328} 329} 330} 331 332# An alternative to `command` that allows input to be passed as an array 333# to work around shell problems with weird characters in arguments 334# if the exec returns non-zero we die 335sub safe_pipe_capture { 336my@output; 337if(my$pid=open my$child,'-|') { 338@output= (<$child>); 339close$childor die join(' ',@_).":$!$?"; 340}else{ 341exec(@_)or die"$!$?";# exec() can fail the executable can't be found 342} 343returnwantarray?@output:join('',@output); 344}