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