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