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# 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} 291if($dirty) { 292if($opt_f) {warn"The tree is not clean -- forced merge\n"; 293$dirty=0; 294}else{ 295die"Exiting: your CVS tree is not clean for this merge."; 296} 297} 298 299print"Applying\n"; 300if($opt_W) { 301system("git checkout -q$commit^0") &&die"cannot patch"; 302}else{ 303`GIT_DIR= git-apply$context--summary --numstat --apply <.cvsexportcommit.diff`||die"cannot patch"; 304} 305 306print"Patch applied successfully. Adding new files and directories to CVS\n"; 307my$dirtypatch=0; 308 309# 310# We have to add the directories in order otherwise we will have 311# problems when we try and add the sub-directory of a directory we 312# have not added yet. 313# 314# Luckily this is easy to deal with by sorting the directories and 315# dealing with the shortest ones first. 316# 317@dirs=sort{length$a<=>length$b}@dirs; 318 319foreachmy$d(@dirs) { 320if(system(@cvs,'add',$d)) { 321$dirtypatch=1; 322warn"Failed to cvs add directory$d-- you may need to do it manually"; 323} 324} 325 326foreachmy$f(@afiles) { 327if(grep{$_eq$f}@bfiles) { 328system(@cvs,'add','-kb',$f); 329}else{ 330system(@cvs,'add',$f); 331} 332if($?) { 333$dirtypatch=1; 334warn"Failed to cvs add$f-- you may need to do it manually"; 335} 336} 337 338foreachmy$f(@dfiles) { 339system(@cvs,'rm','-f',$f); 340if($?) { 341$dirtypatch=1; 342warn"Failed to cvs rm -f$f-- you may need to do it manually"; 343} 344} 345 346print"Commit to CVS\n"; 347print"Patch title (first comment line):$title\n"; 348my@commitfiles=map{unless(m/\s/) {'\''.$_.'\''; }else{$_; }; } (@files); 349my$cmd=join(' ',@cvs)." commit -F .msg@commitfiles"; 350 351if($dirtypatch) { 352print"NOTE: One or more hunks failed to apply cleanly.\n"; 353print"You'll need to apply the patch in .cvsexportcommit.diff manually\n"; 354print"using a patch program. After applying the patch and resolving the\n"; 355print"problems you may commit using:"; 356print"\ncd\"$opt_w\""if$opt_w; 357print"\n$cmd\n"; 358print"\ngit checkout$go_back_to\n"if$go_back_to; 359print"\n"; 360exit(1); 361} 362 363if($opt_c) { 364print"Autocommit\n$cmd\n"; 365print xargs_safe_pipe_capture([@cvs,'commit','-F','.msg'],@files); 366if($?) { 367die"Exiting: The commit did not succeed"; 368} 369print"Committed successfully to CVS\n"; 370# clean up 371unlink(".msg"); 372}else{ 373print"Ready for you to commit, just run:\n\n$cmd\n"; 374} 375 376# clean up 377unlink(".cvsexportcommit.diff"); 378 379if($opt_W) { 380system("git checkout$go_back_to") &&die"cannot move back to$go_back_to"; 381if(!($go_back_to=~/^[0-9a-fA-F]{40}$/)) { 382system("git symbolic-ref HEAD$go_back_to") && 383die"cannot move back to$go_back_to"; 384} 385} 386 387# CVS version 1.11.x and 1.12.x sleeps the wrong way to ensure the timestamp 388# used by CVS and the one set by subsequence file modifications are different. 389# If they are not different CVS will not detect changes. 390sleep(1); 391 392sub usage { 393print STDERR <<END; 394Usage: GIT_DIR=/path/to/.git git cvsexportcommit [-h] [-p] [-v] [-c] [-f] [-u] [-w cvsworkdir] [-m msgprefix] [ parent ] commit 395END 396exit(1); 397} 398 399# An alternative to `command` that allows input to be passed as an array 400# to work around shell problems with weird characters in arguments 401# if the exec returns non-zero we die 402sub safe_pipe_capture { 403my@output; 404if(my$pid=open my$child,'-|') { 405@output= (<$child>); 406close$childor die join(' ',@_).":$!$?"; 407}else{ 408exec(@_)or die"$!$?";# exec() can fail the executable can't be found 409} 410returnwantarray?@output:join('',@output); 411} 412 413sub xargs_safe_pipe_capture { 414my$MAX_ARG_LENGTH=65536; 415my$cmd=shift; 416my@output; 417my$output; 418while(@_) { 419my@args; 420my$length=0; 421while(@_&&$length<$MAX_ARG_LENGTH) { 422push@args,shift; 423$length+=length($args[$#args]); 424} 425if(wantarray) { 426push@output, safe_pipe_capture(@$cmd,@args); 427} 428else{ 429$output.= safe_pipe_capture(@$cmd,@args); 430} 431} 432returnwantarray?@output:$output; 433}