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