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_m); 20 21getopts('hpvcfm:'); 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 32print Dumper(@ARGV); 33# resolve target commit 34my$commit; 35$commit=pop@ARGV; 36$commit= safe_pipe_capture('git-rev-parse','--verify',"$commit^0"); 37chomp$commit; 38if($?) { 39die"The commit reference$commitdid not resolve!"; 40} 41 42# resolve what parent we want 43my$parent; 44if(@ARGV) { 45$parent=pop@ARGV; 46$parent= safe_pipe_capture('git-rev-parse','--verify',"$parent^0"); 47chomp$parent; 48if($?) { 49die"The parent reference did not resolve!"; 50} 51} 52 53# find parents from the commit itself 54my@commit= safe_pipe_capture('git-cat-file','commit',$commit); 55my@parents; 56foreachmy$p(@commit) { 57if($p=~m/^$/) {# end of commit headers, we're done 58last; 59} 60if($p=~m/^parent (\w{40})$/) {# found a parent 61push@parents,$1; 62} 63} 64 65if($parent) { 66# double check that it's a valid parent 67foreachmy$p(@parents) { 68my$found; 69if($peq$parent) { 70$found=1; 71last; 72};# found it 73die"Did not find$parentin the parents for this commit!"; 74} 75}else{# we don't have a parent from the cmdline... 76if(@parents==1) {# it's safe to get it from the commit 77$parent=$parents[0]; 78}else{# or perhaps not! 79die"This commit has more than one parent -- please name the parent you want to use explicitly"; 80} 81} 82 83$opt_v&&print"Applying to CVS commit$commitfrom parent$parent\n"; 84 85# grab the commit message 86open(MSG,">.msg")or die"Cannot open .msg for writing"; 87print MSG $opt_m; 88close MSG; 89 90`git-cat-file commit$commit| sed -e '1,/^\$/d' >> .msg`; 91$?&&die"Error extracting the commit message"; 92 93my(@afiles,@dfiles,@mfiles,@dirs); 94my@files= safe_pipe_capture('git-diff-tree','-r',$parent,$commit); 95#print @files; 96$?&&die"Error in git-diff-tree"; 97foreachmy$f(@files) { 98chomp$f; 99my@fields=split(m!\s+!,$f); 100if($fields[4]eq'A') { 101my$path=$fields[5]; 102push@afiles,$path; 103# add any needed parent directories 104$path= dirname $path; 105while(!-d $pathand!grep{$_eq$path}@dirs) { 106unshift@dirs,$path; 107$path= dirname $path; 108} 109} 110if($fields[4]eq'M') { 111push@mfiles,$fields[5]; 112} 113if($fields[4]eq'R') { 114push@dfiles,$fields[5]; 115} 116} 117$opt_v&&print"The commit affects:\n"; 118$opt_v&&print join("\n",@afiles,@mfiles,@dfiles) ."\n\n"; 119undef@files;# don't need it anymore 120 121# check that the files are clean and up to date according to cvs 122my$dirty; 123foreachmy$d(@dirs) { 124if(-e $d) { 125$dirty=1; 126warn"$dexists and is not a directory!\n"; 127} 128} 129foreachmy$f(@afiles) { 130# This should return only one value 131my@status=grep(m/^File/, safe_pipe_capture('cvs','-q','status',$f)); 132if(@status>1) {warn'Strange! cvs status returned more than one line?'}; 133if(-d dirname $fand$status[0] !~m/Status: Unknown$/ 134and$status[0] !~m/^File: no file /) { 135$dirty=1; 136warn"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"; 137warn"Status was:$status[0]\n"; 138} 139} 140foreachmy$f(@mfiles,@dfiles) { 141# TODO:we need to handle removed in cvs 142my@status=grep(m/^File/, safe_pipe_capture('cvs','-q','status',$f)); 143if(@status>1) {warn'Strange! cvs status returned more than one line?'}; 144unless($status[0] =~m/Status: Up-to-date$/) { 145$dirty=1; 146warn"File$fnot up to date in your CVS checkout!\n"; 147} 148} 149if($dirty) { 150if($opt_f) {warn"The tree is not clean -- forced merge\n"; 151$dirty=0; 152}else{ 153die"Exiting: your CVS tree is not clean for this merge."; 154} 155} 156 157### 158### NOTE: if you are planning to die() past this point 159### you MUST call cleanupcvs(@files) before die() 160### 161 162 163print"Creating new directories\n"; 164foreachmy$d(@dirs) { 165unless(mkdir$d) { 166warn"Could not mkdir$d:$!"; 167$dirty=1; 168} 169`cvs add$d`; 170 if ($?) { 171$dirty= 1; 172 warn "Failed to cvs add directory$d-- you may need to do it manually"; 173 } 174} 175 176print "'Patching' binary files\n"; 177 178my@bfiles= grep(m/^Binary/, safe_pipe_capture('git-diff-tree', '-p',$parent,$commit)); 179@bfiles= map { chomp }@bfiles; 180foreach my$f(@bfiles) { 181 # check that the file in cvs matches the "old" file 182 # extract the file to$tmpdirand comparre with cmp 183 my$tree= safe_pipe_capture('git-rev-parse', "$parent^{tree}"); 184 chomp$tree; 185 my$blob= `git-ls-tree $tree"$f"| cut -f 1| cut -d ' '-f 3`; 186 chomp$blob; 187 `git-cat-file blob $blob>$tmpdir/blob`; 188if(system('cmp','-s',$f,"$tmpdir/blob")) { 189warn"Binary file$fin CVS does not match parent.\n"; 190$dirty=1; 191next; 192} 193 194# replace with the new file 195`git-cat-file blob$blob>$f`; 196 197 # TODO: something smart with file modes 198 199} 200if ($dirty) { 201 cleanupcvs(@files); 202 die "Exiting: Binary files in CVS do not match parent"; 203} 204 205## apply non-binary changes 206my$fuzz=$opt_p? 0 : 2; 207 208print "Patching non-binary files\n"; 209print `(git-diff-tree -p $parent-p $commit| patch -p1 -F $fuzz)2>&1`; 210 211my$dirtypatch= 0; 212if (($?>> 8) == 2) { 213 cleanupcvs(@files); 214 die "Exiting: Patch reported serious trouble -- you will have to apply this patch manually"; 215} elsif (($?>> 8) == 1) { # some hunks failed to apply 216$dirtypatch= 1; 217} 218 219foreach my$f(@afiles) { 220 system('cvs', 'add',$f); 221 if ($?) { 222$dirty= 1; 223 warn "Failed to cvs add$f-- you may need to do it manually"; 224 } 225} 226 227foreach my$f(@dfiles) { 228 system('cvs', 'rm', '-f',$f); 229 if ($?) { 230$dirty= 1; 231 warn "Failed to cvs rm -f$f-- you may need to do it manually"; 232 } 233} 234 235print "Commit to CVS\n"; 236my$commitfiles= join(' ',@afiles,@mfiles,@dfiles); 237my$cmd= "cvs commit -F .msg$commitfiles"; 238 239if ($dirtypatch) { 240 print "NOTE: One or more hunks failed to apply cleanly.\n"; 241 print "Resolve the conflicts and then commit using:\n"; 242 print "\n$cmd\n\n"; 243 exit(1); 244} 245 246 247if ($opt_c) { 248 print "Autocommit\n$cmd\n"; 249 print safe_pipe_capture('cvs', 'commit', '-F', '.msg',@afiles,@mfiles,@dfiles); 250 if ($?) { 251 cleanupcvs(@files); 252 die "Exiting: The commit did not succeed"; 253 } 254 print "Committed successfully to CVS\n"; 255} else { 256 print "Ready for you to commit, just run:\n\n$cmd\n"; 257} 258sub usage { 259 print STDERR <<END; 260Usage: GIT_DIR=/path/to/.git ${\basename$0} [-h] [-p] [-v] [-c] [-f] [-m msgprefix] [ parent ] commit 261END 262exit(1); 263} 264 265# ensure cvs is clean before we die 266sub cleanupcvs { 267my@files=@_; 268foreachmy$f(@files) { 269system('cvs','-q','update','-C',$f); 270if($?) { 271warn"Warning! Failed to cleanup state of$f\n"; 272} 273} 274} 275 276# An alterative to `command` that allows input to be passed as an array 277# to work around shell problems with weird characters in arguments 278# if the exec returns non-zero we die 279sub safe_pipe_capture { 280my@output; 281if(my$pid=open my$child,'-|') { 282@output= (<$child>); 283close$childor die join(' ',@_).":$!$?"; 284}else{ 285exec(@_)or die"$!$?";# exec() can fail the executable can't be found 286} 287returnwantarray?@output:join('',@output); 288}