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