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