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