1#!/usr/bin/perl -w 2 3use strict; 4use Getopt::Std; 5use File::Temp qw(tempdir); 6use Data::Dumper; 7use File::Basename qw(basename dirname); 8 9our($opt_h,$opt_P,$opt_p,$opt_v,$opt_c,$opt_f,$opt_a,$opt_m,$opt_d,$opt_u,$opt_w); 10 11getopts('uhPpvcfam:d:w:'); 12 13$opt_h&& usage(); 14 15die"Need at least one commit identifier!"unless@ARGV; 16 17if($opt_w) { 18unless($ENV{GIT_DIR}) { 19# Remember where our GIT_DIR is before changing to CVS checkout 20my$gd=`git-rev-parse --git-dir`; 21chomp($gd); 22if($gdeq'.git') { 23my$wd=`pwd`; 24chomp($wd); 25$gd=$wd."/.git"; 26} 27$ENV{GIT_DIR} =$gd; 28} 29 30if(! -d $opt_w."/CVS") { 31die"$opt_wis not a CVS checkout"; 32} 33chdir$opt_wor die"Cannot change to CVS checkout at$opt_w"; 34} 35unless($ENV{GIT_DIR} && -r $ENV{GIT_DIR}){ 36die"GIT_DIR is not defined or is unreadable"; 37} 38 39 40my@cvs; 41if($opt_d) { 42@cvs= ('cvs','-d',$opt_d); 43}else{ 44@cvs= ('cvs'); 45} 46 47# resolve target commit 48my$commit; 49$commit=pop@ARGV; 50$commit= safe_pipe_capture('git-rev-parse','--verify',"$commit^0"); 51chomp$commit; 52if($?) { 53die"The commit reference$commitdid not resolve!"; 54} 55 56# resolve what parent we want 57my$parent; 58if(@ARGV) { 59$parent=pop@ARGV; 60$parent= safe_pipe_capture('git-rev-parse','--verify',"$parent^0"); 61chomp$parent; 62if($?) { 63die"The parent reference did not resolve!"; 64} 65} 66 67# find parents from the commit itself 68my@commit= safe_pipe_capture('git-cat-file','commit',$commit); 69my@parents; 70my$committer; 71my$author; 72my$stage='headers';# headers, msg 73my$title; 74my$msg=''; 75 76foreachmy$line(@commit) { 77chomp$line; 78if($stageeq'headers'&&$lineeq'') { 79$stage='msg'; 80next; 81} 82 83if($stageeq'headers') { 84if($line=~m/^parent (\w{40})$/) {# found a parent 85push@parents,$1; 86}elsif($line=~m/^author (.+) \d+ [-+]\d+$/) { 87$author=$1; 88}elsif($line=~m/^committer (.+) \d+ [-+]\d+$/) { 89$committer=$1; 90} 91}else{ 92$msg.=$line."\n"; 93unless($title) { 94$title=$line; 95} 96} 97} 98 99my$noparent="0000000000000000000000000000000000000000"; 100if($parent) { 101my$found; 102# double check that it's a valid parent 103foreachmy$p(@parents) { 104if($peq$parent) { 105$found=1; 106last; 107};# found it 108} 109die"Did not find$parentin the parents for this commit!"if!$foundand!$opt_P; 110}else{# we don't have a parent from the cmdline... 111if(@parents==1) {# it's safe to get it from the commit 112$parent=$parents[0]; 113}elsif(@parents==0) {# there is no parent 114$parent=$noparent; 115}else{# cannot choose automatically from multiple parents 116die"This commit has more than one parent -- please name the parent you want to use explicitly"; 117} 118} 119 120$opt_v&&print"Applying to CVS commit$commitfrom parent$parent\n"; 121 122# grab the commit message 123open(MSG,">.msg")or die"Cannot open .msg for writing"; 124if($opt_m) { 125print MSG $opt_m; 126} 127print MSG $msg; 128if($opt_a) { 129print MSG "\n\nAuthor:$author\n"; 130if($authorne$committer) { 131print MSG "Committer:$committer\n"; 132} 133} 134close MSG; 135 136if($parenteq$noparent) { 137`git-diff-tree --binary -p --root$commit>.cvsexportcommit.diff`;# || die "Cannot diff"; 138}else{ 139`git-diff-tree --binary -p$parent$commit>.cvsexportcommit.diff`;# || die "Cannot diff"; 140} 141 142## apply non-binary changes 143 144# In pedantic mode require all lines of context to match. In normal 145# mode, be compatible with diff/patch: assume 3 lines of context and 146# require at least one line match, i.e. ignore at most 2 lines of 147# context, like diff/patch do by default. 148my$context=$opt_p?'':'-C1'; 149 150print"Checking if patch will apply\n"; 151 152my@stat; 153open APPLY,"GIT_DIR= git-apply$context--summary --numstat<.cvsexportcommit.diff|"||die"cannot patch"; 154@stat=<APPLY>; 155close APPLY ||die"Cannot patch"; 156my(@bfiles,@files,@afiles,@dfiles); 157chomp@stat; 158foreach(@stat) { 159push(@bfiles,$1)ifm/^-\t-\t(.*)$/; 160push(@files,$1)ifm/^-\t-\t(.*)$/; 161push(@files,$1)ifm/^\d+\t\d+\t(.*)$/; 162push(@afiles,$1)ifm/^ create mode [0-7]+ (.*)$/; 163push(@dfiles,$1)ifm/^ delete mode [0-7]+ (.*)$/; 164} 165map{s/^"(.*)"$/$1/g}@bfiles,@files; 166map{s/\\([0-7]{3})/sprintf('%c',oct $1)/eg}@bfiles,@files; 167 168# check that the files are clean and up to date according to cvs 169my$dirty; 170my@dirs; 171foreachmy$p(@afiles) { 172my$path= dirname $p; 173while(!-d $pathand!grep{$_eq$path}@dirs) { 174unshift@dirs,$path; 175$path= dirname $path; 176} 177} 178 179# ... check dirs, 180foreachmy$d(@dirs) { 181if(-e $d) { 182$dirty=1; 183warn"$dexists and is not a directory!\n"; 184} 185} 186 187# ... query status of all files that we have a directory for and parse output of 'cvs status' to %cvsstat. 188my@canstatusfiles; 189foreachmy$f(@files) { 190my$path= dirname $f; 191next if(grep{$_eq$path}@dirs); 192push@canstatusfiles,$f; 193} 194 195my%cvsstat; 196if(@canstatusfiles) { 197if($opt_u) { 198my@updated= xargs_safe_pipe_capture([@cvs,'update'],@canstatusfiles); 199print@updated; 200} 201my@cvsoutput; 202@cvsoutput= xargs_safe_pipe_capture([@cvs,'status'],@canstatusfiles); 203my$matchcount=0; 204foreachmy$l(@cvsoutput) { 205chomp$l; 206if($l=~/^File:/and$l=~/Status: (.*)$/) { 207$cvsstat{$canstatusfiles[$matchcount]} =$1; 208$matchcount++; 209} 210} 211} 212 213# ... validate new files, 214foreachmy$f(@afiles) { 215if(defined($cvsstat{$f})and$cvsstat{$f}ne"Unknown") { 216$dirty=1; 217warn"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"; 218warn"Status was:$cvsstat{$f}\n"; 219} 220} 221# ... validate known files. 222foreachmy$f(@files) { 223next ifgrep{$_eq$f}@afiles; 224# TODO:we need to handle removed in cvs 225unless(defined($cvsstat{$f})and$cvsstat{$f}eq"Up-to-date") { 226$dirty=1; 227warn"File$fnot up to date but has status '$cvsstat{$f}' in your CVS checkout!\n"; 228} 229} 230if($dirty) { 231if($opt_f) {warn"The tree is not clean -- forced merge\n"; 232$dirty=0; 233}else{ 234die"Exiting: your CVS tree is not clean for this merge."; 235} 236} 237 238print"Applying\n"; 239`GIT_DIR= git-apply$context--summary --numstat --apply <.cvsexportcommit.diff`||die"cannot patch"; 240 241print"Patch applied successfully. Adding new files and directories to CVS\n"; 242my$dirtypatch=0; 243 244# 245# We have to add the directories in order otherwise we will have 246# problems when we try and add the sub-directory of a directory we 247# have not added yet. 248# 249# Luckily this is easy to deal with by sorting the directories and 250# dealing with the shortest ones first. 251# 252@dirs=sort{length$a<=>length$b}@dirs; 253 254foreachmy$d(@dirs) { 255if(system(@cvs,'add',$d)) { 256$dirtypatch=1; 257warn"Failed to cvs add directory$d-- you may need to do it manually"; 258} 259} 260 261foreachmy$f(@afiles) { 262if(grep{$_eq$f}@bfiles) { 263system(@cvs,'add','-kb',$f); 264}else{ 265system(@cvs,'add',$f); 266} 267if($?) { 268$dirtypatch=1; 269warn"Failed to cvs add$f-- you may need to do it manually"; 270} 271} 272 273foreachmy$f(@dfiles) { 274system(@cvs,'rm','-f',$f); 275if($?) { 276$dirtypatch=1; 277warn"Failed to cvs rm -f$f-- you may need to do it manually"; 278} 279} 280 281print"Commit to CVS\n"; 282print"Patch title (first comment line):$title\n"; 283my@commitfiles=map{unless(m/\s/) {'\''.$_.'\''; }else{$_; }; } (@files); 284my$cmd=join(' ',@cvs)." commit -F .msg@commitfiles"; 285 286if($dirtypatch) { 287print"NOTE: One or more hunks failed to apply cleanly.\n"; 288print"You'll need to apply the patch in .cvsexportcommit.diff manually\n"; 289print"using a patch program. After applying the patch and resolving the\n"; 290print"problems you may commit using:"; 291print"\ncd\"$opt_w\""if$opt_w; 292print"\n$cmd\n\n"; 293exit(1); 294} 295 296if($opt_c) { 297print"Autocommit\n$cmd\n"; 298print xargs_safe_pipe_capture([@cvs,'commit','-F','.msg'],@files); 299if($?) { 300die"Exiting: The commit did not succeed"; 301} 302print"Committed successfully to CVS\n"; 303# clean up 304unlink(".msg"); 305}else{ 306print"Ready for you to commit, just run:\n\n$cmd\n"; 307} 308 309# clean up 310unlink(".cvsexportcommit.diff"); 311 312# CVS version 1.11.x and 1.12.x sleeps the wrong way to ensure the timestamp 313# used by CVS and the one set by subsequence file modifications are different. 314# If they are not different CVS will not detect changes. 315sleep(1); 316 317sub usage { 318print STDERR <<END; 319Usage: GIT_DIR=/path/to/.git ${\basename$0} [-h] [-p] [-v] [-c] [-f] [-u] [-w cvsworkdir] [-m msgprefix] [ parent ] commit 320END 321exit(1); 322} 323 324# An alternative to `command` that allows input to be passed as an array 325# to work around shell problems with weird characters in arguments 326# if the exec returns non-zero we die 327sub safe_pipe_capture { 328my@output; 329if(my$pid=open my$child,'-|') { 330@output= (<$child>); 331close$childor die join(' ',@_).":$!$?"; 332}else{ 333exec(@_)or die"$!$?";# exec() can fail the executable can't be found 334} 335returnwantarray?@output:join('',@output); 336} 337 338sub xargs_safe_pipe_capture { 339my$MAX_ARG_LENGTH=65536; 340my$cmd=shift; 341my@output; 342my$output; 343while(@_) { 344my@args; 345my$length=0; 346while(@_&&$length<$MAX_ARG_LENGTH) { 347push@args,shift; 348$length+=length($args[$#args]); 349} 350if(wantarray) { 351push@output, safe_pipe_capture(@$cmd,@args); 352} 353else{ 354$output.= safe_pipe_capture(@$cmd,@args); 355} 356} 357returnwantarray?@output:$output; 358}