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