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