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_v,$opt_c,$opt_f,$opt_a,$opt_m); 19 20getopts('hpvcfam:'); 21 22$opt_h&& usage(); 23 24die"Need at least one commit identifier!"unless@ARGV; 25 26# setup a tempdir 27our($tmpdir,$tmpdirname) = tempdir('git-cvsapplycommit-XXXXXX', 28 TMPDIR =>1, 29 CLEANUP =>1); 30 31# resolve target commit 32my$commit; 33$commit=pop@ARGV; 34$commit= safe_pipe_capture('git-rev-parse','--verify',"$commit^0"); 35chomp$commit; 36if($?) { 37die"The commit reference$commitdid not resolve!"; 38} 39 40# resolve what parent we want 41my$parent; 42if(@ARGV) { 43$parent=pop@ARGV; 44$parent= safe_pipe_capture('git-rev-parse','--verify',"$parent^0"); 45chomp$parent; 46if($?) { 47die"The parent reference did not resolve!"; 48} 49} 50 51# find parents from the commit itself 52my@commit= safe_pipe_capture('git-cat-file','commit',$commit); 53my@parents; 54my$committer; 55my$author; 56my$stage='headers';# headers, msg 57my$title; 58my$msg=''; 59 60foreachmy$line(@commit) { 61chomp$line; 62if($stageeq'headers'&&$lineeq'') { 63$stage='msg'; 64next; 65} 66 67if($stageeq'headers') { 68if($line=~m/^parent (\w{40})$/) {# found a parent 69push@parents,$1; 70}elsif($line=~m/^author (.+) \d+ [-+]\d+$/) { 71$author=$1; 72}elsif($line=~m/^committer (.+) \d+ [-+]\d+$/) { 73$committer=$1; 74} 75}else{ 76$msg.=$line."\n"; 77unless($title) { 78$title=$line; 79} 80} 81} 82 83if($parent) { 84my$found; 85# double check that it's a valid parent 86foreachmy$p(@parents) { 87if($peq$parent) { 88$found=1; 89last; 90};# found it 91} 92die"Did not find$parentin the parents for this commit!"if!$found; 93}else{# we don't have a parent from the cmdline... 94if(@parents==1) {# it's safe to get it from the commit 95$parent=$parents[0]; 96}else{# or perhaps not! 97die"This commit has more than one parent -- please name the parent you want to use explicitly"; 98} 99} 100 101$opt_v&&print"Applying to CVS commit$commitfrom parent$parent\n"; 102 103# grab the commit message 104open(MSG,">.msg")or die"Cannot open .msg for writing"; 105if($opt_m) { 106print MSG $opt_m; 107} 108print MSG $msg; 109if($opt_a) { 110print MSG "\n\nAuthor:$author\n"; 111if($authorne$committer) { 112print MSG "Committer:$committer\n"; 113} 114} 115close MSG; 116 117`git-diff-tree --binary -p$parent$commit>.cvsexportcommit.diff`;# || die "Cannot diff"; 118 119## apply non-binary changes 120my$fuzz=$opt_p?0:2; 121 122print"Checking if patch will apply\n"; 123 124my@stat; 125open APPLY,"GIT_DIR= git-apply -C$fuzz--binary --summary --numstat<.cvsexportcommit.diff|"||die"cannot patch"; 126@stat=<APPLY>; 127close APPLY ||die"Cannot patch"; 128my(@bfiles,@files,@afiles,@dfiles); 129chomp@stat; 130foreach(@stat) { 131push(@bfiles,$1)ifm/^-\t-\t(.*)$/; 132push(@files,$1)ifm/^-\t-\t(.*)$/; 133push(@files,$1)ifm/^\d+\t\d+\t(.*)$/; 134push(@afiles,$1)ifm/^ create mode [0-7]+ (.*)$/; 135push(@dfiles,$1)ifm/^ delete mode [0-7]+ (.*)$/; 136} 137map{s/^"(.*)"$/$1/g}@bfiles,@files; 138map{s/\\([0-7]{3})/sprintf('%c',oct $1)/eg}@bfiles,@files; 139 140# check that the files are clean and up to date according to cvs 141my$dirty; 142my@dirs; 143foreachmy$p(@afiles) { 144my$path= dirname $p; 145while(!-d $pathand!grep{$_eq$path}@dirs) { 146unshift@dirs,$path; 147$path= dirname $path; 148} 149} 150 151foreachmy$d(@dirs) { 152if(-e $d) { 153$dirty=1; 154warn"$dexists and is not a directory!\n"; 155} 156} 157foreachmy$f(@afiles) { 158# This should return only one value 159if($f=~ m,(.*)/[^/]*$,) { 160my$p=$1; 161next if(grep{$_eq$p}@dirs); 162} 163my@status=grep(m/^File/, safe_pipe_capture('cvs','-q','status',$f)); 164if(@status>1) {warn'Strange! cvs status returned more than one line?'}; 165if(-d dirname $fand$status[0] !~m/Status: Unknown$/ 166and$status[0] !~m/^File: no file /) { 167$dirty=1; 168warn"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"; 169warn"Status was:$status[0]\n"; 170} 171} 172 173foreachmy$f(@files) { 174next ifgrep{$_eq$f}@afiles; 175# TODO:we need to handle removed in cvs 176my@status=grep(m/^File/, safe_pipe_capture('cvs','-q','status',$f)); 177if(@status>1) {warn'Strange! cvs status returned more than one line?'}; 178unless($status[0] =~m/Status: Up-to-date$/) { 179$dirty=1; 180warn"File$fnot up to date in your CVS checkout!\n"; 181} 182} 183if($dirty) { 184if($opt_f) {warn"The tree is not clean -- forced merge\n"; 185$dirty=0; 186}else{ 187die"Exiting: your CVS tree is not clean for this merge."; 188} 189} 190 191print"Applying\n"; 192`GIT_DIR= git-apply -C$fuzz--binary --summary --numstat --apply <.cvsexportcommit.diff`||die"cannot patch"; 193 194print"Patch applied successfully. Adding new files and directories to CVS\n"; 195my$dirtypatch=0; 196foreachmy$d(@dirs) { 197if(system('cvs','add',$d)) { 198$dirtypatch=1; 199warn"Failed to cvs add directory$d-- you may need to do it manually"; 200} 201} 202 203foreachmy$f(@afiles) { 204if(grep{$_eq$f}@bfiles) { 205system('cvs','add','-kb',$f); 206}else{ 207system('cvs','add',$f); 208} 209if($?) { 210$dirtypatch=1; 211warn"Failed to cvs add$f-- you may need to do it manually"; 212} 213} 214 215foreachmy$f(@dfiles) { 216system('cvs','rm','-f',$f); 217if($?) { 218$dirtypatch=1; 219warn"Failed to cvs rm -f$f-- you may need to do it manually"; 220} 221} 222 223print"Commit to CVS\n"; 224print"Patch title (first comment line):$title\n"; 225my@commitfiles=map{unless(m/\s/) {'\''.$_.'\''; }else{$_; }; } (@files); 226my$cmd="cvs commit -F .msg@commitfiles"; 227 228if($dirtypatch) { 229print"NOTE: One or more hunks failed to apply cleanly.\n"; 230print"You'll need to apply the patch in .cvsexportcommit.diff manually\n"; 231print"using a patch program. After applying the patch and resolving the\n"; 232print"problems you may commit using:"; 233print"\n$cmd\n\n"; 234exit(1); 235} 236 237if($opt_c) { 238print"Autocommit\n$cmd\n"; 239print safe_pipe_capture('cvs','commit','-F','.msg',@files); 240if($?) { 241die"Exiting: The commit did not succeed"; 242} 243print"Committed successfully to CVS\n"; 244}else{ 245print"Ready for you to commit, just run:\n\n$cmd\n"; 246} 247 248# clean up 249unlink(".cvsexportcommit.diff"); 250unlink(".msg"); 251 252sub usage { 253print STDERR <<END; 254Usage: GIT_DIR=/path/to/.git ${\basename$0} [-h] [-p] [-v] [-c] [-f] [-m msgprefix] [ parent ] commit 255END 256exit(1); 257} 258 259# An alternative to `command` that allows input to be passed as an array 260# to work around shell problems with weird characters in arguments 261# if the exec returns non-zero we die 262sub safe_pipe_capture { 263my@output; 264if(my$pid=open my$child,'-|') { 265@output= (<$child>); 266close$childor die join(' ',@_).":$!$?"; 267}else{ 268exec(@_)or die"$!$?";# exec() can fail the executable can't be found 269} 270returnwantarray?@output:join('',@output); 271} 272 273sub safe_pipe_capture_blob { 274my$output; 275if(my$pid=open my$child,'-|') { 276local$/; 277undef$/; 278$output= (<$child>); 279close$childor die join(' ',@_).":$!$?"; 280}else{ 281exec(@_)or die"$!$?";# exec() can fail the executable can't be found 282} 283return$output; 284}