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