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,$opt_k); 12 13getopts('uhPpvcfkam: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# CVS reports files that don't exist in the current revision as 229# "no file $basename" in its "status" output, so we should 230# anticipate that. Totally unknown files will have a status 231# "Unknown". However, if they exist in the Attic, their status 232# will be "Up-to-date" (this means they were added once but have 233# been removed). 234$basename="no file$basename"if$added{$basename}; 235 236$basename=~s/^\s+//; 237$basename=~s/\s+$//; 238 239if(!exists($fullname{$basename})) { 240$fullname{$basename} =$name; 241push(@canstatusfiles2,$name); 242delete($todo{$name}); 243} 244} 245my@cvsoutput; 246@cvsoutput= xargs_safe_pipe_capture([@cvs,'status'],@canstatusfiles2); 247foreachmy$l(@cvsoutput) { 248chomp$l; 249next unless 250my($file,$status) =$l=~/^File:\s+(.*\S)\s+Status: (.*)$/; 251 252my$fullname=$fullname{$file}; 253print STDERR "Huh? Status '$status' reported for unexpected file '$file'\n" 254unlessdefined$fullname; 255 256# This response means the file does not exist except in 257# CVS's attic, so set the status accordingly 258$status="In-attic" 259if$file=~/^no file / 260&&$statuseq'Up-to-date'; 261 262$cvsstat{$fullname{$file}} =$status 263ifdefined$fullname{$file}; 264} 265} 266} 267 268# ... Validate that new files have the correct status 269foreachmy$f(@afiles) { 270next unlessdefined(my$stat=$cvsstat{$f}); 271 272# This means the file has never been seen before 273next if$stateq'Unknown'; 274 275# This means the file has been seen before but was removed 276next if$stateq'In-attic'; 277 278$dirty=1; 279warn"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"; 280warn"Status was:$cvsstat{$f}\n"; 281} 282 283# ... validate known files. 284foreachmy$f(@files) { 285next ifgrep{$_eq$f}@afiles; 286# TODO:we need to handle removed in cvs 287unless(defined($cvsstat{$f})and$cvsstat{$f}eq"Up-to-date") { 288$dirty=1; 289warn"File$fnot up to date but has status '$cvsstat{$f}' in your CVS checkout!\n"; 290} 291 292# Depending on how your GIT tree got imported from CVS you may 293# have a conflict between expanded keywords in your CVS tree and 294# unexpanded keywords in the patch about to be applied. 295if($opt_k) { 296my$orig_file="$f.orig"; 297rename$f,$orig_file; 298open(FILTER_IN,"<$orig_file")or die"Cannot open$orig_file\n"; 299open(FILTER_OUT,">$f")or die"Cannot open$f\n"; 300while(<FILTER_IN>) 301{ 302my$line=$_; 303$line=~s/\$([A-Z][a-z]+):[^\$]+\$/\$$1\$/g; 304print FILTER_OUT $line; 305} 306close FILTER_IN; 307close FILTER_OUT; 308} 309} 310 311if($dirty) { 312if($opt_f) {warn"The tree is not clean -- forced merge\n"; 313$dirty=0; 314}else{ 315die"Exiting: your CVS tree is not clean for this merge."; 316} 317} 318 319print"Applying\n"; 320if($opt_W) { 321system("git checkout -q$commit^0") &&die"cannot patch"; 322}else{ 323`GIT_DIR= git-apply$context--summary --numstat --apply <.cvsexportcommit.diff`||die"cannot patch"; 324} 325 326print"Patch applied successfully. Adding new files and directories to CVS\n"; 327my$dirtypatch=0; 328 329# 330# We have to add the directories in order otherwise we will have 331# problems when we try and add the sub-directory of a directory we 332# have not added yet. 333# 334# Luckily this is easy to deal with by sorting the directories and 335# dealing with the shortest ones first. 336# 337@dirs=sort{length$a<=>length$b}@dirs; 338 339foreachmy$d(@dirs) { 340if(system(@cvs,'add',$d)) { 341$dirtypatch=1; 342warn"Failed to cvs add directory$d-- you may need to do it manually"; 343} 344} 345 346foreachmy$f(@afiles) { 347if(grep{$_eq$f}@bfiles) { 348system(@cvs,'add','-kb',$f); 349}else{ 350system(@cvs,'add',$f); 351} 352if($?) { 353$dirtypatch=1; 354warn"Failed to cvs add$f-- you may need to do it manually"; 355} 356} 357 358foreachmy$f(@dfiles) { 359system(@cvs,'rm','-f',$f); 360if($?) { 361$dirtypatch=1; 362warn"Failed to cvs rm -f$f-- you may need to do it manually"; 363} 364} 365 366print"Commit to CVS\n"; 367print"Patch title (first comment line):$title\n"; 368my@commitfiles=map{unless(m/\s/) {'\''.$_.'\''; }else{$_; }; } (@files); 369my$cmd=join(' ',@cvs)." commit -F .msg@commitfiles"; 370 371if($dirtypatch) { 372print"NOTE: One or more hunks failed to apply cleanly.\n"; 373print"You'll need to apply the patch in .cvsexportcommit.diff manually\n"; 374print"using a patch program. After applying the patch and resolving the\n"; 375print"problems you may commit using:"; 376print"\ncd\"$opt_w\""if$opt_w; 377print"\n$cmd\n"; 378print"\ngit checkout$go_back_to\n"if$go_back_to; 379print"\n"; 380exit(1); 381} 382 383if($opt_c) { 384print"Autocommit\n$cmd\n"; 385print xargs_safe_pipe_capture([@cvs,'commit','-F','.msg'],@files); 386if($?) { 387die"Exiting: The commit did not succeed"; 388} 389print"Committed successfully to CVS\n"; 390# clean up 391unlink(".msg"); 392}else{ 393print"Ready for you to commit, just run:\n\n$cmd\n"; 394} 395 396# clean up 397unlink(".cvsexportcommit.diff"); 398 399if($opt_W) { 400system("git checkout$go_back_to") &&die"cannot move back to$go_back_to"; 401if(!($go_back_to=~/^[0-9a-fA-F]{40}$/)) { 402system("git symbolic-ref HEAD$go_back_to") && 403die"cannot move back to$go_back_to"; 404} 405} 406 407# CVS version 1.11.x and 1.12.x sleeps the wrong way to ensure the timestamp 408# used by CVS and the one set by subsequence file modifications are different. 409# If they are not different CVS will not detect changes. 410sleep(1); 411 412sub usage { 413print STDERR <<END; 414Usage: GIT_DIR=/path/to/.git git cvsexportcommit [-h] [-p] [-v] [-c] [-f] [-u] [-k] [-w cvsworkdir] [-m msgprefix] [ parent ] commit 415END 416exit(1); 417} 418 419# An alternative to `command` that allows input to be passed as an array 420# to work around shell problems with weird characters in arguments 421# if the exec returns non-zero we die 422sub safe_pipe_capture { 423my@output; 424if(my$pid=open my$child,'-|') { 425@output= (<$child>); 426close$childor die join(' ',@_).":$!$?"; 427}else{ 428exec(@_)or die"$!$?";# exec() can fail the executable can't be found 429} 430returnwantarray?@output:join('',@output); 431} 432 433sub xargs_safe_pipe_capture { 434my$MAX_ARG_LENGTH=65536; 435my$cmd=shift; 436my@output; 437my$output; 438while(@_) { 439my@args; 440my$length=0; 441while(@_&&$length<$MAX_ARG_LENGTH) { 442push@args,shift; 443$length+=length($args[$#args]); 444} 445if(wantarray) { 446push@output, safe_pipe_capture(@$cmd,@args); 447} 448else{ 449$output.= safe_pipe_capture(@$cmd,@args); 450} 451} 452returnwantarray?@output:$output; 453}