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