git-svn: 0.9.1: add --version and copyright/license (GPL v2+) information
[git.git] / contrib / git-svn / git-svn.perl
1 #!/usr/bin/env perl
2 # Copyright (C) 2006, Eric Wong <normalperson@yhbt.net>
3 # License: GPL v2 or later
4 use warnings;
5 use strict;
6 use vars qw/    $AUTHOR $VERSION
7                 $SVN_URL $SVN_INFO $SVN_WC
8                 $GIT_SVN_INDEX $GIT_SVN
9                 $GIT_DIR $REV_DIR/;
10 $AUTHOR = 'Eric Wong <normalperson@yhbt.net>';
11 $VERSION = '0.9.1';
12 $GIT_DIR = $ENV{GIT_DIR} || "$ENV{PWD}/.git";
13 $GIT_SVN = $ENV{GIT_SVN_ID} || 'git-svn';
14 $GIT_SVN_INDEX = "$GIT_DIR/$GIT_SVN/index";
15 $ENV{GIT_DIR} ||= $GIT_DIR;
16 $SVN_URL = undef;
17 $REV_DIR = "$GIT_DIR/$GIT_SVN/revs";
18 $SVN_WC = "$GIT_DIR/$GIT_SVN/tree";
19
20 # make sure the svn binary gives consistent output between locales and TZs:
21 $ENV{TZ} = 'UTC';
22 $ENV{LC_ALL} = 'C';
23
24 # If SVN:: library support is added, please make the dependencies
25 # optional and preserve the capability to use the command-line client.
26 # use eval { require SVN::... } to make it lazy load
27 use Carp qw/croak/;
28 use IO::File qw//;
29 use File::Basename qw/dirname basename/;
30 use File::Path qw/mkpath/;
31 use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev/;
32 use File::Spec qw//;
33 my $sha1 = qr/[a-f\d]{40}/;
34 my $sha1_short = qr/[a-f\d]{6,40}/;
35 my ($_revision,$_stdin,$_no_ignore_ext,$_no_stop_copy,$_help,$_rmdir,$_edit,
36         $_find_copies_harder, $_l, $_version);
37
38 GetOptions(     'revision|r=s' => \$_revision,
39                 'no-ignore-externals' => \$_no_ignore_ext,
40                 'stdin|' => \$_stdin,
41                 'edit|e' => \$_edit,
42                 'rmdir' => \$_rmdir,
43                 'help|H|h' => \$_help,
44                 'find-copies-harder' => \$_find_copies_harder,
45                 'l=i' => \$_l,
46                 'version|V' => \$_version,
47                 'no-stop-on-copy' => \$_no_stop_copy );
48 my %cmd = (
49         fetch => [ \&fetch, "Download new revisions from SVN" ],
50         init => [ \&init, "Initialize and fetch (import)"],
51         commit => [ \&commit, "Commit git revisions to SVN" ],
52         rebuild => [ \&rebuild, "Rebuild git-svn metadata (after git clone)" ],
53         help => [ \&usage, "Show help" ],
54 );
55 my $cmd;
56 for (my $i = 0; $i < @ARGV; $i++) {
57         if (defined $cmd{$ARGV[$i]}) {
58                 $cmd = $ARGV[$i];
59                 splice @ARGV, $i, 1;
60                 last;
61         }
62 };
63
64 # we may be called as git-svn-(command), or git-svn(command).
65 foreach (keys %cmd) {
66         if (/git\-svn\-?($_)(?:\.\w+)?$/) {
67                 $cmd = $1;
68                 last;
69         }
70 }
71 usage(0) if $_help;
72 version() if $_version;
73 usage(1) unless (defined $cmd);
74 svn_check_ignore_externals();
75 $cmd{$cmd}->[0]->(@ARGV);
76 exit 0;
77
78 ####################### primary functions ######################
79 sub usage {
80         my $exit = shift || 0;
81         my $fd = $exit ? \*STDERR : \*STDOUT;
82         print $fd <<"";
83 git-svn - bidirectional operations between a single Subversion tree and git
84 Usage: $0 <command> [options] [arguments]\n
85 Available commands:
86
87         foreach (sort keys %cmd) {
88                 print $fd '  ',pack('A10',$_),$cmd{$_}->[1],"\n";
89         }
90         print $fd <<"";
91 \nGIT_SVN_ID may be set in the environment to an arbitrary identifier if
92 you're tracking multiple SVN branches/repositories in one git repository
93 and want to keep them separate.
94
95         exit $exit;
96 }
97
98 sub version {
99         print "git-svn version $VERSION\n";
100         exit 0;
101 }
102
103 sub rebuild {
104         $SVN_URL = shift or undef;
105         my $repo_uuid;
106         my $newest_rev = 0;
107
108         my $pid = open(my $rev_list,'-|');
109         defined $pid or croak $!;
110         if ($pid == 0) {
111                 exec("git-rev-list","$GIT_SVN-HEAD") or croak $!;
112         }
113         my $first;
114         while (<$rev_list>) {
115                 chomp;
116                 my $c = $_;
117                 croak "Non-SHA1: $c\n" unless $c =~ /^$sha1$/o;
118                 my @commit = grep(/^git-svn-id: /,`git-cat-file commit $c`);
119                 next if (!@commit); # skip merges
120                 my $id = $commit[$#commit];
121                 my ($url, $rev, $uuid) = ($id =~ /^git-svn-id:\s(\S+?)\@(\d+)
122                                                 \s([a-f\d\-]+)$/x);
123                 if (!$rev || !$uuid || !$url) {
124                         # some of the original repositories I made had
125                         # indentifiers like this:
126                         ($rev, $uuid) = ($id =~/^git-svn-id:\s(\d+)
127                                                         \@([a-f\d\-]+)/x);
128                         if (!$rev || !$uuid) {
129                                 croak "Unable to extract revision or UUID from ",
130                                         "$c, $id\n";
131                         }
132                 }
133                 print "r$rev = $c\n";
134                 unless (defined $first) {
135                         if (!$SVN_URL && !$url) {
136                                 croak "SVN repository location required: $url\n";
137                         }
138                         $SVN_URL ||= $url;
139                         $repo_uuid = setup_git_svn();
140                         $first = $rev;
141                 }
142                 if ($uuid ne $repo_uuid) {
143                         croak "Repository UUIDs do not match!\ngot: $uuid\n",
144                                                 "expected: $repo_uuid\n";
145                 }
146                 assert_revision_eq_or_unknown($rev, $c);
147                 sys('git-update-ref',"$GIT_SVN/revs/$rev",$c);
148                 $newest_rev = $rev if ($rev > $newest_rev);
149         }
150         close $rev_list or croak $?;
151         if (!chdir $SVN_WC) {
152                 my @svn_co = ('svn','co',"-r$first");
153                 push @svn_co, '--ignore-externals' unless $_no_ignore_ext;
154                 sys(@svn_co, $SVN_URL, $SVN_WC);
155                 chdir $SVN_WC or croak $!;
156         }
157
158         $pid = fork;
159         defined $pid or croak $!;
160         if ($pid == 0) {
161                 my @svn_up = qw(svn up);
162                 push @svn_up, '--ignore-externals' unless $_no_ignore_ext;
163                 sys(@svn_up,"-r$newest_rev");
164                 $ENV{GIT_INDEX_FILE} = $GIT_SVN_INDEX;
165                 git_addremove();
166                 exec('git-write-tree');
167         }
168         waitpid $pid, 0;
169 }
170
171 sub init {
172         $SVN_URL = shift or croak "SVN repository location required\n";
173         unless (-d $GIT_DIR) {
174                 sys('git-init-db');
175         }
176         setup_git_svn();
177 }
178
179 sub fetch {
180         my (@parents) = @_;
181         $SVN_URL ||= file_to_s("$GIT_DIR/$GIT_SVN/info/url");
182         my @log_args = -d $SVN_WC ? ($SVN_WC) : ($SVN_URL);
183         unless ($_revision) {
184                 $_revision = -d $SVN_WC ? 'BASE:HEAD' : '0:HEAD';
185         }
186         push @log_args, "-r$_revision";
187         push @log_args, '--stop-on-copy' unless $_no_stop_copy;
188
189         my $svn_log = svn_log_raw(@log_args);
190         @$svn_log = sort { $a->{revision} <=> $b->{revision} } @$svn_log;
191
192         my $base = shift @$svn_log or croak "No base revision!\n";
193         my $last_commit = undef;
194         unless (-d $SVN_WC) {
195                 my @svn_co = ('svn','co',"-r$base->{revision}");
196                 push @svn_co,'--ignore-externals' unless $_no_ignore_ext;
197                 sys(@svn_co, $SVN_URL, $SVN_WC);
198                 chdir $SVN_WC or croak $!;
199                 $last_commit = git_commit($base, @parents);
200                 unless (-f "$GIT_DIR/refs/heads/master") {
201                         sys(qw(git-update-ref refs/heads/master),$last_commit);
202                 }
203                 assert_svn_wc_clean($base->{revision}, $last_commit);
204         } else {
205                 chdir $SVN_WC or croak $!;
206                 $last_commit = file_to_s("$REV_DIR/$base->{revision}");
207         }
208         my @svn_up = qw(svn up);
209         push @svn_up, '--ignore-externals' unless $_no_ignore_ext;
210         my $last_rev = $base->{revision};
211         foreach my $log_msg (@$svn_log) {
212                 assert_svn_wc_clean($last_rev, $last_commit);
213                 $last_rev = $log_msg->{revision};
214                 sys(@svn_up,"-r$last_rev");
215                 $last_commit = git_commit($log_msg, $last_commit, @parents);
216         }
217         assert_svn_wc_clean($last_rev, $last_commit);
218         return pop @$svn_log;
219 }
220
221 sub commit {
222         my (@commits) = @_;
223         if ($_stdin || !@commits) {
224                 print "Reading from stdin...\n";
225                 @commits = ();
226                 while (<STDIN>) {
227                         if (/\b([a-f\d]{6,40})\b/) {
228                                 unshift @commits, $1;
229                         }
230                 }
231         }
232         my @revs;
233         foreach my $c (@commits) {
234                 chomp(my @tmp = safe_qx('git-rev-parse',$c));
235                 if (scalar @tmp == 1) {
236                         push @revs, $tmp[0];
237                 } elsif (scalar @tmp > 1) {
238                         push @revs, reverse (safe_qx('git-rev-list',@tmp));
239                 } else {
240                         die "Failed to rev-parse $c\n";
241                 }
242         }
243         chomp @revs;
244
245         fetch();
246         chdir $SVN_WC or croak $!;
247         my $svn_current_rev =  svn_info('.')->{'Last Changed Rev'};
248         foreach my $c (@revs) {
249                 print "Committing $c\n";
250                 my $mods = svn_checkout_tree($svn_current_rev, $c);
251                 if (scalar @$mods == 0) {
252                         print "Skipping, no changes detected\n";
253                         next;
254                 }
255                 $svn_current_rev = svn_commit_tree($svn_current_rev, $c);
256         }
257         print "Done committing ",scalar @revs," revisions to SVN\n";
258
259 }
260
261 ########################### utility functions #########################
262
263 sub setup_git_svn {
264         defined $SVN_URL or croak "SVN repository location required\n";
265         unless (-d $GIT_DIR) {
266                 croak "GIT_DIR=$GIT_DIR does not exist!\n";
267         }
268         mkpath(["$GIT_DIR/$GIT_SVN"]);
269         mkpath(["$GIT_DIR/$GIT_SVN/info"]);
270         mkpath([$REV_DIR]);
271         s_to_file($SVN_URL,"$GIT_DIR/$GIT_SVN/info/url");
272         my $uuid = svn_info($SVN_URL)->{'Repository UUID'} or
273                                         croak "Repository UUID unreadable\n";
274         s_to_file($uuid,"$GIT_DIR/$GIT_SVN/info/uuid");
275
276         open my $fd, '>>', "$GIT_DIR/$GIT_SVN/info/exclude" or croak $!;
277         print $fd '.svn',"\n";
278         close $fd or croak $!;
279         return $uuid;
280 }
281
282 sub assert_svn_wc_clean {
283         my ($svn_rev, $treeish) = @_;
284         croak "$svn_rev is not an integer!\n" unless ($svn_rev =~ /^\d+$/);
285         croak "$treeish is not a sha1!\n" unless ($treeish =~ /^$sha1$/o);
286         my $svn_info = svn_info('.');
287         if ($svn_rev != $svn_info->{'Last Changed Rev'}) {
288                 croak "Expected r$svn_rev, got r",
289                                 $svn_info->{'Last Changed Rev'},"\n";
290         }
291         my @status = grep(!/^Performing status on external/,(`svn status`));
292         @status = grep(!/^\s*$/,@status);
293         if (scalar @status) {
294                 print STDERR "Tree ($SVN_WC) is not clean:\n";
295                 print STDERR $_ foreach @status;
296                 croak;
297         }
298         assert_tree($treeish);
299 }
300
301 sub assert_tree {
302         my ($treeish) = @_;
303         croak "Not a sha1: $treeish\n" unless $treeish =~ /^$sha1$/o;
304         chomp(my $type = `git-cat-file -t $treeish`);
305         my $expected;
306         while ($type eq 'tag') {
307                 chomp(($treeish, $type) = `git-cat-file tag $treeish`);
308         }
309         if ($type eq 'commit') {
310                 $expected = (grep /^tree /,`git-cat-file commit $treeish`)[0];
311                 ($expected) = ($expected =~ /^tree ($sha1)$/);
312                 die "Unable to get tree from $treeish\n" unless $expected;
313         } elsif ($type eq 'tree') {
314                 $expected = $treeish;
315         } else {
316                 die "$treeish is a $type, expected tree, tag or commit\n";
317         }
318
319         my $old_index = $ENV{GIT_INDEX_FILE};
320         my $tmpindex = $GIT_SVN_INDEX.'.assert-tmp';
321         if (-e $tmpindex) {
322                 unlink $tmpindex or croak $!;
323         }
324         $ENV{GIT_INDEX_FILE} = $tmpindex;
325         git_addremove();
326         chomp(my $tree = `git-write-tree`);
327         if ($old_index) {
328                 $ENV{GIT_INDEX_FILE} = $old_index;
329         } else {
330                 delete $ENV{GIT_INDEX_FILE};
331         }
332         if ($tree ne $expected) {
333                 croak "Tree mismatch, Got: $tree, Expected: $expected\n";
334         }
335 }
336
337 sub parse_diff_tree {
338         my $diff_fh = shift;
339         local $/ = "\0";
340         my $state = 'meta';
341         my @mods;
342         while (<$diff_fh>) {
343                 chomp $_; # this gets rid of the trailing "\0"
344                 if ($state eq 'meta' && /^:(\d{6})\s(\d{6})\s
345                                         $sha1\s($sha1)\s([MTCRAD])\d*$/xo) {
346                         push @mods, {   mode_a => $1, mode_b => $2,
347                                         sha1_b => $3, chg => $4 };
348                         if ($4 =~ /^(?:C|R)$/) {
349                                 $state = 'file_a';
350                         } else {
351                                 $state = 'file_b';
352                         }
353                 } elsif ($state eq 'file_a') {
354                         my $x = $mods[$#mods] or croak "Empty array\n";
355                         if ($x->{chg} !~ /^(?:C|R)$/) {
356                                 croak "Error parsing $_, $x->{chg}\n";
357                         }
358                         $x->{file_a} = $_;
359                         $state = 'file_b';
360                 } elsif ($state eq 'file_b') {
361                         my $x = $mods[$#mods] or croak "Empty array\n";
362                         if (exists $x->{file_a} && $x->{chg} !~ /^(?:C|R)$/) {
363                                 croak "Error parsing $_, $x->{chg}\n";
364                         }
365                         if (!exists $x->{file_a} && $x->{chg} =~ /^(?:C|R)$/) {
366                                 croak "Error parsing $_, $x->{chg}\n";
367                         }
368                         $x->{file_b} = $_;
369                         $state = 'meta';
370                 } else {
371                         croak "Error parsing $_\n";
372                 }
373         }
374         close $diff_fh or croak $!;
375
376         return \@mods;
377 }
378
379 sub svn_check_prop_executable {
380         my $m = shift;
381         return if -l $m->{file_b};
382         if ($m->{mode_b} =~ /755$/) {
383                 chmod((0755 &~ umask),$m->{file_b}) or croak $!;
384                 if ($m->{mode_a} !~ /755$/) {
385                         sys(qw(svn propset svn:executable 1), $m->{file_b});
386                 }
387                 -x $m->{file_b} or croak "$m->{file_b} is not executable!\n";
388         } elsif ($m->{mode_b} !~ /755$/ && $m->{mode_a} =~ /755$/) {
389                 sys(qw(svn propdel svn:executable), $m->{file_b});
390                 chmod((0644 &~ umask),$m->{file_b}) or croak $!;
391                 -x $m->{file_b} and croak "$m->{file_b} is executable!\n";
392         }
393 }
394
395 sub svn_ensure_parent_path {
396         my $dir_b = dirname(shift);
397         svn_ensure_parent_path($dir_b) if ($dir_b ne File::Spec->curdir);
398         mkpath([$dir_b]) unless (-d $dir_b);
399         sys(qw(svn add -N), $dir_b) unless (-d "$dir_b/.svn");
400 }
401
402 sub precommit_check {
403         my $mods = shift;
404         my (%rm_file, %rmdir_check, %added_check);
405
406         my %o = ( D => 0, R => 1, C => 2, A => 3, M => 3, T => 3 );
407         foreach my $m (sort { $o{$a->{chg}} <=> $o{$b->{chg}} } @$mods) {
408                 if ($m->{chg} eq 'R') {
409                         if (-d $m->{file_b}) {
410                                 err_dir_to_file("$m->{file_a} => $m->{file_b}");
411                         }
412                         # dir/$file => dir/file/$file
413                         my $dirname = dirname($m->{file_b});
414                         while ($dirname ne File::Spec->curdir) {
415                                 if ($dirname ne $m->{file_a}) {
416                                         $dirname = dirname($dirname);
417                                         next;
418                                 }
419                                 err_file_to_dir("$m->{file_a} => $m->{file_b}");
420                         }
421                         # baz/zzz => baz (baz is a file)
422                         $dirname = dirname($m->{file_a});
423                         while ($dirname ne File::Spec->curdir) {
424                                 if ($dirname ne $m->{file_b}) {
425                                         $dirname = dirname($dirname);
426                                         next;
427                                 }
428                                 err_dir_to_file("$m->{file_a} => $m->{file_b}");
429                         }
430                 }
431                 if ($m->{chg} =~ /^(D|R)$/) {
432                         my $t = $1 eq 'D' ? 'file_b' : 'file_a';
433                         $rm_file{ $m->{$t} } = 1;
434                         my $dirname = dirname( $m->{$t} );
435                         my $basename = basename( $m->{$t} );
436                         $rmdir_check{$dirname}->{$basename} = 1;
437                 } elsif ($m->{chg} =~ /^(?:A|C)$/) {
438                         if (-d $m->{file_b}) {
439                                 err_dir_to_file($m->{file_b});
440                         }
441                         my $dirname = dirname( $m->{file_b} );
442                         my $basename = basename( $m->{file_b} );
443                         $added_check{$dirname}->{$basename} = 1;
444                         while ($dirname ne File::Spec->curdir) {
445                                 if ($rm_file{$dirname}) {
446                                         err_file_to_dir($m->{file_b});
447                                 }
448                                 $dirname = dirname $dirname;
449                         }
450                 }
451         }
452         return (\%rmdir_check, \%added_check);
453
454         sub err_dir_to_file {
455                 my $file = shift;
456                 print STDERR "Node change from directory to file ",
457                                 "is not supported by Subversion: ",$file,"\n";
458                 exit 1;
459         }
460         sub err_file_to_dir {
461                 my $file = shift;
462                 print STDERR "Node change from file to directory ",
463                                 "is not supported by Subversion: ",$file,"\n";
464                 exit 1;
465         }
466 }
467
468 sub svn_checkout_tree {
469         my ($svn_rev, $treeish) = @_;
470         my $from = file_to_s("$REV_DIR/$svn_rev");
471         assert_svn_wc_clean($svn_rev,$from);
472         print "diff-tree '$from' '$treeish'\n";
473         my $pid = open my $diff_fh, '-|';
474         defined $pid or croak $!;
475         if ($pid == 0) {
476                 my @diff_tree = qw(git-diff-tree -z -r -C);
477                 push @diff_tree, '--find-copies-harder' if $_find_copies_harder;
478                 push @diff_tree, "-l$_l" if defined $_l;
479                 exec(@diff_tree, $from, $treeish) or croak $!;
480         }
481         my $mods = parse_diff_tree($diff_fh);
482         unless (@$mods) {
483                 # git can do empty commits, SVN doesn't allow it...
484                 return $mods;
485         }
486         my ($rm, $add) = precommit_check($mods);
487
488         my %o = ( D => 1, R => 0, C => -1, A => 3, M => 3, T => 3 );
489         foreach my $m (sort { $o{$a->{chg}} <=> $o{$b->{chg}} } @$mods) {
490                 if ($m->{chg} eq 'C') {
491                         svn_ensure_parent_path( $m->{file_b} );
492                         sys(qw(svn cp),         $m->{file_a}, $m->{file_b});
493                         apply_mod_line_blob($m);
494                         svn_check_prop_executable($m);
495                 } elsif ($m->{chg} eq 'D') {
496                         sys(qw(svn rm --force), $m->{file_b});
497                 } elsif ($m->{chg} eq 'R') {
498                         svn_ensure_parent_path( $m->{file_b} );
499                         sys(qw(svn mv --force), $m->{file_a}, $m->{file_b});
500                         apply_mod_line_blob($m);
501                         svn_check_prop_executable($m);
502                 } elsif ($m->{chg} eq 'M') {
503                         apply_mod_line_blob($m);
504                         svn_check_prop_executable($m);
505                 } elsif ($m->{chg} eq 'T') {
506                         sys(qw(svn rm --force),$m->{file_b});
507                         apply_mod_line_blob($m);
508                         sys(qw(svn add --force), $m->{file_b});
509                         svn_check_prop_executable($m);
510                 } elsif ($m->{chg} eq 'A') {
511                         svn_ensure_parent_path( $m->{file_b} );
512                         apply_mod_line_blob($m);
513                         sys(qw(svn add --force), $m->{file_b});
514                         svn_check_prop_executable($m);
515                 } else {
516                         croak "Invalid chg: $m->{chg}\n";
517                 }
518         }
519
520         assert_tree($treeish);
521         if ($_rmdir) { # remove empty directories
522                 handle_rmdir($rm, $add);
523         }
524         assert_tree($treeish);
525         return $mods;
526 }
527
528 # svn ls doesn't work with respect to the current working tree, but what's
529 # in the repository.  There's not even an option for it... *sigh*
530 # (added files don't show up and removed files remain in the ls listing)
531 sub svn_ls_current {
532         my ($dir, $rm, $add) = @_;
533         chomp(my @ls = safe_qx('svn','ls',$dir));
534         my @ret = ();
535         foreach (@ls) {
536                 s#/$##; # trailing slashes are evil
537                 push @ret, $_ unless $rm->{$dir}->{$_};
538         }
539         if (exists $add->{$dir}) {
540                 push @ret, keys %{$add->{$dir}};
541         }
542         return \@ret;
543 }
544
545 sub handle_rmdir {
546         my ($rm, $add) = @_;
547
548         foreach my $dir (sort {length $b <=> length $a} keys %$rm) {
549                 my $ls = svn_ls_current($dir, $rm, $add);
550                 next if (scalar @$ls);
551                 sys(qw(svn rm --force),$dir);
552
553                 my $dn = dirname $dir;
554                 $rm->{ $dn }->{ basename $dir } = 1;
555                 $ls = svn_ls_current($dn, $rm, $add);
556                 while (scalar @$ls == 0 && $dn ne File::Spec->curdir) {
557                         sys(qw(svn rm --force),$dn);
558                         $dir = basename $dn;
559                         $dn = dirname $dn;
560                         $rm->{ $dn }->{ $dir } = 1;
561                         $ls = svn_ls_current($dn, $rm, $add);
562                 }
563         }
564 }
565
566 sub svn_commit_tree {
567         my ($svn_rev, $commit) = @_;
568         my $commit_msg = "$GIT_DIR/$GIT_SVN/.svn-commit.tmp.$$";
569         open my $msg, '>', $commit_msg  or croak $!;
570
571         chomp(my $type = `git-cat-file -t $commit`);
572         if ($type eq 'commit') {
573                 my $pid = open my $msg_fh, '-|';
574                 defined $pid or croak $!;
575
576                 if ($pid == 0) {
577                         exec(qw(git-cat-file commit), $commit) or croak $!;
578                 }
579                 my $in_msg = 0;
580                 while (<$msg_fh>) {
581                         if (!$in_msg) {
582                                 $in_msg = 1 if (/^\s*$/);
583                         } else {
584                                 print $msg $_ or croak $!;
585                         }
586                 }
587                 close $msg_fh or croak $!;
588         }
589         close $msg or croak $!;
590
591         if ($_edit || ($type eq 'tree')) {
592                 my $editor = $ENV{VISUAL} || $ENV{EDITOR} || 'vi';
593                 system($editor, $commit_msg);
594         }
595         my @ci_output = safe_qx(qw(svn commit -F),$commit_msg);
596         my ($committed) = grep(/^Committed revision \d+\./,@ci_output);
597         unlink $commit_msg;
598         defined $committed or croak
599                         "Commit output failed to parse committed revision!\n",
600                         join("\n",@ci_output),"\n";
601         my ($rev_committed) = ($committed =~ /^Committed revision (\d+)\./);
602
603         # resync immediately
604         my @svn_up = (qw(svn up), "-r$svn_rev");
605         push @svn_up, '--ignore-externals' unless $_no_ignore_ext;
606         sys(@svn_up);
607         return fetch("$rev_committed=$commit")->{revision};
608 }
609
610 sub svn_log_raw {
611         my (@log_args) = @_;
612         my $pid = open my $log_fh,'-|';
613         defined $pid or croak $!;
614
615         if ($pid == 0) {
616                 exec (qw(svn log), @log_args) or croak $!
617         }
618
619         my @svn_log;
620         my $state = 'sep';
621         while (<$log_fh>) {
622                 chomp;
623                 if (/^\-{72}$/) {
624                         if ($state eq 'msg') {
625                                 if ($svn_log[$#svn_log]->{lines}) {
626                                         $svn_log[$#svn_log]->{msg} .= $_."\n";
627                                         unless(--$svn_log[$#svn_log]->{lines}) {
628                                                 $state = 'sep';
629                                         }
630                                 } else {
631                                         croak "Log parse error at: $_\n",
632                                                 $svn_log[$#svn_log]->{revision},
633                                                 "\n";
634                                 }
635                                 next;
636                         }
637                         if ($state ne 'sep') {
638                                 croak "Log parse error at: $_\n",
639                                         "state: $state\n",
640                                         $svn_log[$#svn_log]->{revision},
641                                         "\n";
642                         }
643                         $state = 'rev';
644
645                         # if we have an empty log message, put something there:
646                         if (@svn_log) {
647                                 $svn_log[$#svn_log]->{msg} ||= "\n";
648                                 delete $svn_log[$#svn_log]->{lines};
649                         }
650                         next;
651                 }
652                 if ($state eq 'rev' && s/^r(\d+)\s*\|\s*//) {
653                         my $rev = $1;
654                         my ($author, $date, $lines) = split(/\s*\|\s*/, $_, 3);
655                         ($lines) = ($lines =~ /(\d+)/);
656                         my ($Y,$m,$d,$H,$M,$S,$tz) = ($date =~
657                                         /(\d{4})\-(\d\d)\-(\d\d)\s
658                                          (\d\d)\:(\d\d)\:(\d\d)\s([\-\+]\d+)/x)
659                                          or croak "Failed to parse date: $date\n";
660                         my %log_msg = ( revision => $rev,
661                                         date => "$tz $Y-$m-$d $H:$M:$S",
662                                         author => $author,
663                                         lines => $lines,
664                                         msg => '' );
665                         push @svn_log, \%log_msg;
666                         $state = 'msg_start';
667                         next;
668                 }
669                 # skip the first blank line of the message:
670                 if ($state eq 'msg_start' && /^$/) {
671                         $state = 'msg';
672                 } elsif ($state eq 'msg') {
673                         if ($svn_log[$#svn_log]->{lines}) {
674                                 $svn_log[$#svn_log]->{msg} .= $_."\n";
675                                 unless (--$svn_log[$#svn_log]->{lines}) {
676                                         $state = 'sep';
677                                 }
678                         } else {
679                                 croak "Log parse error at: $_\n",
680                                         $svn_log[$#svn_log]->{revision},"\n";
681                         }
682                 }
683         }
684         close $log_fh or croak $?;
685         return \@svn_log;
686 }
687
688 sub svn_info {
689         my $url = shift || $SVN_URL;
690
691         my $pid = open my $info_fh, '-|';
692         defined $pid or croak $!;
693
694         if ($pid == 0) {
695                 exec(qw(svn info),$url) or croak $!;
696         }
697
698         my $ret = {};
699         # only single-lines seem to exist in svn info output
700         while (<$info_fh>) {
701                 chomp $_;
702                 if (m#^([^:]+)\s*:\s*(\S*)$#) {
703                         $ret->{$1} = $2;
704                         push @{$ret->{-order}}, $1;
705                 }
706         }
707         close $info_fh or croak $!;
708         return $ret;
709 }
710
711 sub sys { system(@_) == 0 or croak $? }
712
713 sub git_addremove {
714         system( "git-diff-files --name-only -z ".
715                                 " | git-update-index --remove -z --stdin && ".
716                 "git-ls-files -z --others ".
717                         "'--exclude-from=$GIT_DIR/$GIT_SVN/info/exclude'".
718                                 " | git-update-index --add -z --stdin"
719                 ) == 0 or croak $?
720 }
721
722 sub s_to_file {
723         my ($str, $file, $mode) = @_;
724         open my $fd,'>',$file or croak $!;
725         print $fd $str,"\n" or croak $!;
726         close $fd or croak $!;
727         chmod ($mode &~ umask, $file) if (defined $mode);
728 }
729
730 sub file_to_s {
731         my $file = shift;
732         open my $fd,'<',$file or croak "$!: file: $file\n";
733         local $/;
734         my $ret = <$fd>;
735         close $fd or croak $!;
736         $ret =~ s/\s*$//s;
737         return $ret;
738 }
739
740 sub assert_revision_unknown {
741         my $revno = shift;
742         if (-f "$REV_DIR/$revno") {
743                 croak "$REV_DIR/$revno already exists! ",
744                                 "Why are we refetching it?";
745         }
746 }
747
748 sub assert_revision_eq_or_unknown {
749         my ($revno, $commit) = @_;
750         if (-f "$REV_DIR/$revno") {
751                 my $current = file_to_s("$REV_DIR/$revno");
752                 if ($commit ne $current) {
753                         croak "$REV_DIR/$revno already exists!\n",
754                                 "current: $current\nexpected: $commit\n";
755                 }
756                 return;
757         }
758 }
759
760 sub git_commit {
761         my ($log_msg, @parents) = @_;
762         assert_revision_unknown($log_msg->{revision});
763         my $out_fh = IO::File->new_tmpfile or croak $!;
764         my $info = svn_info('.');
765         my $uuid = $info->{'Repository UUID'};
766         defined $uuid or croak "Unable to get Repository UUID\n";
767
768         # commit parents can be conditionally bound to a particular
769         # svn revision via: "svn_revno=commit_sha1", filter them out here:
770         my @exec_parents;
771         foreach my $p (@parents) {
772                 next unless defined $p;
773                 if ($p =~ /^(\d+)=($sha1_short)$/o) {
774                         if ($1 == $log_msg->{revision}) {
775                                 push @exec_parents, $2;
776                         }
777                 } else {
778                         push @exec_parents, $p if $p =~ /$sha1_short/o;
779                 }
780         }
781
782         my $pid = fork;
783         defined $pid or croak $!;
784         if ($pid == 0) {
785                 $ENV{GIT_INDEX_FILE} = $GIT_SVN_INDEX;
786                 git_addremove();
787                 chomp(my $tree = `git-write-tree`);
788                 croak if $?;
789                 my $msg_fh = IO::File->new_tmpfile or croak $!;
790                 print $msg_fh $log_msg->{msg}, "\ngit-svn-id: ",
791                                         "$SVN_URL\@$log_msg->{revision}",
792                                         " $uuid\n" or croak $!;
793                 $msg_fh->flush == 0 or croak $!;
794                 seek $msg_fh, 0, 0 or croak $!;
795
796                 $ENV{GIT_AUTHOR_NAME} = $ENV{GIT_COMMITTER_NAME} =
797                                                 $log_msg->{author};
798                 $ENV{GIT_AUTHOR_EMAIL} = $ENV{GIT_COMMITTER_EMAIL} =
799                                                 $log_msg->{author}."\@$uuid";
800                 $ENV{GIT_AUTHOR_DATE} = $ENV{GIT_COMMITTER_DATE} =
801                                                 $log_msg->{date};
802                 my @exec = ('git-commit-tree',$tree);
803                 push @exec, '-p', $_  foreach @exec_parents;
804                 open STDIN, '<&', $msg_fh or croak $!;
805                 open STDOUT, '>&', $out_fh or croak $!;
806                 exec @exec or croak $!;
807         }
808         waitpid($pid,0);
809         croak if $?;
810
811         $out_fh->flush == 0 or croak $!;
812         seek $out_fh, 0, 0 or croak $!;
813         chomp(my $commit = do { local $/; <$out_fh> });
814         if ($commit !~ /^$sha1$/o) {
815                 croak "Failed to commit, invalid sha1: $commit\n";
816         }
817         my @update_ref = ('git-update-ref',"refs/heads/$GIT_SVN-HEAD",$commit);
818         if (my $primary_parent = shift @exec_parents) {
819                 push @update_ref, $primary_parent;
820         }
821         sys(@update_ref);
822         sys('git-update-ref',"$GIT_SVN/revs/$log_msg->{revision}",$commit);
823         print "r$log_msg->{revision} = $commit\n";
824         return $commit;
825 }
826
827 sub apply_mod_line_blob {
828         my $m = shift;
829         if ($m->{mode_b} =~ /^120/) {
830                 blob_to_symlink($m->{sha1_b}, $m->{file_b});
831         } else {
832                 blob_to_file($m->{sha1_b}, $m->{file_b});
833         }
834 }
835
836 sub blob_to_symlink {
837         my ($blob, $link) = @_;
838         defined $link or croak "\$link not defined!\n";
839         croak "Not a sha1: $blob\n" unless $blob =~ /^$sha1$/o;
840         if (-l $link || -f _) {
841                 unlink $link or croak $!;
842         }
843
844         my $dest = `git-cat-file blob $blob`; # no newline, so no chomp
845         symlink $dest, $link or croak $!;
846 }
847
848 sub blob_to_file {
849         my ($blob, $file) = @_;
850         defined $file or croak "\$file not defined!\n";
851         croak "Not a sha1: $blob\n" unless $blob =~ /^$sha1$/o;
852         if (-l $file || -f _) {
853                 unlink $file or croak $!;
854         }
855
856         open my $blob_fh, '>', $file or croak "$!: $file\n";
857         my $pid = fork;
858         defined $pid or croak $!;
859
860         if ($pid == 0) {
861                 open STDOUT, '>&', $blob_fh or croak $!;
862                 exec('git-cat-file','blob',$blob);
863         }
864         waitpid $pid, 0;
865         croak $? if $?;
866
867         close $blob_fh or croak $!;
868 }
869
870 sub safe_qx {
871         my $pid = open my $child, '-|';
872         defined $pid or croak $!;
873         if ($pid == 0) {
874                 exec(@_) or croak $?;
875         }
876         my @ret = (<$child>);
877         close $child or croak $?;
878         die $? if $?; # just in case close didn't error out
879         return wantarray ? @ret : join('',@ret);
880 }
881
882 sub svn_check_ignore_externals {
883         return if $_no_ignore_ext;
884         unless (grep /ignore-externals/,(safe_qx(qw(svn co -h)))) {
885                 print STDERR "W: Installed svn version does not support ",
886                                 "--ignore-externals\n";
887                 $_no_ignore_ext = 1;
888         }
889 }
890 __END__
891
892 Data structures:
893
894 @svn_log = array of log_msg hashes
895
896 $log_msg hash
897 {
898         msg => 'whitespace-formatted log entry
899 ',                                              # trailing newline is preserved
900         revision => '8',                        # integer
901         date => '2004-02-24T17:01:44.108345Z',  # commit date
902         author => 'committer name'
903 };
904
905
906 @mods = array of diff-index line hashes, each element represents one line
907         of diff-index output
908
909 diff-index line ($m hash)
910 {
911         mode_a => first column of diff-index output, no leading ':',
912         mode_b => second column of diff-index output,
913         sha1_b => sha1sum of the final blob,
914         chg => change type [MCRAD],
915         file_a => original file name of a file (iff chg is 'C' or 'R')
916         file_b => new/current file name of a file (any chg)
917 }
918 ;