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