git-svn: add support for Perl SVN::* libraries
[git/gitweb.git] / contrib / git-svn / git-svn.perl
blob9618c8bab5263ca0552209f9d8591ccd3d58434d
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 $SVN_UUID
8 $GIT_SVN_INDEX $GIT_SVN
9 $GIT_DIR $REV_DIR $GIT_SVN_DIR/;
10 $AUTHOR = 'Eric Wong <normalperson@yhbt.net>';
11 $VERSION = '1.1.0-pre';
13 use Cwd qw/abs_path/;
14 $GIT_DIR = abs_path($ENV{GIT_DIR} || '.git');
15 $ENV{GIT_DIR} = $GIT_DIR;
17 my $LC_ALL = $ENV{LC_ALL};
18 my $TZ = $ENV{TZ};
19 # make sure the svn binary gives consistent output between locales and TZs:
20 $ENV{TZ} = 'UTC';
21 $ENV{LC_ALL} = 'C';
23 # If SVN:: library support is added, please make the dependencies
24 # optional and preserve the capability to use the command-line client.
25 # use eval { require SVN::... } to make it lazy load
26 # We don't use any modules not in the standard Perl distribution:
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 pass_through/;
32 use File::Spec qw//;
33 use POSIX qw/strftime/;
35 my ($SVN_PATH, $SVN, $SVN_LOG, $_use_lib);
36 $_use_lib = 1 unless $ENV{GIT_SVN_NO_LIB};
37 libsvn_load();
38 my $sha1 = qr/[a-f\d]{40}/;
39 my $sha1_short = qr/[a-f\d]{4,40}/;
40 my ($_revision,$_stdin,$_no_ignore_ext,$_no_stop_copy,$_help,$_rmdir,$_edit,
41 $_find_copies_harder, $_l, $_cp_similarity,
42 $_repack, $_repack_nr, $_repack_flags,
43 $_template, $_shared, $_no_default_regex, $_no_graft_copy,
44 $_limit, $_verbose, $_incremental, $_oneline, $_l_fmt, $_show_commit,
45 $_version, $_upgrade, $_authors, $_branch_all_refs, @_opt_m);
46 my (@_branch_from, %tree_map, %users, %rusers);
47 my ($_svn_co_url_revs, $_svn_pg_peg_revs);
48 my @repo_path_split_cache;
50 my %fc_opts = ( 'no-ignore-externals' => \$_no_ignore_ext,
51 'branch|b=s' => \@_branch_from,
52 'branch-all-refs|B' => \$_branch_all_refs,
53 'authors-file|A=s' => \$_authors,
54 'repack:i' => \$_repack,
55 'repack-flags|repack-args|repack-opts=s' => \$_repack_flags);
57 my ($_trunk, $_tags, $_branches);
58 my %multi_opts = ( 'trunk|T=s' => \$_trunk,
59 'tags|t=s' => \$_tags,
60 'branches|b=s' => \$_branches );
61 my %init_opts = ( 'template=s' => \$_template, 'shared' => \$_shared );
63 # yes, 'native' sets "\n". Patches to fix this for non-*nix systems welcome:
64 my %EOL = ( CR => "\015", LF => "\012", CRLF => "\015\012", native => "\012" );
66 my %cmd = (
67 fetch => [ \&fetch, "Download new revisions from SVN",
68 { 'revision|r=s' => \$_revision, %fc_opts } ],
69 init => [ \&init, "Initialize a repo for tracking" .
70 " (requires URL argument)",
71 \%init_opts ],
72 commit => [ \&commit, "Commit git revisions to SVN",
73 { 'stdin|' => \$_stdin,
74 'edit|e' => \$_edit,
75 'rmdir' => \$_rmdir,
76 'find-copies-harder' => \$_find_copies_harder,
77 'l=i' => \$_l,
78 'copy-similarity|C=i'=> \$_cp_similarity,
79 %fc_opts,
80 } ],
81 'show-ignore' => [ \&show_ignore, "Show svn:ignore listings",
82 { 'revision|r=i' => \$_revision } ],
83 rebuild => [ \&rebuild, "Rebuild git-svn metadata (after git clone)",
84 { 'no-ignore-externals' => \$_no_ignore_ext,
85 'upgrade' => \$_upgrade } ],
86 'graft-branches' => [ \&graft_branches,
87 'Detect merges/branches from already imported history',
88 { 'merge-rx|m' => \@_opt_m,
89 'no-default-regex' => \$_no_default_regex,
90 'no-graft-copy' => \$_no_graft_copy } ],
91 'multi-init' => [ \&multi_init,
92 'Initialize multiple trees (like git-svnimport)',
93 { %multi_opts, %fc_opts } ],
94 'multi-fetch' => [ \&multi_fetch,
95 'Fetch multiple trees (like git-svnimport)',
96 \%fc_opts ],
97 'log' => [ \&show_log, 'Show commit logs',
98 { 'limit=i' => \$_limit,
99 'revision|r=s' => \$_revision,
100 'verbose|v' => \$_verbose,
101 'incremental' => \$_incremental,
102 'oneline' => \$_oneline,
103 'show-commit' => \$_show_commit,
104 'authors-file|A=s' => \$_authors,
105 } ],
108 my $cmd;
109 for (my $i = 0; $i < @ARGV; $i++) {
110 if (defined $cmd{$ARGV[$i]}) {
111 $cmd = $ARGV[$i];
112 splice @ARGV, $i, 1;
113 last;
117 my %opts = %{$cmd{$cmd}->[2]} if (defined $cmd);
119 read_repo_config(\%opts);
120 my $rv = GetOptions(%opts, 'help|H|h' => \$_help,
121 'version|V' => \$_version,
122 'id|i=s' => \$GIT_SVN);
123 exit 1 if (!$rv && $cmd ne 'log');
125 set_default_vals();
126 usage(0) if $_help;
127 version() if $_version;
128 usage(1) unless defined $cmd;
129 init_vars();
130 load_authors() if $_authors;
131 load_all_refs() if $_branch_all_refs;
132 svn_compat_check();
133 migration_check() unless $cmd =~ /^(?:init|multi-init)$/;
134 $cmd{$cmd}->[0]->(@ARGV);
135 exit 0;
137 ####################### primary functions ######################
138 sub usage {
139 my $exit = shift || 0;
140 my $fd = $exit ? \*STDERR : \*STDOUT;
141 print $fd <<"";
142 git-svn - bidirectional operations between a single Subversion tree and git
143 Usage: $0 <command> [options] [arguments]\n
145 print $fd "Available commands:\n" unless $cmd;
147 foreach (sort keys %cmd) {
148 next if $cmd && $cmd ne $_;
149 print $fd ' ',pack('A13',$_),$cmd{$_}->[1],"\n";
150 foreach (keys %{$cmd{$_}->[2]}) {
151 # prints out arguments as they should be passed:
152 my $x = s#[:=]s$## ? '<arg>' : s#[:=]i$## ? '<num>' : '';
153 print $fd ' ' x 17, join(', ', map { length $_ > 1 ?
154 "--$_" : "-$_" }
155 split /\|/,$_)," $x\n";
158 print $fd <<"";
159 \nGIT_SVN_ID may be set in the environment or via the --id/-i switch to an
160 arbitrary identifier if you're tracking multiple SVN branches/repositories in
161 one git repository and want to keep them separate. See git-svn(1) for more
162 information.
164 exit $exit;
167 sub version {
168 print "git-svn version $VERSION\n";
169 exit 0;
172 sub rebuild {
173 $SVN_URL = shift or undef;
174 my $newest_rev = 0;
175 if ($_upgrade) {
176 sys('git-update-ref',"refs/remotes/$GIT_SVN","$GIT_SVN-HEAD");
177 } else {
178 check_upgrade_needed();
181 my $pid = open(my $rev_list,'-|');
182 defined $pid or croak $!;
183 if ($pid == 0) {
184 exec("git-rev-list","refs/remotes/$GIT_SVN") or croak $!;
186 my $latest;
187 while (<$rev_list>) {
188 chomp;
189 my $c = $_;
190 croak "Non-SHA1: $c\n" unless $c =~ /^$sha1$/o;
191 my @commit = grep(/^git-svn-id: /,`git-cat-file commit $c`);
192 next if (!@commit); # skip merges
193 my ($url, $rev, $uuid) = extract_metadata($commit[$#commit]);
194 if (!$rev || !$uuid) {
195 croak "Unable to extract revision or UUID from ",
196 "$c, $commit[$#commit]\n";
199 # if we merged or otherwise started elsewhere, this is
200 # how we break out of it
201 next if (defined $SVN_UUID && ($uuid ne $SVN_UUID));
202 next if (defined $SVN_URL && defined $url && ($url ne $SVN_URL));
204 print "r$rev = $c\n";
205 unless (defined $latest) {
206 if (!$SVN_URL && !$url) {
207 croak "SVN repository location required: $url\n";
209 $SVN_URL ||= $url;
210 $SVN_UUID ||= $uuid;
211 setup_git_svn();
212 $latest = $rev;
214 assert_revision_eq_or_unknown($rev, $c);
215 sys('git-update-ref',"svn/$GIT_SVN/revs/$rev",$c);
216 $newest_rev = $rev if ($rev > $newest_rev);
218 close $rev_list or croak $?;
220 goto out if $_use_lib;
221 if (!chdir $SVN_WC) {
222 svn_cmd_checkout($SVN_URL, $latest, $SVN_WC);
223 chdir $SVN_WC or croak $!;
226 $pid = fork;
227 defined $pid or croak $!;
228 if ($pid == 0) {
229 my @svn_up = qw(svn up);
230 push @svn_up, '--ignore-externals' unless $_no_ignore_ext;
231 sys(@svn_up,"-r$newest_rev");
232 $ENV{GIT_INDEX_FILE} = $GIT_SVN_INDEX;
233 index_changes();
234 exec('git-write-tree') or croak $!;
236 waitpid $pid, 0;
237 croak $? if $?;
238 out:
239 if ($_upgrade) {
240 print STDERR <<"";
241 Keeping deprecated refs/head/$GIT_SVN-HEAD for now. Please remove it
242 when you have upgraded your tools and habits to use refs/remotes/$GIT_SVN
247 sub init {
248 $SVN_URL = shift or die "SVN repository location required " .
249 "as a command-line argument\n";
250 $SVN_URL =~ s!/+$!!; # strip trailing slash
251 unless (-d $GIT_DIR) {
252 my @init_db = ('git-init-db');
253 push @init_db, "--template=$_template" if defined $_template;
254 push @init_db, "--shared" if defined $_shared;
255 sys(@init_db);
257 setup_git_svn();
260 sub fetch {
261 check_upgrade_needed();
262 $SVN_URL ||= file_to_s("$GIT_SVN_DIR/info/url");
263 my $ret = $_use_lib ? fetch_lib(@_) : fetch_cmd(@_);
264 if ($ret->{commit} && quiet_run(qw(git-rev-parse --verify
265 refs/heads/master^0))) {
266 sys(qw(git-update-ref refs/heads/master),$ret->{commit});
268 return $ret;
271 sub fetch_cmd {
272 my (@parents) = @_;
273 my @log_args = -d $SVN_WC ? ($SVN_WC) : ($SVN_URL);
274 unless ($_revision) {
275 $_revision = -d $SVN_WC ? 'BASE:HEAD' : '0:HEAD';
277 push @log_args, "-r$_revision";
278 push @log_args, '--stop-on-copy' unless $_no_stop_copy;
280 my $svn_log = svn_log_raw(@log_args);
282 my $base = next_log_entry($svn_log) or croak "No base revision!\n";
283 my $last_commit = undef;
284 unless (-d $SVN_WC) {
285 svn_cmd_checkout($SVN_URL,$base->{revision},$SVN_WC);
286 chdir $SVN_WC or croak $!;
287 read_uuid();
288 $last_commit = git_commit($base, @parents);
289 assert_tree($last_commit);
290 } else {
291 chdir $SVN_WC or croak $!;
292 read_uuid();
293 eval { $last_commit = file_to_s("$REV_DIR/$base->{revision}") };
294 # looks like a user manually cp'd and svn switch'ed
295 unless ($last_commit) {
296 sys(qw/svn revert -R ./);
297 assert_svn_wc_clean($base->{revision});
298 $last_commit = git_commit($base, @parents);
299 assert_tree($last_commit);
302 my @svn_up = qw(svn up);
303 push @svn_up, '--ignore-externals' unless $_no_ignore_ext;
304 my $last = $base;
305 while (my $log_msg = next_log_entry($svn_log)) {
306 assert_tree($last_commit);
307 if ($last->{revision} >= $log_msg->{revision}) {
308 croak "Out of order: last >= current: ",
309 "$last->{revision} >= $log_msg->{revision}\n";
311 # Revert is needed for cases like:
312 # https://svn.musicpd.org/Jamming/trunk (r166:167), but
313 # I can't seem to reproduce something like that on a test...
314 sys(qw/svn revert -R ./);
315 assert_svn_wc_clean($last->{revision});
316 sys(@svn_up,"-r$log_msg->{revision}");
317 $last_commit = git_commit($log_msg, $last_commit, @parents);
318 $last = $log_msg;
320 close $svn_log->{fh};
321 $last->{commit} = $last_commit;
322 return $last;
325 sub fetch_lib {
326 my (@parents) = @_;
327 $SVN_URL ||= file_to_s("$GIT_SVN_DIR/info/url");
328 my $repo;
329 ($repo, $SVN_PATH) = repo_path_split($SVN_URL);
330 $SVN_LOG ||= libsvn_connect($repo);
331 $SVN ||= libsvn_connect($repo);
332 my ($last_rev, $last_commit) = svn_grab_base_rev();
333 my ($base, $head) = libsvn_parse_revision($last_rev);
334 if ($base > $head) {
335 return { revision => $last_rev, commit => $last_commit }
337 my $index = set_index($GIT_SVN_INDEX);
339 # limit ourselves and also fork() since get_log won't release memory
340 # after processing a revision and SVN stuff seems to leak
341 my $inc = 1000;
342 my ($min, $max) = ($base, $head < $base+$inc ? $head : $base+$inc);
343 read_uuid();
344 if (defined $last_commit) {
345 unless (-e $GIT_SVN_INDEX) {
346 sys(qw/git-read-tree/, $last_commit);
348 chomp (my $x = `git-write-tree`);
349 my ($y) = (`git-cat-file commit $last_commit`
350 =~ /^tree ($sha1)/m);
351 if ($y ne $x) {
352 unlink $GIT_SVN_INDEX or croak $!;
353 sys(qw/git-read-tree/, $last_commit);
355 chomp ($x = `git-write-tree`);
356 if ($y ne $x) {
357 print STDERR "trees ($last_commit) $y != $x\n",
358 "Something is seriously wrong...\n";
361 while (1) {
362 # fork, because using SVN::Pool with get_log() still doesn't
363 # seem to help enough to keep memory usage down.
364 defined(my $pid = fork) or croak $!;
365 if (!$pid) {
366 $SVN::Error::handler = \&libsvn_skip_unknown_revs;
367 print "Fetching revisions $min .. $max\n";
369 # Yes I'm perfectly aware that the fourth argument
370 # below is the limit revisions number. Unfortunately
371 # performance sucks with it enabled, so it's much
372 # faster to fetch revision ranges instead of relying
373 # on the limiter.
374 $SVN_LOG->get_log( '/'.$SVN_PATH, $min, $max, 0, 1, 1,
375 sub {
376 my $log_msg;
377 if ($last_commit) {
378 $log_msg = libsvn_fetch(
379 $last_commit, @_);
380 $last_commit = git_commit(
381 $log_msg,
382 $last_commit,
383 @parents);
384 } else {
385 $log_msg = libsvn_new_tree(@_);
386 $last_commit = git_commit(
387 $log_msg, @parents);
390 $SVN::Error::handler = sub { 'quiet warnings' };
391 exit 0;
393 waitpid $pid, 0;
394 croak $? if $?;
395 ($last_rev, $last_commit) = svn_grab_base_rev();
396 last if ($max >= $head);
397 $min = $max + 1;
398 $max += $inc;
399 $max = $head if ($max > $head);
401 restore_index($index);
402 return { revision => $last_rev, commit => $last_commit };
405 sub commit {
406 my (@commits) = @_;
407 check_upgrade_needed();
408 if ($_stdin || !@commits) {
409 print "Reading from stdin...\n";
410 @commits = ();
411 while (<STDIN>) {
412 if (/\b($sha1_short)\b/o) {
413 unshift @commits, $1;
417 my @revs;
418 foreach my $c (@commits) {
419 chomp(my @tmp = safe_qx('git-rev-parse',$c));
420 if (scalar @tmp == 1) {
421 push @revs, $tmp[0];
422 } elsif (scalar @tmp > 1) {
423 push @revs, reverse (safe_qx('git-rev-list',@tmp));
424 } else {
425 die "Failed to rev-parse $c\n";
428 chomp @revs;
429 $_use_lib ? commit_lib(@revs) : commit_cmd(@revs);
430 print "Done committing ",scalar @revs," revisions to SVN\n";
433 sub commit_cmd {
434 my (@revs) = @_;
436 chdir $SVN_WC or croak "Unable to chdir $SVN_WC: $!\n";
437 my $info = svn_info('.');
438 my $fetched = fetch();
439 if ($info->{Revision} != $fetched->{revision}) {
440 print STDERR "There are new revisions that were fetched ",
441 "and need to be merged (or acknowledged) ",
442 "before committing.\n";
443 exit 1;
445 $info = svn_info('.');
446 read_uuid($info);
447 my $svn_current_rev = $info->{'Last Changed Rev'};
448 foreach my $c (@revs) {
449 my $mods = svn_checkout_tree($svn_current_rev, $c);
450 if (scalar @$mods == 0) {
451 print "Skipping, no changes detected\n";
452 next;
454 $svn_current_rev = svn_commit_tree($svn_current_rev, $c);
458 sub commit_lib {
459 my (@revs) = @_;
460 my ($r_last, $cmt_last) = svn_grab_base_rev();
461 defined $r_last or die "Must have an existing revision to commit\n";
462 my $fetched = fetch_lib();
463 if ($r_last != $fetched->{revision}) {
464 print STDERR "There are new revisions that were fetched ",
465 "and need to be merged (or acknowledged) ",
466 "before committing.\n",
467 "last rev: $r_last\n",
468 " current: $fetched->{revision}\n";
469 exit 1;
471 read_uuid();
472 my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef, 0) : ();
473 my $commit_msg = "$GIT_SVN_DIR/.svn-commit.tmp.$$";
475 foreach my $c (@revs) {
476 # fork for each commit because there's a memory leak I
477 # can't track down... (it's probably in the SVN code)
478 defined(my $pid = open my $fh, '-|') or croak $!;
479 if (!$pid) {
480 if (defined $LC_ALL) {
481 $ENV{LC_ALL} = $LC_ALL;
482 } else {
483 delete $ENV{LC_ALL};
485 my $log_msg = get_commit_message($c, $commit_msg);
486 my $ed = SVN::Git::Editor->new(
487 { r => $r_last,
488 ra => $SVN,
489 c => $c,
490 svn_path => $SVN_PATH
492 $SVN->get_commit_editor(
493 $log_msg->{msg},
494 sub {
495 libsvn_commit_cb(
496 @_, $c,
497 $log_msg->{msg},
498 $r_last,
499 $cmt_last)
501 @lock)
503 my $mods = libsvn_checkout_tree($r_last, $c, $ed);
504 if (@$mods == 0) {
505 print "No changes\nr$r_last = $cmt_last\n";
506 $ed->abort_edit;
507 } else {
508 $ed->close_edit;
510 exit 0;
512 my ($r_new, $cmt_new, $no);
513 while (<$fh>) {
514 print $_;
515 chomp;
516 if (/^r(\d+) = ($sha1)$/o) {
517 ($r_new, $cmt_new) = ($1, $2);
518 } elsif ($_ eq 'No changes') {
519 $no = 1;
522 close $fh or croak $!;
523 if (! defined $r_new && ! defined $cmt_new) {
524 unless ($no) {
525 die "Failed to parse revision information\n";
527 } else {
528 ($r_last, $cmt_last) = ($r_new, $cmt_new);
531 unlink $commit_msg;
534 sub show_ignore {
535 $SVN_URL ||= file_to_s("$GIT_SVN_DIR/info/url");
536 $_use_lib ? show_ignore_lib() : show_ignore_cmd();
539 sub show_ignore_cmd {
540 require File::Find or die $!;
541 if (defined $_revision) {
542 die "-r/--revision option doesn't work unless the Perl SVN ",
543 "libraries are used\n";
545 chdir $SVN_WC or croak $!;
546 my %ign;
547 File::Find::find({wanted=>sub{if(lstat $_ && -d _ && -d "$_/.svn"){
548 s#^\./##;
549 @{$ign{$_}} = svn_propget_base('svn:ignore', $_);
550 }}, no_chdir=>1},'.');
552 print "\n# /\n";
553 foreach (@{$ign{'.'}}) { print '/',$_ if /\S/ }
554 delete $ign{'.'};
555 foreach my $i (sort keys %ign) {
556 print "\n# ",$i,"\n";
557 foreach (@{$ign{$i}}) { print '/',$i,'/',$_ if /\S/ }
561 sub show_ignore_lib {
562 my $repo;
563 ($repo, $SVN_PATH) = repo_path_split($SVN_URL);
564 $SVN ||= libsvn_connect($repo);
565 my $r = defined $_revision ? $_revision : $SVN->get_latest_revnum;
566 libsvn_traverse_ignore(\*STDOUT, $SVN_PATH, $r);
569 sub graft_branches {
570 my $gr_file = "$GIT_DIR/info/grafts";
571 my ($grafts, $comments) = read_grafts($gr_file);
572 my $gr_sha1;
574 if (%$grafts) {
575 # temporarily disable our grafts file to make this idempotent
576 chomp($gr_sha1 = safe_qx(qw/git-hash-object -w/,$gr_file));
577 rename $gr_file, "$gr_file~$gr_sha1" or croak $!;
580 my $l_map = read_url_paths();
581 my @re = map { qr/$_/is } @_opt_m if @_opt_m;
582 unless ($_no_default_regex) {
583 push @re, ( qr/\b(?:merge|merging|merged)\s+(\S.+)/is,
584 qr/\b(?:from|of)\s+(\S.+)/is );
586 foreach my $u (keys %$l_map) {
587 if (@re) {
588 foreach my $p (keys %{$l_map->{$u}}) {
589 graft_merge_msg($grafts,$l_map,$u,$p);
592 unless ($_no_graft_copy) {
593 if ($_use_lib) {
594 graft_file_copy_lib($grafts,$l_map,$u);
595 } else {
596 graft_file_copy_cmd($grafts,$l_map,$u);
601 write_grafts($grafts, $comments, $gr_file);
602 unlink "$gr_file~$gr_sha1" if $gr_sha1;
605 sub multi_init {
606 my $url = shift;
607 $_trunk ||= 'trunk';
608 $_trunk =~ s#/+$##;
609 $url =~ s#/+$## if $url;
610 if ($_trunk !~ m#^[a-z\+]+://#) {
611 $_trunk = '/' . $_trunk if ($_trunk !~ m#^/#);
612 unless ($url) {
613 print STDERR "E: '$_trunk' is not a complete URL ",
614 "and a separate URL is not specified\n";
615 exit 1;
617 $_trunk = $url . $_trunk;
619 if ($GIT_SVN eq 'git-svn') {
620 print "GIT_SVN_ID set to 'trunk' for $_trunk\n";
621 $GIT_SVN = $ENV{GIT_SVN_ID} = 'trunk';
623 init_vars();
624 init($_trunk);
625 complete_url_ls_init($url, $_branches, '--branches/-b', '');
626 complete_url_ls_init($url, $_tags, '--tags/-t', 'tags/');
629 sub multi_fetch {
630 # try to do trunk first, since branches/tags
631 # may be descended from it.
632 if (-d "$GIT_DIR/svn/trunk") {
633 print "Fetching trunk\n";
634 defined(my $pid = fork) or croak $!;
635 if (!$pid) {
636 $GIT_SVN = $ENV{GIT_SVN_ID} = 'trunk';
637 init_vars();
638 fetch(@_);
639 exit 0;
641 waitpid $pid, 0;
642 croak $? if $?;
644 rec_fetch('', "$GIT_DIR/svn", @_);
647 sub show_log {
648 my (@args) = @_;
649 my ($r_min, $r_max);
650 my $r_last = -1; # prevent dupes
651 rload_authors() if $_authors;
652 if (defined $TZ) {
653 $ENV{TZ} = $TZ;
654 } else {
655 delete $ENV{TZ};
657 if (defined $_revision) {
658 if ($_revision =~ /^(\d+):(\d+)$/) {
659 ($r_min, $r_max) = ($1, $2);
660 } elsif ($_revision =~ /^\d+$/) {
661 $r_min = $r_max = $_revision;
662 } else {
663 print STDERR "-r$_revision is not supported, use ",
664 "standard \'git log\' arguments instead\n";
665 exit 1;
669 my $pid = open(my $log,'-|');
670 defined $pid or croak $!;
671 if (!$pid) {
672 my @rl = (qw/git-log --abbrev-commit --pretty=raw
673 --default/, "remotes/$GIT_SVN");
674 push @rl, '--raw' if $_verbose;
675 exec(@rl, @args) or croak $!;
677 setup_pager();
678 my (@k, $c, $d);
679 while (<$log>) {
680 if (/^commit ($sha1_short)/o) {
681 my $cmt = $1;
682 if ($c && defined $c->{r} && $c->{r} != $r_last) {
683 $r_last = $c->{r};
684 process_commit($c, $r_min, $r_max, \@k) or
685 goto out;
687 $d = undef;
688 $c = { c => $cmt };
689 } elsif (/^author (.+) (\d+) ([\-\+]?\d+)$/) {
690 get_author_info($c, $1, $2, $3);
691 } elsif (/^(?:tree|parent|committer) /) {
692 # ignore
693 } elsif (/^:\d{6} \d{6} $sha1_short/o) {
694 push @{$c->{raw}}, $_;
695 } elsif (/^diff /) {
696 $d = 1;
697 push @{$c->{diff}}, $_;
698 } elsif ($d) {
699 push @{$c->{diff}}, $_;
700 } elsif (/^ (git-svn-id:.+)$/) {
701 my ($url, $rev, $uuid) = extract_metadata($1);
702 $c->{r} = $rev;
703 } elsif (s/^ //) {
704 push @{$c->{l}}, $_;
707 if ($c && defined $c->{r} && $c->{r} != $r_last) {
708 $r_last = $c->{r};
709 process_commit($c, $r_min, $r_max, \@k);
711 if (@k) {
712 my $swap = $r_max;
713 $r_max = $r_min;
714 $r_min = $swap;
715 process_commit($_, $r_min, $r_max) foreach reverse @k;
717 out:
718 close $log;
719 print '-' x72,"\n" unless $_incremental || $_oneline;
722 ########################### utility functions #########################
724 sub rec_fetch {
725 my ($pfx, $p, @args) = @_;
726 my @dir;
727 foreach (sort <$p/*>) {
728 if (-r "$_/info/url") {
729 $pfx .= '/' if $pfx && $pfx !~ m!/$!;
730 my $id = $pfx . basename $_;
731 next if $id eq 'trunk';
732 print "Fetching $id\n";
733 defined(my $pid = fork) or croak $!;
734 if (!$pid) {
735 $GIT_SVN = $ENV{GIT_SVN_ID} = $id;
736 init_vars();
737 fetch(@args);
738 exit 0;
740 waitpid $pid, 0;
741 croak $? if $?;
742 } elsif (-d $_) {
743 push @dir, $_;
746 foreach (@dir) {
747 my $x = $_;
748 $x =~ s!^\Q$GIT_DIR\E/svn/!!;
749 rec_fetch($x, $_);
753 sub complete_url_ls_init {
754 my ($url, $var, $switch, $pfx) = @_;
755 unless ($var) {
756 print STDERR "W: $switch not specified\n";
757 return;
759 $var =~ s#/+$##;
760 if ($var !~ m#^[a-z\+]+://#) {
761 $var = '/' . $var if ($var !~ m#^/#);
762 unless ($url) {
763 print STDERR "E: '$var' is not a complete URL ",
764 "and a separate URL is not specified\n";
765 exit 1;
767 $var = $url . $var;
769 chomp(my @ls = $_use_lib ? libsvn_ls_fullurl($var)
770 : safe_qx(qw/svn ls --non-interactive/, $var));
771 my $old = $GIT_SVN;
772 defined(my $pid = fork) or croak $!;
773 if (!$pid) {
774 foreach my $u (map { "$var/$_" } (grep m!/$!, @ls)) {
775 $u =~ s#/+$##;
776 if ($u !~ m!\Q$var\E/(.+)$!) {
777 print STDERR "W: Unrecognized URL: $u\n";
778 die "This should never happen\n";
780 my $id = $pfx.$1;
781 print "init $u => $id\n";
782 $GIT_SVN = $ENV{GIT_SVN_ID} = $id;
783 init_vars();
784 init($u);
786 exit 0;
788 waitpid $pid, 0;
789 croak $? if $?;
792 sub common_prefix {
793 my $paths = shift;
794 my %common;
795 foreach (@$paths) {
796 my @tmp = split m#/#, $_;
797 my $p = '';
798 while (my $x = shift @tmp) {
799 $p .= "/$x";
800 $common{$p} ||= 0;
801 $common{$p}++;
804 foreach (sort {length $b <=> length $a} keys %common) {
805 if ($common{$_} == @$paths) {
806 return $_;
809 return '';
812 # this isn't funky-filename safe, but good enough for now...
813 sub graft_file_copy_cmd {
814 my ($grafts, $l_map, $u) = @_;
815 my $paths = $l_map->{$u};
816 my $pfx = common_prefix([keys %$paths]);
818 my $pid = open my $fh, '-|';
819 defined $pid or croak $!;
820 unless ($pid) {
821 my @exec = qw/svn log -v/;
822 push @exec, "-r$_revision" if defined $_revision;
823 exec @exec, $u.$pfx or croak $!;
825 my ($r, $mp) = (undef, undef);
826 while (<$fh>) {
827 chomp;
828 if (/^\-{72}$/) {
829 $mp = $r = undef;
830 } elsif (/^r(\d+) \| /) {
831 $r = $1 unless defined $r;
832 } elsif (/^Changed paths:/) {
833 $mp = 1;
834 } elsif ($mp && m#^ [AR] /(\S.*?) \(from /(\S+?):(\d+)\)$#) {
835 my ($p1, $p0, $r0) = ($1, $2, $3);
836 my $c = find_graft_path_commit($paths, $p1, $r);
837 next unless $c;
838 find_graft_path_parents($grafts, $paths, $c, $p0, $r0);
843 sub graft_file_copy_lib {
844 my ($grafts, $l_map, $u) = @_;
845 my $tree_paths = $l_map->{$u};
846 my $pfx = common_prefix([keys %$tree_paths]);
847 my ($repo, $path) = repo_path_split($u.$pfx);
848 $SVN_LOG ||= libsvn_connect($repo);
849 $SVN ||= libsvn_connect($repo);
851 my ($base, $head) = libsvn_parse_revision();
852 my $inc = 1000;
853 my ($min, $max) = ($base, $head < $base+$inc ? $head : $base+$inc);
854 while (1) {
855 my $pool = SVN::Pool->new;
856 $SVN_LOG->get_log( "/$path", $min, $max, 0, 1, 1,
857 sub {
858 libsvn_graft_file_copies($grafts, $tree_paths,
859 $path, @_);
860 }, $pool);
861 $pool->clear;
862 last if ($max >= $head);
863 $min = $max + 1;
864 $max += $inc;
865 $max = $head if ($max > $head);
869 sub process_merge_msg_matches {
870 my ($grafts, $l_map, $u, $p, $c, @matches) = @_;
871 my (@strong, @weak);
872 foreach (@matches) {
873 # merging with ourselves is not interesting
874 next if $_ eq $p;
875 if ($l_map->{$u}->{$_}) {
876 push @strong, $_;
877 } else {
878 push @weak, $_;
881 foreach my $w (@weak) {
882 last if @strong;
883 # no exact match, use branch name as regexp.
884 my $re = qr/\Q$w\E/i;
885 foreach (keys %{$l_map->{$u}}) {
886 if (/$re/) {
887 push @strong, $_;
888 last;
891 last if @strong;
892 $w = basename($w);
893 $re = qr/\Q$w\E/i;
894 foreach (keys %{$l_map->{$u}}) {
895 if (/$re/) {
896 push @strong, $_;
897 last;
901 my ($rev) = ($c->{m} =~ /^git-svn-id:\s(?:\S+?)\@(\d+)
902 \s(?:[a-f\d\-]+)$/xsm);
903 unless (defined $rev) {
904 ($rev) = ($c->{m} =~/^git-svn-id:\s(\d+)
905 \@(?:[a-f\d\-]+)/xsm);
906 return unless defined $rev;
908 foreach my $m (@strong) {
909 my ($r0, $s0) = find_rev_before($rev, $m);
910 $grafts->{$c->{c}}->{$s0} = 1 if defined $s0;
914 sub graft_merge_msg {
915 my ($grafts, $l_map, $u, $p, @re) = @_;
917 my $x = $l_map->{$u}->{$p};
918 my $rl = rev_list_raw($x);
919 while (my $c = next_rev_list_entry($rl)) {
920 foreach my $re (@re) {
921 my (@br) = ($c->{m} =~ /$re/g);
922 next unless @br;
923 process_merge_msg_matches($grafts,$l_map,$u,$p,$c,@br);
928 sub read_uuid {
929 return if $SVN_UUID;
930 if ($_use_lib) {
931 my $pool = SVN::Pool->new;
932 $SVN_UUID = $SVN->get_uuid($pool);
933 $pool->clear;
934 } else {
935 my $info = shift || svn_info('.');
936 $SVN_UUID = $info->{'Repository UUID'} or
937 croak "Repository UUID unreadable\n";
939 s_to_file($SVN_UUID,"$GIT_SVN_DIR/info/uuid");
942 sub quiet_run {
943 my $pid = fork;
944 defined $pid or croak $!;
945 if (!$pid) {
946 open my $null, '>', '/dev/null' or croak $!;
947 open STDERR, '>&', $null or croak $!;
948 open STDOUT, '>&', $null or croak $!;
949 exec @_ or croak $!;
951 waitpid $pid, 0;
952 return $?;
955 sub repo_path_split {
956 my $full_url = shift;
957 $full_url =~ s#/+$##;
959 foreach (@repo_path_split_cache) {
960 if ($full_url =~ s#$_##) {
961 my $u = $1;
962 $full_url =~ s#^/+##;
963 return ($u, $full_url);
967 my ($url, $path) = ($full_url =~ m!^([a-z\+]+://[^/]*)(.*)$!i);
968 $path =~ s#^/+##;
969 my @paths = split(m#/+#, $path);
971 if ($_use_lib) {
972 while (1) {
973 $SVN = libsvn_connect($url);
974 last if (defined $SVN &&
975 defined eval { $SVN->get_latest_revnum });
976 my $n = shift @paths || last;
977 $url .= "/$n";
979 } else {
980 while (quiet_run(qw/svn ls --non-interactive/, $url)) {
981 my $n = shift @paths || last;
982 $url .= "/$n";
985 push @repo_path_split_cache, qr/^(\Q$url\E)/;
986 $path = join('/',@paths);
987 return ($url, $path);
990 sub setup_git_svn {
991 defined $SVN_URL or croak "SVN repository location required\n";
992 unless (-d $GIT_DIR) {
993 croak "GIT_DIR=$GIT_DIR does not exist!\n";
995 mkpath([$GIT_SVN_DIR]);
996 mkpath(["$GIT_SVN_DIR/info"]);
997 mkpath([$REV_DIR]);
998 s_to_file($SVN_URL,"$GIT_SVN_DIR/info/url");
1000 open my $fd, '>>', "$GIT_SVN_DIR/info/exclude" or croak $!;
1001 print $fd '.svn',"\n";
1002 close $fd or croak $!;
1003 my ($url, $path) = repo_path_split($SVN_URL);
1004 s_to_file($url, "$GIT_SVN_DIR/info/repo_url");
1005 s_to_file($path, "$GIT_SVN_DIR/info/repo_path");
1008 sub assert_svn_wc_clean {
1009 return if $_use_lib;
1010 my ($svn_rev) = @_;
1011 croak "$svn_rev is not an integer!\n" unless ($svn_rev =~ /^\d+$/);
1012 my $lcr = svn_info('.')->{'Last Changed Rev'};
1013 if ($svn_rev != $lcr) {
1014 print STDERR "Checking for copy-tree ... ";
1015 my @diff = grep(/^Index: /,(safe_qx(qw(svn diff),
1016 "-r$lcr:$svn_rev")));
1017 if (@diff) {
1018 croak "Nope! Expected r$svn_rev, got r$lcr\n";
1019 } else {
1020 print STDERR "OK!\n";
1023 my @status = grep(!/^Performing status on external/,(`svn status`));
1024 @status = grep(!/^\s*$/,@status);
1025 if (scalar @status) {
1026 print STDERR "Tree ($SVN_WC) is not clean:\n";
1027 print STDERR $_ foreach @status;
1028 croak;
1032 sub get_tree_from_treeish {
1033 my ($treeish) = @_;
1034 croak "Not a sha1: $treeish\n" unless $treeish =~ /^$sha1$/o;
1035 chomp(my $type = `git-cat-file -t $treeish`);
1036 my $expected;
1037 while ($type eq 'tag') {
1038 chomp(($treeish, $type) = `git-cat-file tag $treeish`);
1040 if ($type eq 'commit') {
1041 $expected = (grep /^tree /,`git-cat-file commit $treeish`)[0];
1042 ($expected) = ($expected =~ /^tree ($sha1)$/);
1043 die "Unable to get tree from $treeish\n" unless $expected;
1044 } elsif ($type eq 'tree') {
1045 $expected = $treeish;
1046 } else {
1047 die "$treeish is a $type, expected tree, tag or commit\n";
1049 return $expected;
1052 sub assert_tree {
1053 return if $_use_lib;
1054 my ($treeish) = @_;
1055 my $expected = get_tree_from_treeish($treeish);
1057 my $tmpindex = $GIT_SVN_INDEX.'.assert-tmp';
1058 if (-e $tmpindex) {
1059 unlink $tmpindex or croak $!;
1061 my $old_index = set_index($tmpindex);
1062 index_changes(1);
1063 chomp(my $tree = `git-write-tree`);
1064 restore_index($old_index);
1065 if ($tree ne $expected) {
1066 croak "Tree mismatch, Got: $tree, Expected: $expected\n";
1068 unlink $tmpindex;
1071 sub parse_diff_tree {
1072 my $diff_fh = shift;
1073 local $/ = "\0";
1074 my $state = 'meta';
1075 my @mods;
1076 while (<$diff_fh>) {
1077 chomp $_; # this gets rid of the trailing "\0"
1078 if ($state eq 'meta' && /^:(\d{6})\s(\d{6})\s
1079 $sha1\s($sha1)\s([MTCRAD])\d*$/xo) {
1080 push @mods, { mode_a => $1, mode_b => $2,
1081 sha1_b => $3, chg => $4 };
1082 if ($4 =~ /^(?:C|R)$/) {
1083 $state = 'file_a';
1084 } else {
1085 $state = 'file_b';
1087 } elsif ($state eq 'file_a') {
1088 my $x = $mods[$#mods] or croak "Empty array\n";
1089 if ($x->{chg} !~ /^(?:C|R)$/) {
1090 croak "Error parsing $_, $x->{chg}\n";
1092 $x->{file_a} = $_;
1093 $state = 'file_b';
1094 } elsif ($state eq 'file_b') {
1095 my $x = $mods[$#mods] or croak "Empty array\n";
1096 if (exists $x->{file_a} && $x->{chg} !~ /^(?:C|R)$/) {
1097 croak "Error parsing $_, $x->{chg}\n";
1099 if (!exists $x->{file_a} && $x->{chg} =~ /^(?:C|R)$/) {
1100 croak "Error parsing $_, $x->{chg}\n";
1102 $x->{file_b} = $_;
1103 $state = 'meta';
1104 } else {
1105 croak "Error parsing $_\n";
1108 close $diff_fh or croak $!;
1110 return \@mods;
1113 sub svn_check_prop_executable {
1114 my $m = shift;
1115 return if -l $m->{file_b};
1116 if ($m->{mode_b} =~ /755$/) {
1117 chmod((0755 &~ umask),$m->{file_b}) or croak $!;
1118 if ($m->{mode_a} !~ /755$/) {
1119 sys(qw(svn propset svn:executable 1), $m->{file_b});
1121 -x $m->{file_b} or croak "$m->{file_b} is not executable!\n";
1122 } elsif ($m->{mode_b} !~ /755$/ && $m->{mode_a} =~ /755$/) {
1123 sys(qw(svn propdel svn:executable), $m->{file_b});
1124 chmod((0644 &~ umask),$m->{file_b}) or croak $!;
1125 -x $m->{file_b} and croak "$m->{file_b} is executable!\n";
1129 sub svn_ensure_parent_path {
1130 my $dir_b = dirname(shift);
1131 svn_ensure_parent_path($dir_b) if ($dir_b ne File::Spec->curdir);
1132 mkpath([$dir_b]) unless (-d $dir_b);
1133 sys(qw(svn add -N), $dir_b) unless (-d "$dir_b/.svn");
1136 sub precommit_check {
1137 my $mods = shift;
1138 my (%rm_file, %rmdir_check, %added_check);
1140 my %o = ( D => 0, R => 1, C => 2, A => 3, M => 3, T => 3 );
1141 foreach my $m (sort { $o{$a->{chg}} <=> $o{$b->{chg}} } @$mods) {
1142 if ($m->{chg} eq 'R') {
1143 if (-d $m->{file_b}) {
1144 err_dir_to_file("$m->{file_a} => $m->{file_b}");
1146 # dir/$file => dir/file/$file
1147 my $dirname = dirname($m->{file_b});
1148 while ($dirname ne File::Spec->curdir) {
1149 if ($dirname ne $m->{file_a}) {
1150 $dirname = dirname($dirname);
1151 next;
1153 err_file_to_dir("$m->{file_a} => $m->{file_b}");
1155 # baz/zzz => baz (baz is a file)
1156 $dirname = dirname($m->{file_a});
1157 while ($dirname ne File::Spec->curdir) {
1158 if ($dirname ne $m->{file_b}) {
1159 $dirname = dirname($dirname);
1160 next;
1162 err_dir_to_file("$m->{file_a} => $m->{file_b}");
1165 if ($m->{chg} =~ /^(D|R)$/) {
1166 my $t = $1 eq 'D' ? 'file_b' : 'file_a';
1167 $rm_file{ $m->{$t} } = 1;
1168 my $dirname = dirname( $m->{$t} );
1169 my $basename = basename( $m->{$t} );
1170 $rmdir_check{$dirname}->{$basename} = 1;
1171 } elsif ($m->{chg} =~ /^(?:A|C)$/) {
1172 if (-d $m->{file_b}) {
1173 err_dir_to_file($m->{file_b});
1175 my $dirname = dirname( $m->{file_b} );
1176 my $basename = basename( $m->{file_b} );
1177 $added_check{$dirname}->{$basename} = 1;
1178 while ($dirname ne File::Spec->curdir) {
1179 if ($rm_file{$dirname}) {
1180 err_file_to_dir($m->{file_b});
1182 $dirname = dirname $dirname;
1186 return (\%rmdir_check, \%added_check);
1188 sub err_dir_to_file {
1189 my $file = shift;
1190 print STDERR "Node change from directory to file ",
1191 "is not supported by Subversion: ",$file,"\n";
1192 exit 1;
1194 sub err_file_to_dir {
1195 my $file = shift;
1196 print STDERR "Node change from file to directory ",
1197 "is not supported by Subversion: ",$file,"\n";
1198 exit 1;
1203 sub get_diff {
1204 my ($svn_rev, $treeish) = @_;
1205 my $from = file_to_s("$REV_DIR/$svn_rev");
1206 assert_tree($from);
1207 print "diff-tree $from $treeish\n";
1208 my $pid = open my $diff_fh, '-|';
1209 defined $pid or croak $!;
1210 if ($pid == 0) {
1211 my @diff_tree = qw(git-diff-tree -z -r);
1212 if ($_cp_similarity) {
1213 push @diff_tree, "-C$_cp_similarity";
1214 } else {
1215 push @diff_tree, '-C';
1217 push @diff_tree, '--find-copies-harder' if $_find_copies_harder;
1218 push @diff_tree, "-l$_l" if defined $_l;
1219 exec(@diff_tree, $from, $treeish) or croak $!;
1221 return parse_diff_tree($diff_fh);
1224 sub svn_checkout_tree {
1225 my ($svn_rev, $treeish) = @_;
1226 my $mods = get_diff($svn_rev, $treeish);
1227 return $mods unless (scalar @$mods);
1228 my ($rm, $add) = precommit_check($mods);
1230 my %o = ( D => 1, R => 0, C => -1, A => 3, M => 3, T => 3 );
1231 foreach my $m (sort { $o{$a->{chg}} <=> $o{$b->{chg}} } @$mods) {
1232 if ($m->{chg} eq 'C') {
1233 svn_ensure_parent_path( $m->{file_b} );
1234 sys(qw(svn cp), $m->{file_a}, $m->{file_b});
1235 apply_mod_line_blob($m);
1236 svn_check_prop_executable($m);
1237 } elsif ($m->{chg} eq 'D') {
1238 sys(qw(svn rm --force), $m->{file_b});
1239 } elsif ($m->{chg} eq 'R') {
1240 svn_ensure_parent_path( $m->{file_b} );
1241 sys(qw(svn mv --force), $m->{file_a}, $m->{file_b});
1242 apply_mod_line_blob($m);
1243 svn_check_prop_executable($m);
1244 } elsif ($m->{chg} eq 'M') {
1245 apply_mod_line_blob($m);
1246 svn_check_prop_executable($m);
1247 } elsif ($m->{chg} eq 'T') {
1248 sys(qw(svn rm --force),$m->{file_b});
1249 apply_mod_line_blob($m);
1250 sys(qw(svn add --force), $m->{file_b});
1251 svn_check_prop_executable($m);
1252 } elsif ($m->{chg} eq 'A') {
1253 svn_ensure_parent_path( $m->{file_b} );
1254 apply_mod_line_blob($m);
1255 sys(qw(svn add --force), $m->{file_b});
1256 svn_check_prop_executable($m);
1257 } else {
1258 croak "Invalid chg: $m->{chg}\n";
1262 assert_tree($treeish);
1263 if ($_rmdir) { # remove empty directories
1264 handle_rmdir($rm, $add);
1266 assert_tree($treeish);
1267 return $mods;
1270 sub libsvn_checkout_tree {
1271 my ($svn_rev, $treeish, $ed) = @_;
1272 my $mods = get_diff($svn_rev, $treeish);
1273 return $mods unless (scalar @$mods);
1274 my %o = ( D => 1, R => 0, C => -1, A => 3, M => 3, T => 3 );
1275 foreach my $m (sort { $o{$a->{chg}} <=> $o{$b->{chg}} } @$mods) {
1276 my $f = $m->{chg};
1277 if (defined $o{$f}) {
1278 $ed->$f($m);
1279 } else {
1280 croak "Invalid change type: $f\n";
1283 $ed->rmdirs if $_rmdir;
1284 return $mods;
1287 # svn ls doesn't work with respect to the current working tree, but what's
1288 # in the repository. There's not even an option for it... *sigh*
1289 # (added files don't show up and removed files remain in the ls listing)
1290 sub svn_ls_current {
1291 my ($dir, $rm, $add) = @_;
1292 chomp(my @ls = safe_qx('svn','ls',$dir));
1293 my @ret = ();
1294 foreach (@ls) {
1295 s#/$##; # trailing slashes are evil
1296 push @ret, $_ unless $rm->{$dir}->{$_};
1298 if (exists $add->{$dir}) {
1299 push @ret, keys %{$add->{$dir}};
1301 return \@ret;
1304 sub handle_rmdir {
1305 my ($rm, $add) = @_;
1307 foreach my $dir (sort {length $b <=> length $a} keys %$rm) {
1308 my $ls = svn_ls_current($dir, $rm, $add);
1309 next if (scalar @$ls);
1310 sys(qw(svn rm --force),$dir);
1312 my $dn = dirname $dir;
1313 $rm->{ $dn }->{ basename $dir } = 1;
1314 $ls = svn_ls_current($dn, $rm, $add);
1315 while (scalar @$ls == 0 && $dn ne File::Spec->curdir) {
1316 sys(qw(svn rm --force),$dn);
1317 $dir = basename $dn;
1318 $dn = dirname $dn;
1319 $rm->{ $dn }->{ $dir } = 1;
1320 $ls = svn_ls_current($dn, $rm, $add);
1325 sub get_commit_message {
1326 my ($commit, $commit_msg) = (@_);
1327 my %log_msg = ( msg => '' );
1328 open my $msg, '>', $commit_msg or croak $!;
1330 print "commit: $commit\n";
1331 chomp(my $type = `git-cat-file -t $commit`);
1332 if ($type eq 'commit') {
1333 my $pid = open my $msg_fh, '-|';
1334 defined $pid or croak $!;
1336 if ($pid == 0) {
1337 exec(qw(git-cat-file commit), $commit) or croak $!;
1339 my $in_msg = 0;
1340 while (<$msg_fh>) {
1341 if (!$in_msg) {
1342 $in_msg = 1 if (/^\s*$/);
1343 } elsif (/^git-svn-id: /) {
1344 # skip this, we regenerate the correct one
1345 # on re-fetch anyways
1346 } else {
1347 print $msg $_ or croak $!;
1350 close $msg_fh or croak $!;
1352 close $msg or croak $!;
1354 if ($_edit || ($type eq 'tree')) {
1355 my $editor = $ENV{VISUAL} || $ENV{EDITOR} || 'vi';
1356 system($editor, $commit_msg);
1359 # file_to_s removes all trailing newlines, so just use chomp() here:
1360 open $msg, '<', $commit_msg or croak $!;
1361 { local $/; chomp($log_msg{msg} = <$msg>); }
1362 close $msg or croak $!;
1364 return \%log_msg;
1367 sub svn_commit_tree {
1368 my ($svn_rev, $commit) = @_;
1369 my $commit_msg = "$GIT_SVN_DIR/.svn-commit.tmp.$$";
1370 my $log_msg = get_commit_message($commit, $commit_msg);
1371 my ($oneline) = ($log_msg->{msg} =~ /([^\n\r]+)/);
1372 print "Committing $commit: $oneline\n";
1374 if (defined $LC_ALL) {
1375 $ENV{LC_ALL} = $LC_ALL;
1376 } else {
1377 delete $ENV{LC_ALL};
1379 my @ci_output = safe_qx(qw(svn commit -F),$commit_msg);
1380 $ENV{LC_ALL} = 'C';
1381 unlink $commit_msg;
1382 my ($committed) = ($ci_output[$#ci_output] =~ /(\d+)/);
1383 if (!defined $committed) {
1384 my $out = join("\n",@ci_output);
1385 print STDERR "W: Trouble parsing \`svn commit' output:\n\n",
1386 $out, "\n\nAssuming English locale...";
1387 ($committed) = ($out =~ /^Committed revision \d+\./sm);
1388 defined $committed or die " FAILED!\n",
1389 "Commit output failed to parse committed revision!\n",
1390 print STDERR " OK\n";
1393 my @svn_up = qw(svn up);
1394 push @svn_up, '--ignore-externals' unless $_no_ignore_ext;
1395 if ($committed == ($svn_rev + 1)) {
1396 push @svn_up, "-r$committed";
1397 sys(@svn_up);
1398 my $info = svn_info('.');
1399 my $date = $info->{'Last Changed Date'} or die "Missing date\n";
1400 if ($info->{'Last Changed Rev'} != $committed) {
1401 croak "$info->{'Last Changed Rev'} != $committed\n"
1403 my ($Y,$m,$d,$H,$M,$S,$tz) = ($date =~
1404 /(\d{4})\-(\d\d)\-(\d\d)\s
1405 (\d\d)\:(\d\d)\:(\d\d)\s([\-\+]\d+)/x)
1406 or croak "Failed to parse date: $date\n";
1407 $log_msg->{date} = "$tz $Y-$m-$d $H:$M:$S";
1408 $log_msg->{author} = $info->{'Last Changed Author'};
1409 $log_msg->{revision} = $committed;
1410 $log_msg->{msg} .= "\n";
1411 my $parent = file_to_s("$REV_DIR/$svn_rev");
1412 git_commit($log_msg, $parent, $commit);
1413 return $committed;
1415 # resync immediately
1416 push @svn_up, "-r$svn_rev";
1417 sys(@svn_up);
1418 return fetch("$committed=$commit")->{revision};
1421 sub rev_list_raw {
1422 my (@args) = @_;
1423 my $pid = open my $fh, '-|';
1424 defined $pid or croak $!;
1425 if (!$pid) {
1426 exec(qw/git-rev-list --pretty=raw/, @args) or croak $!;
1428 return { fh => $fh, t => { } };
1431 sub next_rev_list_entry {
1432 my $rl = shift;
1433 my $fh = $rl->{fh};
1434 my $x = $rl->{t};
1435 while (<$fh>) {
1436 if (/^commit ($sha1)$/o) {
1437 if ($x->{c}) {
1438 $rl->{t} = { c => $1 };
1439 return $x;
1440 } else {
1441 $x->{c} = $1;
1443 } elsif (/^parent ($sha1)$/o) {
1444 $x->{p}->{$1} = 1;
1445 } elsif (s/^ //) {
1446 $x->{m} ||= '';
1447 $x->{m} .= $_;
1450 return ($x != $rl->{t}) ? $x : undef;
1453 # read the entire log into a temporary file (which is removed ASAP)
1454 # and store the file handle + parser state
1455 sub svn_log_raw {
1456 my (@log_args) = @_;
1457 my $log_fh = IO::File->new_tmpfile or croak $!;
1458 my $pid = fork;
1459 defined $pid or croak $!;
1460 if (!$pid) {
1461 open STDOUT, '>&', $log_fh or croak $!;
1462 exec (qw(svn log), @log_args) or croak $!
1464 waitpid $pid, 0;
1465 croak $? if $?;
1466 seek $log_fh, 0, 0 or croak $!;
1467 return { state => 'sep', fh => $log_fh };
1470 sub next_log_entry {
1471 my $log = shift; # retval of svn_log_raw()
1472 my $ret = undef;
1473 my $fh = $log->{fh};
1475 while (<$fh>) {
1476 chomp;
1477 if (/^\-{72}$/) {
1478 if ($log->{state} eq 'msg') {
1479 if ($ret->{lines}) {
1480 $ret->{msg} .= $_."\n";
1481 unless(--$ret->{lines}) {
1482 $log->{state} = 'sep';
1484 } else {
1485 croak "Log parse error at: $_\n",
1486 $ret->{revision},
1487 "\n";
1489 next;
1491 if ($log->{state} ne 'sep') {
1492 croak "Log parse error at: $_\n",
1493 "state: $log->{state}\n",
1494 $ret->{revision},
1495 "\n";
1497 $log->{state} = 'rev';
1499 # if we have an empty log message, put something there:
1500 if ($ret) {
1501 $ret->{msg} ||= "\n";
1502 delete $ret->{lines};
1503 return $ret;
1505 next;
1507 if ($log->{state} eq 'rev' && s/^r(\d+)\s*\|\s*//) {
1508 my $rev = $1;
1509 my ($author, $date, $lines) = split(/\s*\|\s*/, $_, 3);
1510 ($lines) = ($lines =~ /(\d+)/);
1511 my ($Y,$m,$d,$H,$M,$S,$tz) = ($date =~
1512 /(\d{4})\-(\d\d)\-(\d\d)\s
1513 (\d\d)\:(\d\d)\:(\d\d)\s([\-\+]\d+)/x)
1514 or croak "Failed to parse date: $date\n";
1515 $ret = { revision => $rev,
1516 date => "$tz $Y-$m-$d $H:$M:$S",
1517 author => $author,
1518 lines => $lines,
1519 msg => '' };
1520 if (defined $_authors && ! defined $users{$author}) {
1521 die "Author: $author not defined in ",
1522 "$_authors file\n";
1524 $log->{state} = 'msg_start';
1525 next;
1527 # skip the first blank line of the message:
1528 if ($log->{state} eq 'msg_start' && /^$/) {
1529 $log->{state} = 'msg';
1530 } elsif ($log->{state} eq 'msg') {
1531 if ($ret->{lines}) {
1532 $ret->{msg} .= $_."\n";
1533 unless (--$ret->{lines}) {
1534 $log->{state} = 'sep';
1536 } else {
1537 croak "Log parse error at: $_\n",
1538 $ret->{revision},"\n";
1542 return $ret;
1545 sub svn_info {
1546 my $url = shift || $SVN_URL;
1548 my $pid = open my $info_fh, '-|';
1549 defined $pid or croak $!;
1551 if ($pid == 0) {
1552 exec(qw(svn info),$url) or croak $!;
1555 my $ret = {};
1556 # only single-lines seem to exist in svn info output
1557 while (<$info_fh>) {
1558 chomp $_;
1559 if (m#^([^:]+)\s*:\s*(\S.*)$#) {
1560 $ret->{$1} = $2;
1561 push @{$ret->{-order}}, $1;
1564 close $info_fh or croak $!;
1565 return $ret;
1568 sub sys { system(@_) == 0 or croak $? }
1570 sub eol_cp {
1571 my ($from, $to) = @_;
1572 my $es = svn_propget_base('svn:eol-style', $to);
1573 open my $rfd, '<', $from or croak $!;
1574 binmode $rfd or croak $!;
1575 open my $wfd, '>', $to or croak $!;
1576 binmode $wfd or croak $!;
1577 eol_cp_fd($rfd, $wfd, $es);
1578 close $rfd or croak $!;
1579 close $wfd or croak $!;
1582 sub eol_cp_fd {
1583 my ($rfd, $wfd, $es) = @_;
1584 my $eol = defined $es ? $EOL{$es} : undef;
1585 my $buf;
1586 use bytes;
1587 while (1) {
1588 my ($r, $w, $t);
1589 defined($r = sysread($rfd, $buf, 4096)) or croak $!;
1590 return unless $r;
1591 if ($eol) {
1592 if ($buf =~ /\015$/) {
1593 my $c;
1594 defined($r = sysread($rfd,$c,1)) or croak $!;
1595 $buf .= $c if $r > 0;
1597 $buf =~ s/(?:\015\012|\015|\012)/$eol/gs;
1598 $r = length($buf);
1600 for ($w = 0; $w < $r; $w += $t) {
1601 $t = syswrite($wfd, $buf, $r - $w, $w) or croak $!;
1604 no bytes;
1607 sub do_update_index {
1608 my ($z_cmd, $cmd, $no_text_base) = @_;
1610 my $z = open my $p, '-|';
1611 defined $z or croak $!;
1612 unless ($z) { exec @$z_cmd or croak $! }
1614 my $pid = open my $ui, '|-';
1615 defined $pid or croak $!;
1616 unless ($pid) {
1617 exec('git-update-index',"--$cmd",'-z','--stdin') or croak $!;
1619 local $/ = "\0";
1620 while (my $x = <$p>) {
1621 chomp $x;
1622 if (!$no_text_base && lstat $x && ! -l _ &&
1623 svn_propget_base('svn:keywords', $x)) {
1624 my $mode = -x _ ? 0755 : 0644;
1625 my ($v,$d,$f) = File::Spec->splitpath($x);
1626 my $tb = File::Spec->catfile($d, '.svn', 'tmp',
1627 'text-base',"$f.svn-base");
1628 $tb =~ s#^/##;
1629 unless (-f $tb) {
1630 $tb = File::Spec->catfile($d, '.svn',
1631 'text-base',"$f.svn-base");
1632 $tb =~ s#^/##;
1634 unlink $x or croak $!;
1635 eol_cp($tb, $x);
1636 chmod(($mode &~ umask), $x) or croak $!;
1638 print $ui $x,"\0";
1640 close $ui or croak $!;
1643 sub index_changes {
1644 return if $_use_lib;
1645 my $no_text_base = shift;
1646 do_update_index([qw/git-diff-files --name-only -z/],
1647 'remove',
1648 $no_text_base);
1649 do_update_index([qw/git-ls-files -z --others/,
1650 "--exclude-from=$GIT_SVN_DIR/info/exclude"],
1651 'add',
1652 $no_text_base);
1655 sub s_to_file {
1656 my ($str, $file, $mode) = @_;
1657 open my $fd,'>',$file or croak $!;
1658 print $fd $str,"\n" or croak $!;
1659 close $fd or croak $!;
1660 chmod ($mode &~ umask, $file) if (defined $mode);
1663 sub file_to_s {
1664 my $file = shift;
1665 open my $fd,'<',$file or croak "$!: file: $file\n";
1666 local $/;
1667 my $ret = <$fd>;
1668 close $fd or croak $!;
1669 $ret =~ s/\s*$//s;
1670 return $ret;
1673 sub assert_revision_unknown {
1674 my $revno = shift;
1675 if (-f "$REV_DIR/$revno") {
1676 croak "$REV_DIR/$revno already exists! ",
1677 "Why are we refetching it?";
1681 sub trees_eq {
1682 my ($x, $y) = @_;
1683 my @x = safe_qx('git-cat-file','commit',$x);
1684 my @y = safe_qx('git-cat-file','commit',$y);
1685 if (($y[0] ne $x[0]) || $x[0] !~ /^tree $sha1\n$/
1686 || $y[0] !~ /^tree $sha1\n$/) {
1687 print STDERR "Trees not equal: $y[0] != $x[0]\n";
1688 return 0
1690 return 1;
1693 sub assert_revision_eq_or_unknown {
1694 my ($revno, $commit) = @_;
1695 if (-f "$REV_DIR/$revno") {
1696 my $current = file_to_s("$REV_DIR/$revno");
1697 if (($commit ne $current) && !trees_eq($commit, $current)) {
1698 croak "$REV_DIR/$revno already exists!\n",
1699 "current: $current\nexpected: $commit\n";
1701 return;
1705 sub git_commit {
1706 my ($log_msg, @parents) = @_;
1707 assert_revision_unknown($log_msg->{revision});
1708 map_tree_joins() if (@_branch_from && !%tree_map);
1710 my (@tmp_parents, @exec_parents, %seen_parent);
1711 if (my $lparents = $log_msg->{parents}) {
1712 @tmp_parents = @$lparents
1714 # commit parents can be conditionally bound to a particular
1715 # svn revision via: "svn_revno=commit_sha1", filter them out here:
1716 foreach my $p (@parents) {
1717 next unless defined $p;
1718 if ($p =~ /^(\d+)=($sha1_short)$/o) {
1719 if ($1 == $log_msg->{revision}) {
1720 push @tmp_parents, $2;
1722 } else {
1723 push @tmp_parents, $p if $p =~ /$sha1_short/o;
1726 my $tree = $log_msg->{tree};
1727 if (!defined $tree) {
1728 my $index = set_index($GIT_SVN_INDEX);
1729 index_changes();
1730 chomp($tree = `git-write-tree`);
1731 croak $? if $?;
1732 restore_index($index);
1734 if (exists $tree_map{$tree}) {
1735 push @tmp_parents, @{$tree_map{$tree}};
1737 foreach (@tmp_parents) {
1738 next if $seen_parent{$_};
1739 $seen_parent{$_} = 1;
1740 push @exec_parents, $_;
1741 # MAXPARENT is defined to 16 in commit-tree.c:
1742 last if @exec_parents > 16;
1745 defined(my $pid = open my $out_fh, '-|') or croak $!;
1746 if ($pid == 0) {
1747 my $msg_fh = IO::File->new_tmpfile or croak $!;
1748 print $msg_fh $log_msg->{msg}, "\ngit-svn-id: ",
1749 "$SVN_URL\@$log_msg->{revision}",
1750 " $SVN_UUID\n" or croak $!;
1751 $msg_fh->flush == 0 or croak $!;
1752 seek $msg_fh, 0, 0 or croak $!;
1753 set_commit_env($log_msg);
1754 my @exec = ('git-commit-tree',$tree);
1755 push @exec, '-p', $_ foreach @exec_parents;
1756 open STDIN, '<&', $msg_fh or croak $!;
1757 exec @exec or croak $!;
1759 chomp(my $commit = do { local $/; <$out_fh> });
1760 close $out_fh or croak $?;
1761 if ($commit !~ /^$sha1$/o) {
1762 croak "Failed to commit, invalid sha1: $commit\n";
1764 my @update_ref = ('git-update-ref',"refs/remotes/$GIT_SVN",$commit);
1765 if (my $primary_parent = shift @exec_parents) {
1766 $pid = fork;
1767 defined $pid or croak $!;
1768 if (!$pid) {
1769 close STDERR;
1770 close STDOUT;
1771 exec 'git-rev-parse','--verify',
1772 "refs/remotes/$GIT_SVN^0" or croak $!;
1774 waitpid $pid, 0;
1775 push @update_ref, $primary_parent unless $?;
1777 sys(@update_ref);
1778 sys('git-update-ref',"svn/$GIT_SVN/revs/$log_msg->{revision}",$commit);
1779 # this output is read via pipe, do not change:
1780 print "r$log_msg->{revision} = $commit\n";
1781 if ($_repack && (--$_repack_nr == 0)) {
1782 $_repack_nr = $_repack;
1783 sys("git repack $_repack_flags");
1785 return $commit;
1788 sub set_commit_env {
1789 my ($log_msg) = @_;
1790 my $author = $log_msg->{author};
1791 if (!defined $author || length $author == 0) {
1792 $author = '(no author)';
1794 my ($name,$email) = defined $users{$author} ? @{$users{$author}}
1795 : ($author,"$author\@$SVN_UUID");
1796 $ENV{GIT_AUTHOR_NAME} = $ENV{GIT_COMMITTER_NAME} = $name;
1797 $ENV{GIT_AUTHOR_EMAIL} = $ENV{GIT_COMMITTER_EMAIL} = $email;
1798 $ENV{GIT_AUTHOR_DATE} = $ENV{GIT_COMMITTER_DATE} = $log_msg->{date};
1801 sub apply_mod_line_blob {
1802 my $m = shift;
1803 if ($m->{mode_b} =~ /^120/) {
1804 blob_to_symlink($m->{sha1_b}, $m->{file_b});
1805 } else {
1806 blob_to_file($m->{sha1_b}, $m->{file_b});
1810 sub blob_to_symlink {
1811 my ($blob, $link) = @_;
1812 defined $link or croak "\$link not defined!\n";
1813 croak "Not a sha1: $blob\n" unless $blob =~ /^$sha1$/o;
1814 if (-l $link || -f _) {
1815 unlink $link or croak $!;
1818 my $dest = `git-cat-file blob $blob`; # no newline, so no chomp
1819 symlink $dest, $link or croak $!;
1822 sub blob_to_file {
1823 my ($blob, $file) = @_;
1824 defined $file or croak "\$file not defined!\n";
1825 croak "Not a sha1: $blob\n" unless $blob =~ /^$sha1$/o;
1826 if (-l $file || -f _) {
1827 unlink $file or croak $!;
1830 open my $blob_fh, '>', $file or croak "$!: $file\n";
1831 my $pid = fork;
1832 defined $pid or croak $!;
1834 if ($pid == 0) {
1835 open STDOUT, '>&', $blob_fh or croak $!;
1836 exec('git-cat-file','blob',$blob) or croak $!;
1838 waitpid $pid, 0;
1839 croak $? if $?;
1841 close $blob_fh or croak $!;
1844 sub safe_qx {
1845 my $pid = open my $child, '-|';
1846 defined $pid or croak $!;
1847 if ($pid == 0) {
1848 exec(@_) or croak $!;
1850 my @ret = (<$child>);
1851 close $child or croak $?;
1852 die $? if $?; # just in case close didn't error out
1853 return wantarray ? @ret : join('',@ret);
1856 sub svn_compat_check {
1857 my @co_help = safe_qx(qw(svn co -h));
1858 unless (grep /ignore-externals/,@co_help) {
1859 print STDERR "W: Installed svn version does not support ",
1860 "--ignore-externals\n";
1861 $_no_ignore_ext = 1;
1863 if (grep /usage: checkout URL\[\@REV\]/,@co_help) {
1864 $_svn_co_url_revs = 1;
1866 if (grep /\[TARGET\[\@REV\]\.\.\.\]/, `svn propget -h`) {
1867 $_svn_pg_peg_revs = 1;
1870 # I really, really hope nobody hits this...
1871 unless (grep /stop-on-copy/, (safe_qx(qw(svn log -h)))) {
1872 print STDERR <<'';
1873 W: The installed svn version does not support the --stop-on-copy flag in
1874 the log command.
1875 Lets hope the directory you're tracking is not a branch or tag
1876 and was never moved within the repository...
1878 $_no_stop_copy = 1;
1882 # *sigh*, new versions of svn won't honor -r<rev> without URL@<rev>,
1883 # (and they won't honor URL@<rev> without -r<rev>, too!)
1884 sub svn_cmd_checkout {
1885 my ($url, $rev, $dir) = @_;
1886 my @cmd = ('svn','co', "-r$rev");
1887 push @cmd, '--ignore-externals' unless $_no_ignore_ext;
1888 $url .= "\@$rev" if $_svn_co_url_revs;
1889 sys(@cmd, $url, $dir);
1892 sub check_upgrade_needed {
1893 my $old = eval {
1894 my $pid = open my $child, '-|';
1895 defined $pid or croak $!;
1896 if ($pid == 0) {
1897 close STDERR;
1898 exec('git-rev-parse',"$GIT_SVN-HEAD") or croak $!;
1900 my @ret = (<$child>);
1901 close $child or croak $?;
1902 die $? if $?; # just in case close didn't error out
1903 return wantarray ? @ret : join('',@ret);
1905 return unless $old;
1906 my $head = eval { safe_qx('git-rev-parse',"refs/remotes/$GIT_SVN") };
1907 if ($@ || !$head) {
1908 print STDERR "Please run: $0 rebuild --upgrade\n";
1909 exit 1;
1913 # fills %tree_map with a reverse mapping of trees to commits. Useful
1914 # for finding parents to commit on.
1915 sub map_tree_joins {
1916 my %seen;
1917 foreach my $br (@_branch_from) {
1918 my $pid = open my $pipe, '-|';
1919 defined $pid or croak $!;
1920 if ($pid == 0) {
1921 exec(qw(git-rev-list --topo-order --pretty=raw), $br)
1922 or croak $!;
1924 while (<$pipe>) {
1925 if (/^commit ($sha1)$/o) {
1926 my $commit = $1;
1928 # if we've seen a commit,
1929 # we've seen its parents
1930 last if $seen{$commit};
1931 my ($tree) = (<$pipe> =~ /^tree ($sha1)$/o);
1932 unless (defined $tree) {
1933 die "Failed to parse commit $commit\n";
1935 push @{$tree_map{$tree}}, $commit;
1936 $seen{$commit} = 1;
1939 close $pipe; # we could be breaking the pipe early
1943 sub load_all_refs {
1944 if (@_branch_from) {
1945 print STDERR '--branch|-b parameters are ignored when ',
1946 "--branch-all-refs|-B is passed\n";
1949 # don't worry about rev-list on non-commit objects/tags,
1950 # it shouldn't blow up if a ref is a blob or tree...
1951 chomp(@_branch_from = `git-rev-parse --symbolic --all`);
1954 # '<svn username> = real-name <email address>' mapping based on git-svnimport:
1955 sub load_authors {
1956 open my $authors, '<', $_authors or die "Can't open $_authors $!\n";
1957 while (<$authors>) {
1958 chomp;
1959 next unless /^(\S+?)\s*=\s*(.+?)\s*<(.+)>\s*$/;
1960 my ($user, $name, $email) = ($1, $2, $3);
1961 $users{$user} = [$name, $email];
1963 close $authors or croak $!;
1966 sub rload_authors {
1967 open my $authors, '<', $_authors or die "Can't open $_authors $!\n";
1968 while (<$authors>) {
1969 chomp;
1970 next unless /^(\S+?)\s*=\s*(.+?)\s*<(.+)>\s*$/;
1971 my ($user, $name, $email) = ($1, $2, $3);
1972 $rusers{"$name <$email>"} = $user;
1974 close $authors or croak $!;
1977 sub svn_propget_base {
1978 my ($p, $f) = @_;
1979 $f .= '@BASE' if $_svn_pg_peg_revs;
1980 return safe_qx(qw/svn propget/, $p, $f);
1983 sub git_svn_each {
1984 my $sub = shift;
1985 foreach (`git-rev-parse --symbolic --all`) {
1986 next unless s#^refs/remotes/##;
1987 chomp $_;
1988 next unless -f "$GIT_DIR/svn/$_/info/url";
1989 &$sub($_);
1993 sub migration_check {
1994 return if (-d "$GIT_DIR/svn" || !-d $GIT_DIR);
1995 print "Upgrading repository...\n";
1996 unless (-d "$GIT_DIR/svn") {
1997 mkdir "$GIT_DIR/svn" or croak $!;
1999 print "Data from a previous version of git-svn exists, but\n\t",
2000 "$GIT_SVN_DIR\n\t(required for this version ",
2001 "($VERSION) of git-svn) does not.\n";
2003 foreach my $x (`git-rev-parse --symbolic --all`) {
2004 next unless $x =~ s#^refs/remotes/##;
2005 chomp $x;
2006 next unless -f "$GIT_DIR/$x/info/url";
2007 my $u = eval { file_to_s("$GIT_DIR/$x/info/url") };
2008 next unless $u;
2009 my $dn = dirname("$GIT_DIR/svn/$x");
2010 mkpath([$dn]) unless -d $dn;
2011 rename "$GIT_DIR/$x", "$GIT_DIR/svn/$x" or croak "$!: $x";
2012 my ($url, $path) = repo_path_split($u);
2013 s_to_file($url, "$GIT_DIR/svn/$x/info/repo_url");
2014 s_to_file($path, "$GIT_DIR/svn/$x/info/repo_path");
2016 print "Done upgrading.\n";
2019 sub find_rev_before {
2020 my ($r, $git_svn_id) = @_;
2021 my @revs = map { basename $_ } <$GIT_DIR/svn/$git_svn_id/revs/*>;
2022 foreach my $r0 (sort { $b <=> $a } @revs) {
2023 next if $r0 >= $r;
2024 return ($r0, file_to_s("$GIT_DIR/svn/$git_svn_id/revs/$r0"));
2026 return (undef, undef);
2029 sub init_vars {
2030 $GIT_SVN ||= $ENV{GIT_SVN_ID} || 'git-svn';
2031 $GIT_SVN_DIR = "$GIT_DIR/svn/$GIT_SVN";
2032 $GIT_SVN_INDEX = "$GIT_SVN_DIR/index";
2033 $SVN_URL = undef;
2034 $REV_DIR = "$GIT_SVN_DIR/revs";
2035 $SVN_WC = "$GIT_SVN_DIR/tree";
2038 # convert GetOpt::Long specs for use by git-repo-config
2039 sub read_repo_config {
2040 return unless -d $GIT_DIR;
2041 my $opts = shift;
2042 foreach my $o (keys %$opts) {
2043 my $v = $opts->{$o};
2044 my ($key) = ($o =~ /^([a-z\-]+)/);
2045 $key =~ s/-//g;
2046 my $arg = 'git-repo-config';
2047 $arg .= ' --int' if ($o =~ /[:=]i$/);
2048 $arg .= ' --bool' if ($o !~ /[:=][sfi]$/);
2049 if (ref $v eq 'ARRAY') {
2050 chomp(my @tmp = `$arg --get-all svn.$key`);
2051 @$v = @tmp if @tmp;
2052 } else {
2053 chomp(my $tmp = `$arg --get svn.$key`);
2054 if ($tmp && !($arg =~ / --bool / && $tmp eq 'false')) {
2055 $$v = $tmp;
2061 sub set_default_vals {
2062 if (defined $_repack) {
2063 $_repack = 1000 if ($_repack <= 0);
2064 $_repack_nr = $_repack;
2065 $_repack_flags ||= '';
2069 sub read_grafts {
2070 my $gr_file = shift;
2071 my ($grafts, $comments) = ({}, {});
2072 if (open my $fh, '<', $gr_file) {
2073 my @tmp;
2074 while (<$fh>) {
2075 if (/^($sha1)\s+/) {
2076 my $c = $1;
2077 if (@tmp) {
2078 @{$comments->{$c}} = @tmp;
2079 @tmp = ();
2081 foreach my $p (split /\s+/, $_) {
2082 $grafts->{$c}->{$p} = 1;
2084 } else {
2085 push @tmp, $_;
2088 close $fh or croak $!;
2089 @{$comments->{'END'}} = @tmp if @tmp;
2091 return ($grafts, $comments);
2094 sub write_grafts {
2095 my ($grafts, $comments, $gr_file) = @_;
2097 open my $fh, '>', $gr_file or croak $!;
2098 foreach my $c (sort keys %$grafts) {
2099 if ($comments->{$c}) {
2100 print $fh $_ foreach @{$comments->{$c}};
2102 my $p = $grafts->{$c};
2103 delete $p->{$c}; # commits are not self-reproducing...
2104 my $pid = open my $ch, '-|';
2105 defined $pid or croak $!;
2106 if (!$pid) {
2107 exec(qw/git-cat-file commit/, $c) or croak $!;
2109 while (<$ch>) {
2110 if (/^parent ([a-f\d]{40})/) {
2111 $p->{$1} = 1;
2112 } else {
2113 last unless /^\S/i;
2116 close $ch; # breaking the pipe
2117 print $fh $c, ' ', join(' ', sort keys %$p),"\n";
2119 if ($comments->{'END'}) {
2120 print $fh $_ foreach @{$comments->{'END'}};
2122 close $fh or croak $!;
2125 sub read_url_paths {
2126 my $l_map = {};
2127 git_svn_each(sub { my $x = shift;
2128 my $u = file_to_s("$GIT_DIR/svn/$x/info/repo_url");
2129 my $p = file_to_s("$GIT_DIR/svn/$x/info/repo_path");
2130 # we hate trailing slashes
2131 if ($u =~ s#(?:^\/+|\/+$)##g) {
2132 s_to_file($u,"$GIT_DIR/svn/$x/info/repo_url");
2134 if ($p =~ s#(?:^\/+|\/+$)##g) {
2135 s_to_file($p,"$GIT_DIR/svn/$x/info/repo_path");
2137 $l_map->{$u}->{$p} = $x;
2139 return $l_map;
2142 sub extract_metadata {
2143 my $id = shift;
2144 my ($url, $rev, $uuid) = ($id =~ /^git-svn-id:\s(\S+?)\@(\d+)
2145 \s([a-f\d\-]+)$/x);
2146 if (!$rev || !$uuid || !$url) {
2147 # some of the original repositories I made had
2148 # indentifiers like this:
2149 ($rev, $uuid) = ($id =~/^git-svn-id:\s(\d+)\@([a-f\d\-]+)/);
2151 return ($url, $rev, $uuid);
2154 sub tz_to_s_offset {
2155 my ($tz) = @_;
2156 $tz =~ s/(\d\d)$//;
2157 return ($1 * 60) + ($tz * 3600);
2160 sub setup_pager { # translated to Perl from pager.c
2161 return unless (-t *STDOUT);
2162 my $pager = $ENV{PAGER};
2163 if (!defined $pager) {
2164 $pager = 'less';
2165 } elsif (length $pager == 0 || $pager eq 'cat') {
2166 return;
2168 pipe my $rfd, my $wfd or return;
2169 defined(my $pid = fork) or croak $!;
2170 if (!$pid) {
2171 open STDOUT, '>&', $wfd or croak $!;
2172 return;
2174 open STDIN, '<&', $rfd or croak $!;
2175 $ENV{LESS} ||= '-S';
2176 exec $pager or croak "Can't run pager: $!\n";;
2179 sub get_author_info {
2180 my ($dest, $author, $t, $tz) = @_;
2181 $author =~ s/(?:^\s*|\s*$)//g;
2182 my $_a;
2183 if ($_authors) {
2184 $_a = $rusers{$author} || undef;
2186 if (!$_a) {
2187 ($_a) = ($author =~ /<([^>]+)\@[^>]+>$/);
2189 $dest->{t} = $t;
2190 $dest->{tz} = $tz;
2191 $dest->{a} = $_a;
2192 # Date::Parse isn't in the standard Perl distro :(
2193 if ($tz =~ s/^\+//) {
2194 $t += tz_to_s_offset($tz);
2195 } elsif ($tz =~ s/^\-//) {
2196 $t -= tz_to_s_offset($tz);
2198 $dest->{t_utc} = $t;
2201 sub process_commit {
2202 my ($c, $r_min, $r_max, $defer) = @_;
2203 if (defined $r_min && defined $r_max) {
2204 if ($r_min == $c->{r} && $r_min == $r_max) {
2205 show_commit($c);
2206 return 0;
2208 return 1 if $r_min == $r_max;
2209 if ($r_min < $r_max) {
2210 # we need to reverse the print order
2211 return 0 if (defined $_limit && --$_limit < 0);
2212 push @$defer, $c;
2213 return 1;
2215 if ($r_min != $r_max) {
2216 return 1 if ($r_min < $c->{r});
2217 return 1 if ($r_max > $c->{r});
2220 return 0 if (defined $_limit && --$_limit < 0);
2221 show_commit($c);
2222 return 1;
2225 sub show_commit {
2226 my $c = shift;
2227 if ($_oneline) {
2228 my $x = "\n";
2229 if (my $l = $c->{l}) {
2230 while ($l->[0] =~ /^\s*$/) { shift @$l }
2231 $x = $l->[0];
2233 $_l_fmt ||= 'A' . length($c->{r});
2234 print 'r',pack($_l_fmt, $c->{r}),' | ';
2235 print "$c->{c} | " if $_show_commit;
2236 print $x;
2237 } else {
2238 show_commit_normal($c);
2242 sub show_commit_normal {
2243 my ($c) = @_;
2244 print '-' x72, "\nr$c->{r} | ";
2245 print "$c->{c} | " if $_show_commit;
2246 print "$c->{a} | ", strftime("%Y-%m-%d %H:%M:%S %z (%a, %d %b %Y)",
2247 localtime($c->{t_utc})), ' | ';
2248 my $nr_line = 0;
2250 if (my $l = $c->{l}) {
2251 while ($l->[$#$l] eq "\n" && $l->[($#$l - 1)] eq "\n") {
2252 pop @$l;
2254 $nr_line = scalar @$l;
2255 if (!$nr_line) {
2256 print "1 line\n\n\n";
2257 } else {
2258 if ($nr_line == 1) {
2259 $nr_line = '1 line';
2260 } else {
2261 $nr_line .= ' lines';
2263 print $nr_line, "\n\n";
2264 print $_ foreach @$l;
2266 } else {
2267 print "1 line\n\n";
2270 foreach my $x (qw/raw diff/) {
2271 if ($c->{$x}) {
2272 print "\n";
2273 print $_ foreach @{$c->{$x}}
2278 sub libsvn_load {
2279 return unless $_use_lib;
2280 $_use_lib = eval {
2281 require SVN::Core;
2282 if ($SVN::Core::VERSION lt '1.2.1') {
2283 die "Need SVN::Core 1.2.1 or better ",
2284 "(got $SVN::Core::VERSION) ",
2285 "Falling back to command-line svn\n";
2287 require SVN::Ra;
2288 require SVN::Delta;
2289 push @SVN::Git::Editor::ISA, 'SVN::Delta::Editor';
2290 my $kill_stupid_warnings = $SVN::Node::none.$SVN::Node::file.
2291 $SVN::Node::dir.$SVN::Node::unknown.
2292 $SVN::Node::none.$SVN::Node::file.
2293 $SVN::Node::dir.$SVN::Node::unknown;
2298 sub libsvn_connect {
2299 my ($url) = @_;
2300 my $auth = SVN::Core::auth_open([SVN::Client::get_simple_provider(),
2301 SVN::Client::get_ssl_server_trust_file_provider(),
2302 SVN::Client::get_username_provider()]);
2303 my $s = eval { SVN::Ra->new(url => $url, auth => $auth) };
2304 return $s;
2307 sub libsvn_get_file {
2308 my ($gui, $f, $rev) = @_;
2309 my $p = $f;
2310 return unless ($p =~ s#^\Q$SVN_PATH\E/?##);
2312 my $fd = IO::File->new_tmpfile or croak $!;
2313 my $pool = SVN::Pool->new;
2314 my ($r, $props) = $SVN->get_file($f, $rev, $fd, $pool);
2315 $pool->clear;
2316 $fd->flush == 0 or croak $!;
2317 seek $fd, 0, 0 or croak $!;
2318 if (my $es = $props->{'svn:eol-style'}) {
2319 my $new_fd = IO::File->new_tmpfile or croak $!;
2320 eol_cp_fd($fd, $new_fd, $es);
2321 close $fd or croak $!;
2322 $fd = $new_fd;
2323 seek $fd, 0, 0 or croak $!;
2324 $fd->flush == 0 or croak $!;
2326 my $mode = '100644';
2327 if (exists $props->{'svn:executable'}) {
2328 $mode = '100755';
2330 if (exists $props->{'svn:special'}) {
2331 $mode = '120000';
2332 local $/;
2333 my $link = <$fd>;
2334 $link =~ s/^link // or die "svn:special file with contents: <",
2335 $link, "> is not understood\n";
2336 seek $fd, 0, 0 or croak $!;
2337 truncate $fd, 0 or croak $!;
2338 print $fd $link or croak $!;
2339 seek $fd, 0, 0 or croak $!;
2340 $fd->flush == 0 or croak $!;
2342 my $pid = open my $ho, '-|';
2343 defined $pid or croak $!;
2344 if (!$pid) {
2345 open STDIN, '<&', $fd or croak $!;
2346 exec qw/git-hash-object -w --stdin/ or croak $!;
2348 chomp(my $hash = do { local $/; <$ho> });
2349 close $ho or croak $?;
2350 $hash =~ /^$sha1$/o or die "not a sha1: $hash\n";
2351 print $gui $mode,' ',$hash,"\t",$p,"\0" or croak $!;
2352 close $fd or croak $!;
2355 sub libsvn_log_entry {
2356 my ($rev, $author, $date, $msg, $parents) = @_;
2357 my ($Y,$m,$d,$H,$M,$S) = ($date =~ /^(\d{4})\-(\d\d)\-(\d\d)T
2358 (\d\d)\:(\d\d)\:(\d\d).\d+Z$/x)
2359 or die "Unable to parse date: $date\n";
2360 if (defined $_authors && ! defined $users{$author}) {
2361 die "Author: $author not defined in $_authors file\n";
2363 return { revision => $rev, date => "+0000 $Y-$m-$d $H:$M:$S",
2364 author => $author, msg => $msg."\n", parents => $parents || [] }
2367 sub process_rm {
2368 my ($gui, $last_commit, $f) = @_;
2369 $f =~ s#^\Q$SVN_PATH\E/?## or return;
2370 # remove entire directories.
2371 if (safe_qx('git-ls-tree',$last_commit,'--',$f) =~ /^040000 tree/) {
2372 defined(my $pid = open my $ls, '-|') or croak $!;
2373 if (!$pid) {
2374 exec(qw/git-ls-tree -r --name-only -z/,
2375 $last_commit,'--',$f) or croak $!;
2377 local $/ = "\0";
2378 while (<$ls>) {
2379 print $gui '0 ',0 x 40,"\t",$_ or croak $!;
2381 close $ls or croak $!;
2382 } else {
2383 print $gui '0 ',0 x 40,"\t",$f,"\0" or croak $!;
2387 sub libsvn_fetch {
2388 my ($last_commit, $paths, $rev, $author, $date, $msg) = @_;
2389 open my $gui, '| git-update-index -z --index-info' or croak $!;
2390 my @amr;
2391 foreach my $f (keys %$paths) {
2392 my $m = $paths->{$f}->action();
2393 $f =~ s#^/+##;
2394 if ($m =~ /^[DR]$/) {
2395 process_rm($gui, $last_commit, $f);
2396 next if $m eq 'D';
2397 # 'R' can be file replacements, too, right?
2399 my $pool = SVN::Pool->new;
2400 my $t = $SVN->check_path($f, $rev, $pool);
2401 if ($t == $SVN::Node::file) {
2402 if ($m =~ /^[AMR]$/) {
2403 push @amr, $f;
2404 } else {
2405 die "Unrecognized action: $m, ($f r$rev)\n";
2408 $pool->clear;
2410 libsvn_get_file($gui, $_, $rev) foreach (@amr);
2411 close $gui or croak $!;
2412 return libsvn_log_entry($rev, $author, $date, $msg, [$last_commit]);
2415 sub svn_grab_base_rev {
2416 defined(my $pid = open my $fh, '-|') or croak $!;
2417 if (!$pid) {
2418 open my $null, '>', '/dev/null' or croak $!;
2419 open STDERR, '>&', $null or croak $!;
2420 exec qw/git-rev-parse --verify/,"refs/remotes/$GIT_SVN^0"
2421 or croak $!;
2423 chomp(my $c = do { local $/; <$fh> });
2424 close $fh;
2425 if (defined $c && length $c) {
2426 my ($url, $rev, $uuid) = extract_metadata((grep(/^git-svn-id: /,
2427 safe_qx(qw/git-cat-file commit/, $c)))[0]);
2428 return ($rev, $c);
2430 return (undef, undef);
2433 sub libsvn_parse_revision {
2434 my $base = shift;
2435 my $head = $SVN->get_latest_revnum();
2436 if (!defined $_revision || $_revision eq 'BASE:HEAD') {
2437 return ($base + 1, $head) if (defined $base);
2438 return (0, $head);
2440 return ($1, $2) if ($_revision =~ /^(\d+):(\d+)$/);
2441 return ($_revision, $_revision) if ($_revision =~ /^\d+$/);
2442 if ($_revision =~ /^BASE:(\d+)$/) {
2443 return ($base + 1, $1) if (defined $base);
2444 return (0, $head);
2446 return ($1, $head) if ($_revision =~ /^(\d+):HEAD$/);
2447 die "revision argument: $_revision not understood by git-svn\n",
2448 "Try using the command-line svn client instead\n";
2451 sub libsvn_traverse {
2452 my ($gui, $pfx, $path, $rev) = @_;
2453 my $cwd = "$pfx/$path";
2454 my $pool = SVN::Pool->new;
2455 $cwd =~ s#^/+##g;
2456 my ($dirent, $r, $props) = $SVN->get_dir($cwd, $rev, $pool);
2457 foreach my $d (keys %$dirent) {
2458 my $t = $dirent->{$d}->kind;
2459 if ($t == $SVN::Node::dir) {
2460 libsvn_traverse($gui, $cwd, $d, $rev);
2461 } elsif ($t == $SVN::Node::file) {
2462 libsvn_get_file($gui, "$cwd/$d", $rev);
2465 $pool->clear;
2468 sub libsvn_traverse_ignore {
2469 my ($fh, $path, $r) = @_;
2470 $path =~ s#^/+##g;
2471 my $pool = SVN::Pool->new;
2472 my ($dirent, undef, $props) = $SVN->get_dir($path, $r, $pool);
2473 my $p = $path;
2474 $p =~ s#^\Q$SVN_PATH\E/?##;
2475 print $fh length $p ? "\n# $p\n" : "\n# /\n";
2476 if (my $s = $props->{'svn:ignore'}) {
2477 $s =~ s/[\r\n]+/\n/g;
2478 chomp $s;
2479 if (length $p == 0) {
2480 $s =~ s#\n#\n/$p#g;
2481 print $fh "/$s\n";
2482 } else {
2483 $s =~ s#\n#\n/$p/#g;
2484 print $fh "/$p/$s\n";
2487 foreach (sort keys %$dirent) {
2488 next if $dirent->{$_}->kind != $SVN::Node::dir;
2489 libsvn_traverse_ignore($fh, "$path/$_", $r);
2491 $pool->clear;
2494 sub libsvn_new_tree {
2495 my ($paths, $rev, $author, $date, $msg) = @_;
2496 my $svn_path = '/'.$SVN_PATH;
2498 # look for a parent from another branch:
2499 foreach (keys %$paths) {
2500 next if ($_ ne $svn_path);
2501 my $i = $paths->{$_};
2502 my $branch_from = $i->copyfrom_path or next;
2503 my $r = $i->copyfrom_rev;
2504 print STDERR "Found possible branch point: ",
2505 "$branch_from => $svn_path, $r\n";
2506 $branch_from =~ s#^/##;
2507 my $l_map = read_url_paths();
2508 my $url = $SVN->{url};
2509 defined $l_map->{$url} or next;
2510 my $id = $l_map->{$url}->{$branch_from} or next;
2511 my $f = "$GIT_DIR/svn/$id/revs/$r";
2512 while ($r && !-r $f) {
2513 $r--;
2514 $f = "$GIT_DIR/svn/$id/revs/$r";
2516 if (-r $f) {
2517 my $parent = file_to_s($f);
2518 unlink $GIT_SVN_INDEX;
2519 print STDERR "Found branch parent: $parent\n";
2520 sys(qw/git-read-tree/, $parent);
2521 return libsvn_fetch($parent, $paths, $rev,
2522 $author, $date, $msg);
2524 print STDERR "Nope, branch point not imported or unknown\n";
2526 open my $gui, '| git-update-index -z --index-info' or croak $!;
2527 my $pool = SVN::Pool->new;
2528 libsvn_traverse($gui, '', $SVN_PATH, $rev, $pool);
2529 $pool->clear;
2530 close $gui or croak $!;
2531 return libsvn_log_entry($rev, $author, $date, $msg);
2534 sub find_graft_path_commit {
2535 my ($tree_paths, $p1, $r1) = @_;
2536 foreach my $x (keys %$tree_paths) {
2537 next unless ($p1 =~ /^\Q$x\E/);
2538 my $i = $tree_paths->{$x};
2539 my $f = "$GIT_DIR/svn/$i/revs/$r1";
2541 return file_to_s($f) if (-r $f);
2543 print STDERR "r$r1 of $i not imported\n";
2544 next;
2546 return undef;
2549 sub find_graft_path_parents {
2550 my ($grafts, $tree_paths, $c, $p0, $r0) = @_;
2551 foreach my $x (keys %$tree_paths) {
2552 next unless ($p0 =~ /^\Q$x\E/);
2553 my $i = $tree_paths->{$x};
2554 my $f = "$GIT_DIR/svn/$i/revs/$r0";
2555 while ($r0 && !-r $f) {
2556 # could be an older revision, too...
2557 $r0--;
2558 $f = "$GIT_DIR/svn/$i/revs/$r0";
2560 unless (-r $f) {
2561 print STDERR "r$r0 of $i not imported\n";
2562 next;
2564 my $parent = file_to_s($f);
2565 $grafts->{$c}->{$parent} = 1;
2569 sub libsvn_graft_file_copies {
2570 my ($grafts, $tree_paths, $path, $paths, $rev) = @_;
2571 foreach (keys %$paths) {
2572 my $i = $paths->{$_};
2573 my ($m, $p0, $r0) = ($i->action, $i->copyfrom_path,
2574 $i->copyfrom_rev);
2575 next unless (defined $p0 && defined $r0);
2577 my $p1 = $_;
2578 $p1 =~ s#^/##;
2579 $p0 =~ s#^/##;
2580 my $c = find_graft_path_commit($tree_paths, $p1, $rev);
2581 next unless $c;
2582 find_graft_path_parents($grafts, $tree_paths, $c, $p0, $r0);
2586 sub set_index {
2587 my $old = $ENV{GIT_INDEX_FILE};
2588 $ENV{GIT_INDEX_FILE} = shift;
2589 return $old;
2592 sub restore_index {
2593 my ($old) = @_;
2594 if (defined $old) {
2595 $ENV{GIT_INDEX_FILE} = $old;
2596 } else {
2597 delete $ENV{GIT_INDEX_FILE};
2601 sub libsvn_commit_cb {
2602 my ($rev, $date, $committer, $c, $msg, $r_last, $cmt_last) = @_;
2603 if ($rev == ($r_last + 1)) {
2604 # optimized (avoid fetch)
2605 my $log = libsvn_log_entry($rev,$committer,$date,$msg);
2606 $log->{tree} = get_tree_from_treeish($c);
2607 my $cmt = git_commit($log, $cmt_last, $c);
2608 my @diff = safe_qx('git-diff-tree', $cmt, $c);
2609 if (@diff) {
2610 print STDERR "Trees differ: $cmt $c\n",
2611 join('',@diff),"\n";
2612 exit 1;
2614 } else {
2615 fetch_lib("$rev=$c");
2619 sub libsvn_ls_fullurl {
2620 my $fullurl = shift;
2621 my ($repo, $path) = repo_path_split($fullurl);
2622 $SVN ||= libsvn_connect($repo);
2623 my @ret;
2624 my $pool = SVN::Pool->new;
2625 my ($dirent, undef, undef) = $SVN->get_dir($path,
2626 $SVN->get_latest_revnum, $pool);
2627 foreach my $d (keys %$dirent) {
2628 if ($dirent->{$d}->kind == $SVN::Node::dir) {
2629 push @ret, "$d/"; # add '/' for compat with cli svn
2632 $pool->clear;
2633 return @ret;
2637 sub libsvn_skip_unknown_revs {
2638 my $err = shift;
2639 my $errno = $err->apr_err();
2640 # Maybe the branch we're tracking didn't
2641 # exist when the repo started, so it's
2642 # not an error if it doesn't, just continue
2644 # Wonderfully consistent library, eh?
2645 # 160013 - svn:// and file://
2646 # 175002 - http(s)://
2647 # More codes may be discovered later...
2648 if ($errno == 175002 || $errno == 160013) {
2649 print STDERR "directory non-existent\n";
2650 return;
2652 croak "Error from SVN, ($errno): ", $err->expanded_message,"\n";
2655 package SVN::Git::Editor;
2656 use vars qw/@ISA/;
2657 use strict;
2658 use warnings;
2659 use Carp qw/croak/;
2660 use IO::File;
2662 sub new {
2663 my $class = shift;
2664 my $git_svn = shift;
2665 my $self = SVN::Delta::Editor->new(@_);
2666 bless $self, $class;
2667 foreach (qw/svn_path c r ra /) {
2668 die "$_ required!\n" unless (defined $git_svn->{$_});
2669 $self->{$_} = $git_svn->{$_};
2671 $self->{pool} = SVN::Pool->new;
2672 $self->{bat} = { '' => $self->open_root($self->{r}, $self->{pool}) };
2673 $self->{rm} = { };
2674 require Digest::MD5;
2675 return $self;
2678 sub split_path {
2679 return ($_[0] =~ m#^(.*?)/?([^/]+)$#);
2682 sub repo_path {
2683 (defined $_[1] && length $_[1]) ? "$_[0]->{svn_path}/$_[1]"
2684 : $_[0]->{svn_path}
2687 sub url_path {
2688 my ($self, $path) = @_;
2689 $self->{ra}->{url} . '/' . $self->repo_path($path);
2692 sub rmdirs {
2693 my ($self) = @_;
2694 my $rm = $self->{rm};
2695 delete $rm->{''}; # we never delete the url we're tracking
2696 return unless %$rm;
2698 foreach (keys %$rm) {
2699 my @d = split m#/#, $_;
2700 my $c = shift @d;
2701 $rm->{$c} = 1;
2702 while (@d) {
2703 $c .= '/' . shift @d;
2704 $rm->{$c} = 1;
2707 delete $rm->{$self->{svn_path}};
2708 delete $rm->{''}; # we never delete the url we're tracking
2709 return unless %$rm;
2711 defined(my $pid = open my $fh,'-|') or croak $!;
2712 if (!$pid) {
2713 exec qw/git-ls-tree --name-only -r -z/, $self->{c} or croak $!;
2715 local $/ = "\0";
2716 while (<$fh>) {
2717 chomp;
2718 $_ = $self->{svn_path} . '/' . $_;
2719 my ($dn) = ($_ =~ m#^(.*?)/?(?:[^/]+)$#);
2720 delete $rm->{$dn};
2721 last unless %$rm;
2723 my ($r, $p, $bat) = ($self->{r}, $self->{pool}, $self->{bat});
2724 foreach my $d (sort { $b =~ tr#/#/# <=> $a =~ tr#/#/# } keys %$rm) {
2725 $self->close_directory($bat->{$d}, $p);
2726 my ($dn) = ($d =~ m#^(.*?)/?(?:[^/]+)$#);
2727 $self->SUPER::delete_entry($d, $r, $bat->{$dn}, $p);
2728 delete $bat->{$d};
2732 sub open_or_add_dir {
2733 my ($self, $full_path, $baton) = @_;
2734 my $p = SVN::Pool->new;
2735 my $t = $self->{ra}->check_path($full_path, $self->{r}, $p);
2736 $p->clear;
2737 if ($t == $SVN::Node::none) {
2738 return $self->add_directory($full_path, $baton,
2739 undef, -1, $self->{pool});
2740 } elsif ($t == $SVN::Node::dir) {
2741 return $self->open_directory($full_path, $baton,
2742 $self->{r}, $self->{pool});
2744 print STDERR "$full_path already exists in repository at ",
2745 "r$self->{r} and it is not a directory (",
2746 ($t == $SVN::Node::file ? 'file' : 'unknown'),"/$t)\n";
2747 exit 1;
2750 sub ensure_path {
2751 my ($self, $path) = @_;
2752 my $bat = $self->{bat};
2753 $path = $self->repo_path($path);
2754 return $bat->{''} unless (length $path);
2755 my @p = split m#/+#, $path;
2756 my $c = shift @p;
2757 $bat->{$c} ||= $self->open_or_add_dir($c, $bat->{''});
2758 while (@p) {
2759 my $c0 = $c;
2760 $c .= '/' . shift @p;
2761 $bat->{$c} ||= $self->open_or_add_dir($c, $bat->{$c0});
2763 return $bat->{$c};
2766 sub A {
2767 my ($self, $m) = @_;
2768 my ($dir, $file) = split_path($m->{file_b});
2769 my $pbat = $self->ensure_path($dir);
2770 my $fbat = $self->add_file($self->repo_path($m->{file_b}), $pbat,
2771 undef, -1);
2772 $self->chg_file($fbat, $m);
2773 $self->close_file($fbat,undef,$self->{pool});
2776 sub C {
2777 my ($self, $m) = @_;
2778 my ($dir, $file) = split_path($m->{file_b});
2779 my $pbat = $self->ensure_path($dir);
2780 my $fbat = $self->add_file($self->repo_path($m->{file_b}), $pbat,
2781 $self->url_path($m->{file_a}), $self->{r});
2782 $self->chg_file($fbat, $m);
2783 $self->close_file($fbat,undef,$self->{pool});
2786 sub delete_entry {
2787 my ($self, $path, $pbat) = @_;
2788 my $rpath = $self->repo_path($path);
2789 my ($dir, $file) = split_path($rpath);
2790 $self->{rm}->{$dir} = 1;
2791 $self->SUPER::delete_entry($rpath, $self->{r}, $pbat, $self->{pool});
2794 sub R {
2795 my ($self, $m) = @_;
2796 my ($dir, $file) = split_path($m->{file_b});
2797 my $pbat = $self->ensure_path($dir);
2798 my $fbat = $self->add_file($self->repo_path($m->{file_b}), $pbat,
2799 $self->url_path($m->{file_a}), $self->{r});
2800 $self->chg_file($fbat, $m);
2801 $self->close_file($fbat,undef,$self->{pool});
2803 ($dir, $file) = split_path($m->{file_a});
2804 $pbat = $self->ensure_path($dir);
2805 $self->delete_entry($m->{file_a}, $pbat);
2808 sub M {
2809 my ($self, $m) = @_;
2810 my ($dir, $file) = split_path($m->{file_b});
2811 my $pbat = $self->ensure_path($dir);
2812 my $fbat = $self->open_file($self->repo_path($m->{file_b}),
2813 $pbat,$self->{r},$self->{pool});
2814 $self->chg_file($fbat, $m);
2815 $self->close_file($fbat,undef,$self->{pool});
2818 sub T { shift->M(@_) }
2820 sub change_file_prop {
2821 my ($self, $fbat, $pname, $pval) = @_;
2822 $self->SUPER::change_file_prop($fbat, $pname, $pval, $self->{pool});
2825 sub chg_file {
2826 my ($self, $fbat, $m) = @_;
2827 if ($m->{mode_b} =~ /755$/ && $m->{mode_a} !~ /755$/) {
2828 $self->change_file_prop($fbat,'svn:executable','*');
2829 } elsif ($m->{mode_b} !~ /755$/ && $m->{mode_a} =~ /755$/) {
2830 $self->change_file_prop($fbat,'svn:executable',undef);
2832 my $fh = IO::File->new_tmpfile or croak $!;
2833 if ($m->{mode_b} =~ /^120/) {
2834 print $fh 'link ' or croak $!;
2835 $self->change_file_prop($fbat,'svn:special','*');
2836 } elsif ($m->{mode_a} =~ /^120/ && $m->{mode_b} !~ /^120/) {
2837 $self->change_file_prop($fbat,'svn:special',undef);
2839 defined(my $pid = fork) or croak $!;
2840 if (!$pid) {
2841 open STDOUT, '>&', $fh or croak $!;
2842 exec qw/git-cat-file blob/, $m->{sha1_b} or croak $!;
2844 waitpid $pid, 0;
2845 croak $? if $?;
2846 $fh->flush == 0 or croak $!;
2847 seek $fh, 0, 0 or croak $!;
2849 my $md5 = Digest::MD5->new;
2850 $md5->addfile($fh) or croak $!;
2851 seek $fh, 0, 0 or croak $!;
2853 my $exp = $md5->hexdigest;
2854 my $atd = $self->apply_textdelta($fbat, undef, $self->{pool});
2855 my $got = SVN::TxDelta::send_stream($fh, @$atd, $self->{pool});
2856 die "Checksum mismatch\nexpected: $exp\ngot: $got\n" if ($got ne $exp);
2858 close $fh or croak $!;
2861 sub D {
2862 my ($self, $m) = @_;
2863 my ($dir, $file) = split_path($m->{file_b});
2864 my $pbat = $self->ensure_path($dir);
2865 $self->delete_entry($m->{file_b}, $pbat);
2868 sub close_edit {
2869 my ($self) = @_;
2870 my ($p,$bat) = ($self->{pool}, $self->{bat});
2871 foreach (sort { $b =~ tr#/#/# <=> $a =~ tr#/#/# } keys %$bat) {
2872 $self->close_directory($bat->{$_}, $p);
2874 $self->SUPER::close_edit($p);
2875 $p->clear;
2878 sub abort_edit {
2879 my ($self) = @_;
2880 $self->SUPER::abort_edit($self->{pool});
2881 $self->{pool}->clear;
2884 __END__
2886 Data structures:
2888 $svn_log hashref (as returned by svn_log_raw)
2890 fh => file handle of the log file,
2891 state => state of the log file parser (sep/msg/rev/msg_start...)
2894 $log_msg hashref as returned by next_log_entry($svn_log)
2896 msg => 'whitespace-formatted log entry
2897 ', # trailing newline is preserved
2898 revision => '8', # integer
2899 date => '2004-02-24T17:01:44.108345Z', # commit date
2900 author => 'committer name'
2904 @mods = array of diff-index line hashes, each element represents one line
2905 of diff-index output
2907 diff-index line ($m hash)
2909 mode_a => first column of diff-index output, no leading ':',
2910 mode_b => second column of diff-index output,
2911 sha1_b => sha1sum of the final blob,
2912 chg => change type [MCRADT],
2913 file_a => original file name of a file (iff chg is 'C' or 'R')
2914 file_b => new/current file name of a file (any chg)
2918 Notes:
2919 I don't trust the each() function on unless I created %hash myself
2920 because the internal iterator may not have started at base.