Extract Git::SVN from git-svn into its own .pm file.
Except for adding the 1; at the end, this is a straight copy & paste. Tests still pass, but its doubtful Git::SVN will compile on its own without git-svn being loaded. Next commit will fix that. Signed-off-by: Junio C Hamano <gitster@pobox.com> Signed-off-by: Eric Wong <normalperson@yhbt.net>
Michael G. Schwern committed
Jul 26, 2012 at 16:22 UTC
29499c0b2741cc2ea13b78a342048bad928dfe2a
3 files changed
+2321
-2319
git-svn.perl
+1
-2319
@@ -10,6 +10,7 @@ use vars qw/ $AUTHOR $VERSION
10
$AUTHOR = 'Eric Wong <normalperson@yhbt.net>';
11
$VERSION = '@@GIT_VERSION@@';
12
13
+use Git::SVN;
14
use Git::SVN::Utils qw(fatal can_compress);
15
16
# From which subdir have we been invoked?
@@ -2041,2325 +2042,6 @@ sub gc_directory {
2042
}
2043
}
2044
2044
-{
2045
-package Git::SVN;
2046
-use strict;
2047
-use warnings;
2048
-use Fcntl qw/:DEFAULT :seek/;
2049
-use constant rev_map_fmt => 'NH40';
2050
-use vars qw/$default_repo_id $default_ref_id $_no_metadata $_follow_parent
2051
- $_repack $_repack_flags $_use_svm_props $_head
2052
- $_use_svnsync_props $no_reuse_existing $_minimize_url
2053
- $_use_log_author $_add_author_from $_localtime/;
2054
-use Carp qw/croak/;
2055
-use File::Path qw/mkpath/;
2056
-use File::Copy qw/copy/;
2057
-use IPC::Open3;
2058
-use Time::Local;
2059
-use Memoize; # core since 5.8.0, Jul 2002
2060
-use Memoize::Storable;
2061
-use POSIX qw(:signal_h);
2062
-
2063
-use Git qw(
2064
- command
2065
- command_oneline
2066
- command_noisy
2067
- command_output_pipe
2068
- command_close_pipe
2069
-);
2070
-use Git::SVN::Utils qw(fatal can_compress);
2071
-
2072
-my $can_use_yaml;
2073
-BEGIN {
2074
- $can_use_yaml = eval { require Git::SVN::Memoize::YAML; 1};
2075
-}
2076
-
2077
-my ($_gc_nr, $_gc_period);
2078
-
2079
-# properties that we do not log:
2080
-my %SKIP_PROP;
2081
-BEGIN {
2082
- %SKIP_PROP = map { $_ => 1 } qw/svn:wc:ra_dav:version-url
2083
- svn:special svn:executable
2084
- svn:entry:committed-rev
2085
- svn:entry:last-author
2086
- svn:entry:uuid
2087
- svn:entry:committed-date/;
2088
-
2089
- # some options are read globally, but can be overridden locally
2090
- # per [svn-remote "..."] section. Command-line options will *NOT*
2091
- # override options set in an [svn-remote "..."] section
2092
- no strict 'refs';
2093
- for my $option (qw/follow_parent no_metadata use_svm_props
2094
- use_svnsync_props/) {
2095
- my $key = $option;
2096
- $key =~ tr/_//d;
2097
- my $prop = "-$option";
2098
- *$option = sub {
2099
- my ($self) = @_;
2100
- return $self->{$prop} if exists $self->{$prop};
2101
- my $k = "svn-remote.$self->{repo_id}.$key";
2102
- eval { command_oneline(qw/config --get/, $k) };
2103
- if ($@) {
2104
- $self->{$prop} = ${"Git::SVN::_$option"};
2105
- } else {
2106
- my $v = command_oneline(qw/config --bool/,$k);
2107
- $self->{$prop} = $v eq 'false' ? 0 : 1;
2108
- }
2109
- return $self->{$prop};
2110
- }
2111
- }
2112
-}
2113
-
2114
-
2115
-my (%LOCKFILES, %INDEX_FILES);
2116
-END {
2117
- unlink keys %LOCKFILES if %LOCKFILES;
2118
- unlink keys %INDEX_FILES if %INDEX_FILES;
2119
-}
2120
-
2121
-sub resolve_local_globs {
2122
- my ($url, $fetch, $glob_spec) = @_;
2123
- return unless defined $glob_spec;
2124
- my $ref = $glob_spec->{ref};
2125
- my $path = $glob_spec->{path};
2126
- foreach (command(qw#for-each-ref --format=%(refname) refs/#)) {
2127
- next unless m#^$ref->{regex}$#;
2128
- my $p = $1;
2129
- my $pathname = desanitize_refname($path->full_path($p));
2130
- my $refname = desanitize_refname($ref->full_path($p));
2131
- if (my $existing = $fetch->{$pathname}) {
2132
- if ($existing ne $refname) {
2133
- die "Refspec conflict:\n",
2134
- "existing: $existing\n",
2135
- " globbed: $refname\n";
2136
- }
2137
- my $u = (::cmt_metadata("$refname"))[0];
2138
- $u =~ s!^\Q$url\E(/|$)!! or die
2139
- "$refname: '$url' not found in '$u'\n";
2140
- if ($pathname ne $u) {
2141
- warn "W: Refspec glob conflict ",
2142
- "(ref: $refname):\n",
2143
- "expected path: $pathname\n",
2144
- " real path: $u\n",
2145
- "Continuing ahead with $u\n";
2146
- next;
2147
- }
2148
- } else {
2149
- $fetch->{$pathname} = $refname;
2150
- }
2151
- }
2152
-}
2153
-
2154
-sub parse_revision_argument {
2155
- my ($base, $head) = @_;
2156
- if (!defined $::_revision || $::_revision eq 'BASE:HEAD') {
2157
- return ($base, $head);
2158
- }
2159
- return ($1, $2) if ($::_revision =~ /^(\d+):(\d+)$/);
2160
- return ($::_revision, $::_revision) if ($::_revision =~ /^\d+$/);
2161
- return ($head, $head) if ($::_revision eq 'HEAD');
2162
- return ($base, $1) if ($::_revision =~ /^BASE:(\d+)$/);
2163
- return ($1, $head) if ($::_revision =~ /^(\d+):HEAD$/);
2164
- die "revision argument: $::_revision not understood by git-svn\n";
2165
-}
2166
-
2167
-sub fetch_all {
2168
- my ($repo_id, $remotes) = @_;
2169
- if (ref $repo_id) {
2170
- my $gs = $repo_id;
2171
- $repo_id = undef;
2172
- $repo_id = $gs->{repo_id};
2173
- }
2174
- $remotes ||= read_all_remotes();
2175
- my $remote = $remotes->{$repo_id} or
2176
- die "[svn-remote \"$repo_id\"] unknown\n";
2177
- my $fetch = $remote->{fetch};
2178
- my $url = $remote->{url} or die "svn-remote.$repo_id.url not defined\n";
2179
- my (@gs, @globs);
2180
- my $ra = Git::SVN::Ra->new($url);
2181
- my $uuid = $ra->get_uuid;
2182
- my $head = $ra->get_latest_revnum;
2183
-
2184
- # ignore errors, $head revision may not even exist anymore
2185
- eval { $ra->get_log("", $head, 0, 1, 0, 1, sub { $head = $_[1] }) };
2186
- warn "W: $@\n" if $@;
2187
-
2188
- my $base = defined $fetch ? $head : 0;
2189
-
2190
- # read the max revs for wildcard expansion (branches/*, tags/*)
2191
- foreach my $t (qw/branches tags/) {
2192
- defined $remote->{$t} or next;
2193
- push @globs, @{$remote->{$t}};
2194
-
2195
- my $max_rev = eval { tmp_config(qw/--int --get/,
2196
- "svn-remote.$repo_id.${t}-maxRev") };
2197
- if (defined $max_rev && ($max_rev < $base)) {
2198
- $base = $max_rev;
2199
- } elsif (!defined $max_rev) {
2200
- $base = 0;
2201
- }
2202
- }
2203
-
2204
- if ($fetch) {
2205
- foreach my $p (sort keys %$fetch) {
2206
- my $gs = Git::SVN->new($fetch->{$p}, $repo_id, $p);
2207
- my $lr = $gs->rev_map_max;
2208
- if (defined $lr) {
2209
- $base = $lr if ($lr < $base);
2210
- }
2211
- push @gs, $gs;
2212
- }
2213
- }
2214
-
2215
- ($base, $head) = parse_revision_argument($base, $head);
2216
- $ra->gs_fetch_loop_common($base, $head, \@gs, \@globs);
2217
-}
2218
-
2219
-sub read_all_remotes {
2220
- my $r = {};
2221
- my $use_svm_props = eval { command_oneline(qw/config --bool
2222
- svn.useSvmProps/) };
2223
- $use_svm_props = $use_svm_props eq 'true' if $use_svm_props;
2224
- my $svn_refspec = qr{\s*(.*?)\s*:\s*(.+?)\s*};
2225
- foreach (grep { s/^svn-remote\.// } command(qw/config -l/)) {
2226
- if (m!^(.+)\.fetch=$svn_refspec$!) {
2227
- my ($remote, $local_ref, $remote_ref) = ($1, $2, $3);
2228
- die("svn-remote.$remote: remote ref '$remote_ref' "
2229
- . "must start with 'refs/'\n")
2230
- unless $remote_ref =~ m{^refs/};
2231
- $local_ref = uri_decode($local_ref);
2232
- $r->{$remote}->{fetch}->{$local_ref} = $remote_ref;
2233
- $r->{$remote}->{svm} = {} if $use_svm_props;
2234
- } elsif (m!^(.+)\.usesvmprops=\s*(.*)\s*$!) {
2235
- $r->{$1}->{svm} = {};
2236
- } elsif (m!^(.+)\.url=\s*(.*)\s*$!) {
2237
- $r->{$1}->{url} = $2;
2238
- } elsif (m!^(.+)\.pushurl=\s*(.*)\s*$!) {
2239
- $r->{$1}->{pushurl} = $2;
2240
- } elsif (m!^(.+)\.ignore-refs=\s*(.*)\s*$!) {
2241
- $r->{$1}->{ignore_refs_regex} = $2;
2242
- } elsif (m!^(.+)\.(branches|tags)=$svn_refspec$!) {
2243
- my ($remote, $t, $local_ref, $remote_ref) =
2244
- ($1, $2, $3, $4);
2245
- die("svn-remote.$remote: remote ref '$remote_ref' ($t) "
2246
- . "must start with 'refs/'\n")
2247
- unless $remote_ref =~ m{^refs/};
2248
- $local_ref = uri_decode($local_ref);
2249
- my $rs = {
2250
- t => $t,
2251
- remote => $remote,
2252
- path => Git::SVN::GlobSpec->new($local_ref, 1),
2253
- ref => Git::SVN::GlobSpec->new($remote_ref, 0) };
2254
- if (length($rs->{ref}->{right}) != 0) {
2255
- die "The '*' glob character must be the last ",
2256
- "character of '$remote_ref'\n";
2257
- }
2258
- push @{ $r->{$remote}->{$t} }, $rs;
2259
- }
2260
- }
2261
-
2262
- map {
2263
- if (defined $r->{$_}->{svm}) {
2264
- my $svm;
2265
- eval {
2266
- my $section = "svn-remote.$_";
2267
- $svm = {
2268
- source => tmp_config('--get',
2269
- "$section.svm-source"),
2270
- replace => tmp_config('--get',
2271
- "$section.svm-replace"),
2272
- }
2273
- };
2274
- $r->{$_}->{svm} = $svm;
2275
- }
2276
- } keys %$r;
2277
-
2278
- foreach my $remote (keys %$r) {
2279
- foreach ( grep { defined $_ }
2280
- map { $r->{$remote}->{$_} } qw(branches tags) ) {
2281
- foreach my $rs ( @$_ ) {
2282
- $rs->{ignore_refs_regex} =
2283
- $r->{$remote}->{ignore_refs_regex};
2284
- }
2285
- }
2286
- }
2287
-
2288
- $r;
2289
-}
2290
-
2291
-sub init_vars {
2292
- $_gc_nr = $_gc_period = 1000;
2293
- if (defined $_repack || defined $_repack_flags) {
2294
- warn "Repack options are obsolete; they have no effect.\n";
2295
- }
2296
-}
2297
-
2298
-sub verify_remotes_sanity {
2299
- return unless -d $ENV{GIT_DIR};
2300
- my %seen;
2301
- foreach (command(qw/config -l/)) {
2302
- if (m!^svn-remote\.(?:.+)\.fetch=.*:refs/remotes/(\S+)\s*$!) {
2303
- if ($seen{$1}) {
2304
- die "Remote ref refs/remote/$1 is tracked by",
2305
- "\n \"$_\"\nand\n \"$seen{$1}\"\n",
2306
- "Please resolve this ambiguity in ",
2307
- "your git configuration file before ",
2308
- "continuing\n";
2309
- }
2310
- $seen{$1} = $_;
2311
- }
2312
- }
2313
-}
2314
-
2315
-sub find_existing_remote {
2316
- my ($url, $remotes) = @_;
2317
- return undef if $no_reuse_existing;
2318
- my $existing;
2319
- foreach my $repo_id (keys %$remotes) {
2320
- my $u = $remotes->{$repo_id}->{url} or next;
2321
- next if $u ne $url;
2322
- $existing = $repo_id;
2323
- last;
2324
- }
2325
- $existing;
2326
-}
2327
-
2328
-sub init_remote_config {
2329
- my ($self, $url, $no_write) = @_;
2330
- $url =~ s!/+$!!; # strip trailing slash
2331
- my $r = read_all_remotes();
2332
- my $existing = find_existing_remote($url, $r);
2333
- if ($existing) {
2334
- unless ($no_write) {
2335
- print STDERR "Using existing ",
2336
- "[svn-remote \"$existing\"]\n";
2337
- }
2338
- $self->{repo_id} = $existing;
2339
- } elsif ($_minimize_url) {
2340
- my $min_url = Git::SVN::Ra->new($url)->minimize_url;
2341
- $existing = find_existing_remote($min_url, $r);
2342
- if ($existing) {
2343
- unless ($no_write) {
2344
- print STDERR "Using existing ",
2345
- "[svn-remote \"$existing\"]\n";
2346
- }
2347
- $self->{repo_id} = $existing;
2348
- }
2349
- if ($min_url ne $url) {
2350
- unless ($no_write) {
2351
- print STDERR "Using higher level of URL: ",
2352
- "$url => $min_url\n";
2353
- }
2354
- my $old_path = $self->{path};
2355
- $self->{path} = $url;
2356
- $self->{path} =~ s!^\Q$min_url\E(/|$)!!;
2357
- if (length $old_path) {
2358
- $self->{path} .= "/$old_path";
2359
- }
2360
- $url = $min_url;
2361
- }
2362
- }
2363
- my $orig_url;
2364
- if (!$existing) {
2365
- # verify that we aren't overwriting anything:
2366
- $orig_url = eval {
2367
- command_oneline('config', '--get',
2368
- "svn-remote.$self->{repo_id}.url")
2369
- };
2370
- if ($orig_url && ($orig_url ne $url)) {
2371
- die "svn-remote.$self->{repo_id}.url already set: ",
2372
- "$orig_url\nwanted to set to: $url\n";
2373
- }
2374
- }
2375
- my ($xrepo_id, $xpath) = find_ref($self->refname);
2376
- if (!$no_write && defined $xpath) {
2377
- die "svn-remote.$xrepo_id.fetch already set to track ",
2378
- "$xpath:", $self->refname, "\n";
2379
- }
2380
- unless ($no_write) {
2381
- command_noisy('config',
2382
- "svn-remote.$self->{repo_id}.url", $url);
2383
- $self->{path} =~ s{^/}{};
2384
- $self->{path} =~ s{%([0-9A-F]{2})}{chr hex($1)}ieg;
2385
- command_noisy('config', '--add',
2386
- "svn-remote.$self->{repo_id}.fetch",
2387
- "$self->{path}:".$self->refname);
2388
- }
2389
- $self->{url} = $url;
2390
-}
2391
-
2392
-sub find_by_url { # repos_root and, path are optional
2393
- my ($class, $full_url, $repos_root, $path) = @_;
2394
-
2395
- return undef unless defined $full_url;
2396
- remove_username($full_url);
2397
- remove_username($repos_root) if defined $repos_root;
2398
- my $remotes = read_all_remotes();
2399
- if (defined $full_url && defined $repos_root && !defined $path) {
2400
- $path = $full_url;
2401
- $path =~ s#^\Q$repos_root\E(?:/|$)##;
2402
- }
2403
- foreach my $repo_id (keys %$remotes) {
2404
- my $u = $remotes->{$repo_id}->{url} or next;
2405
- remove_username($u);
2406
- next if defined $repos_root && $repos_root ne $u;
2407
-
2408
- my $fetch = $remotes->{$repo_id}->{fetch} || {};
2409
- foreach my $t (qw/branches tags/) {
2410
- foreach my $globspec (@{$remotes->{$repo_id}->{$t}}) {
2411
- resolve_local_globs($u, $fetch, $globspec);
2412
- }
2413
- }
2414
- my $p = $path;
2415
- my $rwr = rewrite_root({repo_id => $repo_id});
2416
- my $svm = $remotes->{$repo_id}->{svm}
2417
- if defined $remotes->{$repo_id}->{svm};
2418
- unless (defined $p) {
2419
- $p = $full_url;
2420
- my $z = $u;
2421
- my $prefix = '';
2422
- if ($rwr) {
2423
- $z = $rwr;
2424
- remove_username($z);
2425
- } elsif (defined $svm) {
2426
- $z = $svm->{source};
2427
- $prefix = $svm->{replace};
2428
- $prefix =~ s#^\Q$u\E(?:/|$)##;
2429
- $prefix =~ s#/$##;
2430
- }
2431
- $p =~ s#^\Q$z\E(?:/|$)#$prefix# or next;
2432
- }
2433
- foreach my $f (keys %$fetch) {
2434
- next if $f ne $p;
2435
- return Git::SVN->new($fetch->{$f}, $repo_id, $f);
2436
- }
2437
- }
2438
- undef;
2439
-}
2440
-
2441
-sub init {
2442
- my ($class, $url, $path, $repo_id, $ref_id, $no_write) = @_;
2443
- my $self = _new($class, $repo_id, $ref_id, $path);
2444
- if (defined $url) {
2445
- $self->init_remote_config($url, $no_write);
2446
- }
2447
- $self;
2448
-}
2449
-
2450
-sub find_ref {
2451
- my ($ref_id) = @_;
2452
- foreach (command(qw/config -l/)) {
2453
- next unless m!^svn-remote\.(.+)\.fetch=
2454
- \s*(.*?)\s*:\s*(.+?)\s*$!x;
2455
- my ($repo_id, $path, $ref) = ($1, $2, $3);
2456
- if ($ref eq $ref_id) {
2457
- $path = '' if ($path =~ m#^\./?#);
2458
- return ($repo_id, $path);
2459
- }
2460
- }
2461
- (undef, undef, undef);
2462
-}
2463
-
2464
-sub new {
2465
- my ($class, $ref_id, $repo_id, $path) = @_;
2466
- if (defined $ref_id && !defined $repo_id && !defined $path) {
2467
- ($repo_id, $path) = find_ref($ref_id);
2468
- if (!defined $repo_id) {
2469
- die "Could not find a \"svn-remote.*.fetch\" key ",
2470
- "in the repository configuration matching: ",
2471
- "$ref_id\n";
2472
- }
2473
- }
2474
- my $self = _new($class, $repo_id, $ref_id, $path);
2475
- if (!defined $self->{path} || !length $self->{path}) {
2476
- my $fetch = command_oneline('config', '--get',
2477
- "svn-remote.$repo_id.fetch",
2478
- ":$ref_id\$") or
2479
- die "Failed to read \"svn-remote.$repo_id.fetch\" ",
2480
- "\":$ref_id\$\" in config\n";
2481
- ($self->{path}, undef) = split(/\s*:\s*/, $fetch);
2482
- }
2483
- $self->{path} =~ s{/+}{/}g;
2484
- $self->{path} =~ s{\A/}{};
2485
- $self->{path} =~ s{/\z}{};
2486
- $self->{url} = command_oneline('config', '--get',
2487
- "svn-remote.$repo_id.url") or
2488
- die "Failed to read \"svn-remote.$repo_id.url\" in config\n";
2489
- $self->{pushurl} = eval { command_oneline('config', '--get',
2490
- "svn-remote.$repo_id.pushurl") };
2491
- $self->rebuild;
2492
- $self;
2493
-}
2494
-
2495
-sub refname {
2496
- my ($refname) = $_[0]->{ref_id} ;
2497
-
2498
- # It cannot end with a slash /, we'll throw up on this because
2499
- # SVN can't have directories with a slash in their name, either:
2500
- if ($refname =~ m{/$}) {
2501
- die "ref: '$refname' ends with a trailing slash, this is ",
2502
- "not permitted by git nor Subversion\n";
2503
- }
2504
-
2505
- # It cannot have ASCII control character space, tilde ~, caret ^,
2506
- # colon :, question-mark ?, asterisk *, space, or open bracket [
2507
- # anywhere.
2508
- #
2509
- # Additionally, % must be escaped because it is used for escaping
2510
- # and we want our escaped refname to be reversible
2511
- $refname =~ s{([ \%~\^:\?\*\[\t])}{uc sprintf('%%%02x',ord($1))}eg;
2512
-
2513
- # no slash-separated component can begin with a dot .
2514
- # /.* becomes /%2E*
2515
- $refname =~ s{/\.}{/%2E}g;
2516
-
2517
- # It cannot have two consecutive dots .. anywhere
2518
- # .. becomes %2E%2E
2519
- $refname =~ s{\.\.}{%2E%2E}g;
2520
-
2521
- # trailing dots and .lock are not allowed
2522
- # .$ becomes %2E and .lock becomes %2Elock
2523
- $refname =~ s{\.(?=$|lock$)}{%2E};
2524
-
2525
- # the sequence @{ is used to access the reflog
2526
- # @{ becomes %40{
2527
- $refname =~ s{\@\{}{%40\{}g;
2528
-
2529
- return $refname;
2530
-}
2531
-
2532
-sub desanitize_refname {
2533
- my ($refname) = @_;
2534
- $refname =~ s{%(?:([0-9A-F]{2}))}{chr hex($1)}eg;
2535
- return $refname;
2536
-}
2537
-
2538
-sub svm_uuid {
2539
- my ($self) = @_;
2540
- return $self->{svm}->{uuid} if $self->svm;
2541
- $self->ra;
2542
- unless ($self->{svm}) {
2543
- die "SVM UUID not cached, and reading remotely failed\n";
2544
- }
2545
- $self->{svm}->{uuid};
2546
-}
2547
-
2548
-sub svm {
2549
- my ($self) = @_;
2550
- return $self->{svm} if $self->{svm};
2551
- my $svm;
2552
- # see if we have it in our config, first:
2553
- eval {
2554
- my $section = "svn-remote.$self->{repo_id}";
2555
- $svm = {
2556
- source => tmp_config('--get', "$section.svm-source"),
2557
- uuid => tmp_config('--get', "$section.svm-uuid"),
2558
- replace => tmp_config('--get', "$section.svm-replace"),
2559
- }
2560
- };
2561
- if ($svm && $svm->{source} && $svm->{uuid} && $svm->{replace}) {
2562
- $self->{svm} = $svm;
2563
- }
2564
- $self->{svm};
2565
-}
2566
-
2567
-sub _set_svm_vars {
2568
- my ($self, $ra) = @_;
2569
- return $ra if $self->svm;
2570
-
2571
- my @err = ( "useSvmProps set, but failed to read SVM properties\n",
2572
- "(svm:source, svm:uuid) ",
2573
- "from the following URLs:\n" );
2574
- sub read_svm_props {
2575
- my ($self, $ra, $path, $r) = @_;
2576
- my $props = ($ra->get_dir($path, $r))[2];
2577
- my $src = $props->{'svm:source'};
2578
- my $uuid = $props->{'svm:uuid'};
2579
- return undef if (!$src || !$uuid);
2580
-
2581
- chomp($src, $uuid);
2582
-
2583
- $uuid =~ m{^[0-9a-f\-]{30,}$}i
2584
- or die "doesn't look right - svm:uuid is '$uuid'\n";
2585
-
2586
- # the '!' is used to mark the repos_root!/relative/path
2587
- $src =~ s{/?!/?}{/};
2588
- $src =~ s{/+$}{}; # no trailing slashes please
2589
- # username is of no interest
2590
- $src =~ s{(^[a-z\+]*://)[^/@]*@}{$1};
2591
-
2592
- my $replace = $ra->{url};
2593
- $replace .= "/$path" if length $path;
2594
-
2595
- my $section = "svn-remote.$self->{repo_id}";
2596
- tmp_config("$section.svm-source", $src);
2597
- tmp_config("$section.svm-replace", $replace);
2598
- tmp_config("$section.svm-uuid", $uuid);
2599
- $self->{svm} = {
2600
- source => $src,
2601
- uuid => $uuid,
2602
- replace => $replace
2603
- };
2604
- }
2605
-
2606
- my $r = $ra->get_latest_revnum;
2607
- my $path = $self->{path};
2608
- my %tried;
2609
- while (length $path) {
2610
- unless ($tried{"$self->{url}/$path"}) {
2611
- return $ra if $self->read_svm_props($ra, $path, $r);
2612
- $tried{"$self->{url}/$path"} = 1;
2613
- }
2614
- $path =~ s#/?[^/]+$##;
2615
- }
2616
- die "Path: '$path' should be ''\n" if $path ne '';
2617
- return $ra if $self->read_svm_props($ra, $path, $r);
2618
- $tried{"$self->{url}/$path"} = 1;
2619
-
2620
- if ($ra->{repos_root} eq $self->{url}) {
2621
- die @err, (map { " $_\n" } keys %tried), "\n";
2622
- }
2623
-
2624
- # nope, make sure we're connected to the repository root:
2625
- my $ok;
2626
- my @tried_b;
2627
- $path = $ra->{svn_path};
2628
- $ra = Git::SVN::Ra->new($ra->{repos_root});
2629
- while (length $path) {
2630
- unless ($tried{"$ra->{url}/$path"}) {
2631
- $ok = $self->read_svm_props($ra, $path, $r);
2632
- last if $ok;
2633
- $tried{"$ra->{url}/$path"} = 1;
2634
- }
2635
- $path =~ s#/?[^/]+$##;
2636
- }
2637
- die "Path: '$path' should be ''\n" if $path ne '';
2638
- $ok ||= $self->read_svm_props($ra, $path, $r);
2639
- $tried{"$ra->{url}/$path"} = 1;
2640
- if (!$ok) {
2641
- die @err, (map { " $_\n" } keys %tried), "\n";
2642
- }
2643
- Git::SVN::Ra->new($self->{url});
2644
-}
2645
-
2646
-sub svnsync {
2647
- my ($self) = @_;
2648
- return $self->{svnsync} if $self->{svnsync};
2649
-
2650
- if ($self->no_metadata) {
2651
- die "Can't have both 'noMetadata' and ",
2652
- "'useSvnsyncProps' options set!\n";
2653
- }
2654
- if ($self->rewrite_root) {
2655
- die "Can't have both 'useSvnsyncProps' and 'rewriteRoot' ",
2656
- "options set!\n";
2657
- }
2658
- if ($self->rewrite_uuid) {
2659
- die "Can't have both 'useSvnsyncProps' and 'rewriteUUID' ",
2660
- "options set!\n";
2661
- }
2662
-
2663
- my $svnsync;
2664
- # see if we have it in our config, first:
2665
- eval {
2666
- my $section = "svn-remote.$self->{repo_id}";
2667
-
2668
- my $url = tmp_config('--get', "$section.svnsync-url");
2669
- ($url) = ($url =~ m{^([a-z\+]+://\S+)$}) or
2670
- die "doesn't look right - svn:sync-from-url is '$url'\n";
2671
-
2672
- my $uuid = tmp_config('--get', "$section.svnsync-uuid");
2673
- ($uuid) = ($uuid =~ m{^([0-9a-f\-]{30,})$}i) or
2674
- die "doesn't look right - svn:sync-from-uuid is '$uuid'\n";
2675
-
2676
- $svnsync = { url => $url, uuid => $uuid }
2677
- };
2678
- if ($svnsync && $svnsync->{url} && $svnsync->{uuid}) {
2679
- return $self->{svnsync} = $svnsync;
2680
- }
2681
-
2682
- my $err = "useSvnsyncProps set, but failed to read " .
2683
- "svnsync property: svn:sync-from-";
2684
- my $rp = $self->ra->rev_proplist(0);
2685
-
2686
- my $url = $rp->{'svn:sync-from-url'} or die $err . "url\n";
2687
- ($url) = ($url =~ m{^([a-z\+]+://\S+)$}) or
2688
- die "doesn't look right - svn:sync-from-url is '$url'\n";
2689
-
2690
- my $uuid = $rp->{'svn:sync-from-uuid'} or die $err . "uuid\n";
2691
- ($uuid) = ($uuid =~ m{^([0-9a-f\-]{30,})$}i) or
2692
- die "doesn't look right - svn:sync-from-uuid is '$uuid'\n";
2693
-
2694
- my $section = "svn-remote.$self->{repo_id}";
2695
- tmp_config('--add', "$section.svnsync-uuid", $uuid);
2696
- tmp_config('--add', "$section.svnsync-url", $url);
2697
- return $self->{svnsync} = { url => $url, uuid => $uuid };
2698
-}
2699
-
2700
-# this allows us to memoize our SVN::Ra UUID locally and avoid a
2701
-# remote lookup (useful for 'git svn log').
2702
-sub ra_uuid {
2703
- my ($self) = @_;
2704
- unless ($self->{ra_uuid}) {
2705
- my $key = "svn-remote.$self->{repo_id}.uuid";
2706
- my $uuid = eval { tmp_config('--get', $key) };
2707
- if (!$@ && $uuid && $uuid =~ /^([a-f\d\-]{30,})$/i) {
2708
- $self->{ra_uuid} = $uuid;
2709
- } else {
2710
- die "ra_uuid called without URL\n" unless $self->{url};
2711
- $self->{ra_uuid} = $self->ra->get_uuid;
2712
- tmp_config('--add', $key, $self->{ra_uuid});
2713
- }
2714
- }
2715
- $self->{ra_uuid};
2716
-}
2717
-
2718
-sub _set_repos_root {
2719
- my ($self, $repos_root) = @_;
2720
- my $k = "svn-remote.$self->{repo_id}.reposRoot";
2721
- $repos_root ||= $self->ra->{repos_root};
2722
- tmp_config($k, $repos_root);
2723
- $repos_root;
2724
-}
2725
-
2726
-sub repos_root {
2727
- my ($self) = @_;
2728
- my $k = "svn-remote.$self->{repo_id}.reposRoot";
2729
- eval { tmp_config('--get', $k) } || $self->_set_repos_root;
2730
-}
2731
-
2732
-sub ra {
2733
- my ($self) = shift;
2734
- my $ra = Git::SVN::Ra->new($self->{url});
2735
- $self->_set_repos_root($ra->{repos_root});
2736
- if ($self->use_svm_props && !$self->{svm}) {
2737
- if ($self->no_metadata) {
2738
- die "Can't have both 'noMetadata' and ",
2739
- "'useSvmProps' options set!\n";
2740
- } elsif ($self->use_svnsync_props) {
2741
- die "Can't have both 'useSvnsyncProps' and ",
2742
- "'useSvmProps' options set!\n";
2743
- }
2744
- $ra = $self->_set_svm_vars($ra);
2745
- $self->{-want_revprops} = 1;
2746
- }
2747
- $ra;
2748
-}
2749
-
2750
-# prop_walk(PATH, REV, SUB)
2751
-# -------------------------
2752
-# Recursively traverse PATH at revision REV and invoke SUB for each
2753
-# directory that contains a SVN property. SUB will be invoked as
2754
-# follows: &SUB(gs, path, props); where `gs' is this instance of
2755
-# Git::SVN, `path' the path to the directory where the properties
2756
-# `props' were found. The `path' will be relative to point of checkout,
2757
-# that is, if url://repo/trunk is the current Git branch, and that
2758
-# directory contains a sub-directory `d', SUB will be invoked with `/d/'
2759
-# as `path' (note the trailing `/').
2760
-sub prop_walk {
2761
- my ($self, $path, $rev, $sub) = @_;
2762
-
2763
- $path =~ s#^/##;
2764
- my ($dirent, undef, $props) = $self->ra->get_dir($path, $rev);
2765
- $path =~ s#^/*#/#g;
2766
- my $p = $path;
2767
- # Strip the irrelevant part of the path.
2768
- $p =~ s#^/+\Q$self->{path}\E(/|$)#/#;
2769
- # Ensure the path is terminated by a `/'.
2770
- $p =~ s#/*$#/#;
2771
-
2772
- # The properties contain all the internal SVN stuff nobody
2773
- # (usually) cares about.
2774
- my $interesting_props = 0;
2775
- foreach (keys %{$props}) {
2776
- # If it doesn't start with `svn:', it must be a
2777
- # user-defined property.
2778
- ++$interesting_props and next if $_ !~ /^svn:/;
2779
- # FIXME: Fragile, if SVN adds new public properties,
2780
- # this needs to be updated.
2781
- ++$interesting_props if /^svn:(?:ignore|keywords|executable
2782
- |eol-style|mime-type
2783
- |externals|needs-lock)$/x;
2784
- }
2785
- &$sub($self, $p, $props) if $interesting_props;
2786
-
2787
- foreach (sort keys %$dirent) {
2788
- next if $dirent->{$_}->{kind} != $SVN::Node::dir;
2789
- $self->prop_walk($self->{path} . $p . $_, $rev, $sub);
2790
- }
2791
-}
2792
-
2793
-sub last_rev { ($_[0]->last_rev_commit)[0] }
2794
-sub last_commit { ($_[0]->last_rev_commit)[1] }
2795
-
2796
-# returns the newest SVN revision number and newest commit SHA1
2797
-sub last_rev_commit {
2798
- my ($self) = @_;
2799
- if (defined $self->{last_rev} && defined $self->{last_commit}) {
2800
- return ($self->{last_rev}, $self->{last_commit});
2801
- }
2802
- my $c = ::verify_ref($self->refname.'^0');
2803
- if ($c && !$self->use_svm_props && !$self->no_metadata) {
2804
- my $rev = (::cmt_metadata($c))[1];
2805
- if (defined $rev) {
2806
- ($self->{last_rev}, $self->{last_commit}) = ($rev, $c);
2807
- return ($rev, $c);
2808
- }
2809
- }
2810
- my $map_path = $self->map_path;
2811
- unless (-e $map_path) {
2812
- ($self->{last_rev}, $self->{last_commit}) = (undef, undef);
2813
- return (undef, undef);
2814
- }
2815
- my ($rev, $commit) = $self->rev_map_max(1);
2816
- ($self->{last_rev}, $self->{last_commit}) = ($rev, $commit);
2817
- return ($rev, $commit);
2818
-}
2819
-
2820
-sub get_fetch_range {
2821
- my ($self, $min, $max) = @_;
2822
- $max ||= $self->ra->get_latest_revnum;
2823
- $min ||= $self->rev_map_max;
2824
- (++$min, $max);
2825
-}
2826
-
2827
-sub tmp_config {
2828
- my (@args) = @_;
2829
- my $old_def_config = "$ENV{GIT_DIR}/svn/config";
2830
- my $config = "$ENV{GIT_DIR}/svn/.metadata";
2831
- if (! -f $config && -f $old_def_config) {
2832
- rename $old_def_config, $config or
2833
- die "Failed rename $old_def_config => $config: $!\n";
2834
- }
2835
- my $old_config = $ENV{GIT_CONFIG};
2836
- $ENV{GIT_CONFIG} = $config;
2837
- $@ = undef;
2838
- my @ret = eval {
2839
- unless (-f $config) {
2840
- mkfile($config);
2841
- open my $fh, '>', $config or
2842
- die "Can't open $config: $!\n";
2843
- print $fh "; This file is used internally by ",
2844
- "git-svn\n" or die
2845
- "Couldn't write to $config: $!\n";
2846
- print $fh "; You should not have to edit it\n" or
2847
- die "Couldn't write to $config: $!\n";
2848
- close $fh or die "Couldn't close $config: $!\n";
2849
- }
2850
- command('config', @args);
2851
- };
2852
- my $err = $@;
2853
- if (defined $old_config) {
2854
- $ENV{GIT_CONFIG} = $old_config;
2855
- } else {
2856
- delete $ENV{GIT_CONFIG};
2857
- }
2858
- die $err if $err;
2859
- wantarray ? @ret : $ret[0];
2860
-}
2861
-
2862
-sub tmp_index_do {
2863
- my ($self, $sub) = @_;
2864
- my $old_index = $ENV{GIT_INDEX_FILE};
2865
- $ENV{GIT_INDEX_FILE} = $self->{index};
2866
- $@ = undef;
2867
- my @ret = eval {
2868
- my ($dir, $base) = ($self->{index} =~ m#^(.*?)/?([^/]+)$#);
2869
- mkpath([$dir]) unless -d $dir;
2870
- &$sub;
2871
- };
2872
- my $err = $@;
2873
- if (defined $old_index) {
2874
- $ENV{GIT_INDEX_FILE} = $old_index;
2875
- } else {
2876
- delete $ENV{GIT_INDEX_FILE};
2877
- }
2878
- die $err if $err;
2879
- wantarray ? @ret : $ret[0];
2880
-}
2881
-
2882
-sub assert_index_clean {
2883
- my ($self, $treeish) = @_;
2884
-
2885
- $self->tmp_index_do(sub {
2886
- command_noisy('read-tree', $treeish) unless -e $self->{index};
2887
- my $x = command_oneline('write-tree');
2888
- my ($y) = (command(qw/cat-file commit/, $treeish) =~
2889
- /^tree ($::sha1)/mo);
2890
- return if $y eq $x;
2891
-
2892
- warn "Index mismatch: $y != $x\nrereading $treeish\n";
2893
- unlink $self->{index} or die "unlink $self->{index}: $!\n";
2894
- command_noisy('read-tree', $treeish);
2895
- $x = command_oneline('write-tree');
2896
- if ($y ne $x) {
2897
- fatal "trees ($treeish) $y != $x\n",
2898
- "Something is seriously wrong...";
2899
- }
2900
- });
2901
-}
2902
-
2903
-sub get_commit_parents {
2904
- my ($self, $log_entry) = @_;
2905
- my (%seen, @ret, @tmp);
2906
- # legacy support for 'set-tree'; this is only used by set_tree_cb:
2907
- if (my $ip = $self->{inject_parents}) {
2908
- if (my $commit = delete $ip->{$log_entry->{revision}}) {
2909
- push @tmp, $commit;
2910
- }
2911
- }
2912
- if (my $cur = ::verify_ref($self->refname.'^0')) {
2913
- push @tmp, $cur;
2914
- }
2915
- if (my $ipd = $self->{inject_parents_dcommit}) {
2916
- if (my $commit = delete $ipd->{$log_entry->{revision}}) {
2917
- push @tmp, @$commit;
2918
- }
2919
- }
2920
- push @tmp, $_ foreach (@{$log_entry->{parents}}, @tmp);
2921
- while (my $p = shift @tmp) {
2922
- next if $seen{$p};
2923
- $seen{$p} = 1;
2924
- push @ret, $p;
2925
- }
2926
- @ret;
2927
-}
2928
-
2929
-sub rewrite_root {
2930
- my ($self) = @_;
2931
- return $self->{-rewrite_root} if exists $self->{-rewrite_root};
2932
- my $k = "svn-remote.$self->{repo_id}.rewriteRoot";
2933
- my $rwr = eval { command_oneline(qw/config --get/, $k) };
2934
- if ($rwr) {
2935
- $rwr =~ s#/+$##;
2936
- if ($rwr !~ m#^[a-z\+]+://#) {
2937
- die "$rwr is not a valid URL (key: $k)\n";
2938
- }
2939
- }
2940
- $self->{-rewrite_root} = $rwr;
2941
-}
2942
-
2943
-sub rewrite_uuid {
2944
- my ($self) = @_;
2945
- return $self->{-rewrite_uuid} if exists $self->{-rewrite_uuid};
2946
- my $k = "svn-remote.$self->{repo_id}.rewriteUUID";
2947
- my $rwid = eval { command_oneline(qw/config --get/, $k) };
2948
- if ($rwid) {
2949
- $rwid =~ s#/+$##;
2950
- if ($rwid !~ m#^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$#) {
2951
- die "$rwid is not a valid UUID (key: $k)\n";
2952
- }
2953
- }
2954
- $self->{-rewrite_uuid} = $rwid;
2955
-}
2956
-
2957
-sub metadata_url {
2958
- my ($self) = @_;
2959
- ($self->rewrite_root || $self->{url}) .
2960
- (length $self->{path} ? '/' . $self->{path} : '');
2961
-}
2962
-
2963
-sub full_url {
2964
- my ($self) = @_;
2965
- $self->{url} . (length $self->{path} ? '/' . $self->{path} : '');
2966
-}
2967
-
2968
-sub full_pushurl {
2969
- my ($self) = @_;
2970
- if ($self->{pushurl}) {
2971
- return $self->{pushurl} . (length $self->{path} ? '/' .
2972
- $self->{path} : '');
2973
- } else {
2974
- return $self->full_url;
2975
- }
2976
-}
2977
-
2978
-sub set_commit_header_env {
2979
- my ($log_entry) = @_;
2980
- my %env;
2981
- foreach my $ned (qw/NAME EMAIL DATE/) {
2982
- foreach my $ac (qw/AUTHOR COMMITTER/) {
2983
- $env{"GIT_${ac}_${ned}"} = $ENV{"GIT_${ac}_${ned}"};
2984
- }
2985
- }
2986
-
2987
- $ENV{GIT_AUTHOR_NAME} = $log_entry->{name};
2988
- $ENV{GIT_AUTHOR_EMAIL} = $log_entry->{email};
2989
- $ENV{GIT_AUTHOR_DATE} = $ENV{GIT_COMMITTER_DATE} = $log_entry->{date};
2990
-
2991
- $ENV{GIT_COMMITTER_NAME} = (defined $log_entry->{commit_name})
2992
- ? $log_entry->{commit_name}
2993
- : $log_entry->{name};
2994
- $ENV{GIT_COMMITTER_EMAIL} = (defined $log_entry->{commit_email})
2995
- ? $log_entry->{commit_email}
2996
- : $log_entry->{email};
2997
- \%env;
2998
-}
2999
-
3000
-sub restore_commit_header_env {
3001
- my ($env) = @_;
3002
- foreach my $ned (qw/NAME EMAIL DATE/) {
3003
- foreach my $ac (qw/AUTHOR COMMITTER/) {
3004
- my $k = "GIT_${ac}_${ned}";
3005
- if (defined $env->{$k}) {
3006
- $ENV{$k} = $env->{$k};
3007
- } else {
3008
- delete $ENV{$k};
3009
- }
3010
- }
3011
- }
3012
-}
3013
-
3014
-sub gc {
3015
- command_noisy('gc', '--auto');
3016
-};
3017
-
3018
-sub do_git_commit {
3019
- my ($self, $log_entry) = @_;
3020
- my $lr = $self->last_rev;
3021
- if (defined $lr && $lr >= $log_entry->{revision}) {
3022
- die "Last fetched revision of ", $self->refname,
3023
- " was r$lr, but we are about to fetch: ",
3024
- "r$log_entry->{revision}!\n";
3025
- }
3026
- if (my $c = $self->rev_map_get($log_entry->{revision})) {
3027
- croak "$log_entry->{revision} = $c already exists! ",
3028
- "Why are we refetching it?\n";
3029
- }
3030
- my $old_env = set_commit_header_env($log_entry);
3031
- my $tree = $log_entry->{tree};
3032
- if (!defined $tree) {
3033
- $tree = $self->tmp_index_do(sub {
3034
- command_oneline('write-tree') });
3035
- }
3036
- die "Tree is not a valid sha1: $tree\n" if $tree !~ /^$::sha1$/o;
3037
-
3038
- my @exec = ('git', 'commit-tree', $tree);
3039
- foreach ($self->get_commit_parents($log_entry)) {
3040
- push @exec, '-p', $_;
3041
- }
3042
- defined(my $pid = open3(my $msg_fh, my $out_fh, '>&STDERR', @exec))
3043
- or croak $!;
3044
- binmode $msg_fh;
3045
-
3046
- # we always get UTF-8 from SVN, but we may want our commits in
3047
- # a different encoding.
3048
- if (my $enc = Git::config('i18n.commitencoding')) {
3049
- require Encode;
3050
- Encode::from_to($log_entry->{log}, 'UTF-8', $enc);
3051
- }
3052
- print $msg_fh $log_entry->{log} or croak $!;
3053
- restore_commit_header_env($old_env);
3054
- unless ($self->no_metadata) {
3055
- print $msg_fh "\ngit-svn-id: $log_entry->{metadata}\n"
3056
- or croak $!;
3057
- }
3058
- $msg_fh->flush == 0 or croak $!;
3059
- close $msg_fh or croak $!;
3060
- chomp(my $commit = do { local $/; <$out_fh> });
3061
- close $out_fh or croak $!;
3062
- waitpid $pid, 0;
3063
- croak $? if $?;
3064
- if ($commit !~ /^$::sha1$/o) {
3065
- die "Failed to commit, invalid sha1: $commit\n";
3066
- }
3067
-
3068
- $self->rev_map_set($log_entry->{revision}, $commit, 1);
3069
-
3070
- $self->{last_rev} = $log_entry->{revision};
3071
- $self->{last_commit} = $commit;
3072
- print "r$log_entry->{revision}" unless $::_q > 1;
3073
- if (defined $log_entry->{svm_revision}) {
3074
- print " (\@$log_entry->{svm_revision})" unless $::_q > 1;
3075
- $self->rev_map_set($log_entry->{svm_revision}, $commit,
3076
- 0, $self->svm_uuid);
3077
- }
3078
- print " = $commit ($self->{ref_id})\n" unless $::_q > 1;
3079
- if (--$_gc_nr == 0) {
3080
- $_gc_nr = $_gc_period;
3081
- gc();
3082
- }
3083
- return $commit;
3084
-}
3085
-
3086
-sub match_paths {
3087
- my ($self, $paths, $r) = @_;
3088
- return 1 if $self->{path} eq '';
3089
- if (my $path = $paths->{"/$self->{path}"}) {
3090
- return ($path->{action} eq 'D') ? 0 : 1;
3091
- }
3092
- $self->{path_regex} ||= qr/^\/\Q$self->{path}\E\//;
3093
- if (grep /$self->{path_regex}/, keys %$paths) {
3094
- return 1;
3095
- }
3096
- my $c = '';
3097
- foreach (split m#/#, $self->{path}) {
3098
- $c .= "/$_";
3099
- next unless ($paths->{$c} &&
3100
- ($paths->{$c}->{action} =~ /^[AR]$/));
3101
- if ($self->ra->check_path($self->{path}, $r) ==
3102
- $SVN::Node::dir) {
3103
- return 1;
3104
- }
3105
- }
3106
- return 0;
3107
-}
3108
-
3109
-sub find_parent_branch {
3110
- my ($self, $paths, $rev) = @_;
3111
- return undef unless $self->follow_parent;
3112
- unless (defined $paths) {
3113
- my $err_handler = $SVN::Error::handler;
3114
- $SVN::Error::handler = \&Git::SVN::Ra::skip_unknown_revs;
3115
- $self->ra->get_log([$self->{path}], $rev, $rev, 0, 1, 1,
3116
- sub { $paths = $_[0] });
3117
- $SVN::Error::handler = $err_handler;
3118
- }
3119
- return undef unless defined $paths;
3120
-
3121
- # look for a parent from another branch:
3122
- my @b_path_components = split m#/#, $self->{path};
3123
- my @a_path_components;
3124
- my $i;
3125
- while (@b_path_components) {
3126
- $i = $paths->{'/'.join('/', @b_path_components)};
3127
- last if $i && defined $i->{copyfrom_path};
3128
- unshift(@a_path_components, pop(@b_path_components));
3129
- }
3130
- return undef unless defined $i && defined $i->{copyfrom_path};
3131
- my $branch_from = $i->{copyfrom_path};
3132
- if (@a_path_components) {
3133
- print STDERR "branch_from: $branch_from => ";
3134
- $branch_from .= '/'.join('/', @a_path_components);
3135
- print STDERR $branch_from, "\n";
3136
- }
3137
- my $r = $i->{copyfrom_rev};
3138
- my $repos_root = $self->ra->{repos_root};
3139
- my $url = $self->ra->{url};
3140
- my $new_url = $url . $branch_from;
3141
- print STDERR "Found possible branch point: ",
3142
- "$new_url => ", $self->full_url, ", $r\n"
3143
- unless $::_q > 1;
3144
- $branch_from =~ s#^/##;
3145
- my $gs = $self->other_gs($new_url, $url,
3146
- $branch_from, $r, $self->{ref_id});
3147
- my ($r0, $parent) = $gs->find_rev_before($r, 1);
3148
- {
3149
- my ($base, $head);
3150
- if (!defined $r0 || !defined $parent) {
3151
- ($base, $head) = parse_revision_argument(0, $r);
3152
- } else {
3153
- if ($r0 < $r) {
3154
- $gs->ra->get_log([$gs->{path}], $r0 + 1, $r, 1,
3155
- 0, 1, sub { $base = $_[1] - 1 });
3156
- }
3157
- }
3158
- if (defined $base && $base <= $r) {
3159
- $gs->fetch($base, $r);
3160
- }
3161
- ($r0, $parent) = $gs->find_rev_before($r, 1);
3162
- }
3163
- if (defined $r0 && defined $parent) {
3164
- print STDERR "Found branch parent: ($self->{ref_id}) $parent\n"
3165
- unless $::_q > 1;
3166
- my $ed;
3167
- if ($self->ra->can_do_switch) {
3168
- $self->assert_index_clean($parent);
3169
- print STDERR "Following parent with do_switch\n"
3170
- unless $::_q > 1;
3171
- # do_switch works with svn/trunk >= r22312, but that
3172
- # is not included with SVN 1.4.3 (the latest version
3173
- # at the moment), so we can't rely on it
3174
- $self->{last_rev} = $r0;
3175
- $self->{last_commit} = $parent;
3176
- $ed = Git::SVN::Fetcher->new($self, $gs->{path});
3177
- $gs->ra->gs_do_switch($r0, $rev, $gs,
3178
- $self->full_url, $ed)
3179
- or die "SVN connection failed somewhere...\n";
3180
- } elsif ($self->ra->trees_match($new_url, $r0,
3181
- $self->full_url, $rev)) {
3182
- print STDERR "Trees match:\n",
3183
- " $new_url\@$r0\n",
3184
- " ${\$self->full_url}\@$rev\n",
3185
- "Following parent with no changes\n"
3186
- unless $::_q > 1;
3187
- $self->tmp_index_do(sub {
3188
- command_noisy('read-tree', $parent);
3189
- });
3190
- $self->{last_commit} = $parent;
3191
- } else {
3192
- print STDERR "Following parent with do_update\n"
3193
- unless $::_q > 1;
3194
- $ed = Git::SVN::Fetcher->new($self);
3195
- $self->ra->gs_do_update($rev, $rev, $self, $ed)
3196
- or die "SVN connection failed somewhere...\n";
3197
- }
3198
- print STDERR "Successfully followed parent\n" unless $::_q > 1;
3199
- return $self->make_log_entry($rev, [$parent], $ed);
3200
- }
3201
- return undef;
3202
-}
3203
-
3204
-sub do_fetch {
3205
- my ($self, $paths, $rev) = @_;
3206
- my $ed;
3207
- my ($last_rev, @parents);
3208
- if (my $lc = $self->last_commit) {
3209
- # we can have a branch that was deleted, then re-added
3210
- # under the same name but copied from another path, in
3211
- # which case we'll have multiple parents (we don't
3212
- # want to break the original ref, nor lose copypath info):
3213
- if (my $log_entry = $self->find_parent_branch($paths, $rev)) {
3214
- push @{$log_entry->{parents}}, $lc;
3215
- return $log_entry;
3216
- }
3217
- $ed = Git::SVN::Fetcher->new($self);
3218
- $last_rev = $self->{last_rev};
3219
- $ed->{c} = $lc;
3220
- @parents = ($lc);
3221
- } else {
3222
- $last_rev = $rev;
3223
- if (my $log_entry = $self->find_parent_branch($paths, $rev)) {
3224
- return $log_entry;
3225
- }
3226
- $ed = Git::SVN::Fetcher->new($self);
3227
- }
3228
- unless ($self->ra->gs_do_update($last_rev, $rev, $self, $ed)) {
3229
- die "SVN connection failed somewhere...\n";
3230
- }
3231
- $self->make_log_entry($rev, \@parents, $ed);
3232
-}
3233
-
3234
-sub mkemptydirs {
3235
- my ($self, $r) = @_;
3236
-
3237
- sub scan {
3238
- my ($r, $empty_dirs, $line) = @_;
3239
- if (defined $r && $line =~ /^r(\d+)$/) {
3240
- return 0 if $1 > $r;
3241
- } elsif ($line =~ /^ \+empty_dir: (.+)$/) {
3242
- $empty_dirs->{$1} = 1;
3243
- } elsif ($line =~ /^ \-empty_dir: (.+)$/) {
3244
- my @d = grep {m[^\Q$1\E(/|$)]} (keys %$empty_dirs);
3245
- delete @$empty_dirs{@d};
3246
- }
3247
- 1; # continue
3248
- };
3249
-
3250
- my %empty_dirs = ();
3251
- my $gz_file = "$self->{dir}/unhandled.log.gz";
3252
- if (-f $gz_file) {
3253
- if (!can_compress()) {
3254
- warn "Compress::Zlib could not be found; ",
3255
- "empty directories in $gz_file will not be read\n";
3256
- } else {
3257
- my $gz = Compress::Zlib::gzopen($gz_file, "rb") or
3258
- die "Unable to open $gz_file: $!\n";
3259
- my $line;
3260
- while ($gz->gzreadline($line) > 0) {
3261
- scan($r, \%empty_dirs, $line) or last;
3262
- }
3263
- $gz->gzclose;
3264
- }
3265
- }
3266
-
3267
- if (open my $fh, '<', "$self->{dir}/unhandled.log") {
3268
- binmode $fh or croak "binmode: $!";
3269
- while (<$fh>) {
3270
- scan($r, \%empty_dirs, $_) or last;
3271
- }
3272
- close $fh;
3273
- }
3274
-
3275
- my $strip = qr/\A\Q$self->{path}\E(?:\/|$)/;
3276
- foreach my $d (sort keys %empty_dirs) {
3277
- $d = uri_decode($d);
3278
- $d =~ s/$strip//;
3279
- next unless length($d);
3280
- next if -d $d;
3281
- if (-e $d) {
3282
- warn "$d exists but is not a directory\n";
3283
- } else {
3284
- print "creating empty directory: $d\n";
3285
- mkpath([$d]);
3286
- }
3287
- }
3288
-}
3289
-
3290
-sub get_untracked {
3291
- my ($self, $ed) = @_;
3292
- my @out;
3293
- my $h = $ed->{empty};
3294
- foreach (sort keys %$h) {
3295
- my $act = $h->{$_} ? '+empty_dir' : '-empty_dir';
3296
- push @out, " $act: " . uri_encode($_);
3297
- warn "W: $act: $_\n";
3298
- }
3299
- foreach my $t (qw/dir_prop file_prop/) {
3300
- $h = $ed->{$t} or next;
3301
- foreach my $path (sort keys %$h) {
3302
- my $ppath = $path eq '' ? '.' : $path;
3303
- foreach my $prop (sort keys %{$h->{$path}}) {
3304
- next if $SKIP_PROP{$prop};
3305
- my $v = $h->{$path}->{$prop};
3306
- my $t_ppath_prop = "$t: " .
3307
- uri_encode($ppath) . ' ' .
3308
- uri_encode($prop);
3309
- if (defined $v) {
3310
- push @out, " +$t_ppath_prop " .
3311
- uri_encode($v);
3312
- } else {
3313
- push @out, " -$t_ppath_prop";
3314
- }
3315
- }
3316
- }
3317
- }
3318
- foreach my $t (qw/absent_file absent_directory/) {
3319
- $h = $ed->{$t} or next;
3320
- foreach my $parent (sort keys %$h) {
3321
- foreach my $path (sort @{$h->{$parent}}) {
3322
- push @out, " $t: " .
3323
- uri_encode("$parent/$path");
3324
- warn "W: $t: $parent/$path ",
3325
- "Insufficient permissions?\n";
3326
- }
3327
- }
3328
- }
3329
- \@out;
3330
-}
3331
-
3332
-sub get_tz {
3333
- # some systmes don't handle or mishandle %z, so be creative.
3334
- my $t = shift || time;
3335
- my $gm = timelocal(gmtime($t));
3336
- my $sign = qw( + + - )[ $t <=> $gm ];
3337
- return sprintf("%s%02d%02d", $sign, (gmtime(abs($t - $gm)))[2,1]);
3338
-}
3339
-
3340
-# parse_svn_date(DATE)
3341
-# --------------------
3342
-# Given a date (in UTC) from Subversion, return a string in the format
3343
-# "<TZ Offset> <local date/time>" that Git will use.
3344
-#
3345
-# By default the parsed date will be in UTC; if $Git::SVN::_localtime
3346
-# is true we'll convert it to the local timezone instead.
3347
-sub parse_svn_date {
3348
- my $date = shift || return '+0000 1970-01-01 00:00:00';
3349
- my ($Y,$m,$d,$H,$M,$S) = ($date =~ /^(\d{4})\-(\d\d)\-(\d\d)T
3350
- (\d\d)\:(\d\d)\:(\d\d)\.\d*Z$/x) or
3351
- croak "Unable to parse date: $date\n";
3352
- my $parsed_date; # Set next.
3353
-
3354
- if ($Git::SVN::_localtime) {
3355
- # Translate the Subversion datetime to an epoch time.
3356
- # Begin by switching ourselves to $date's timezone, UTC.
3357
- my $old_env_TZ = $ENV{TZ};
3358
- $ENV{TZ} = 'UTC';
3359
-
3360
- my $epoch_in_UTC =
3361
- POSIX::strftime('%s', $S, $M, $H, $d, $m - 1, $Y - 1900);
3362
-
3363
- # Determine our local timezone (including DST) at the
3364
- # time of $epoch_in_UTC. $Git::SVN::Log::TZ stored the
3365
- # value of TZ, if any, at the time we were run.
3366
- if (defined $Git::SVN::Log::TZ) {
3367
- $ENV{TZ} = $Git::SVN::Log::TZ;
3368
- } else {
3369
- delete $ENV{TZ};
3370
- }
3371
-
3372
- my $our_TZ = get_tz();
3373
-
3374
- # This converts $epoch_in_UTC into our local timezone.
3375
- my ($sec, $min, $hour, $mday, $mon, $year,
3376
- $wday, $yday, $isdst) = localtime($epoch_in_UTC);
3377
-
3378
- $parsed_date = sprintf('%s %04d-%02d-%02d %02d:%02d:%02d',
3379
- $our_TZ, $year + 1900, $mon + 1,
3380
- $mday, $hour, $min, $sec);
3381
-
3382
- # Reset us to the timezone in effect when we entered
3383
- # this routine.
3384
- if (defined $old_env_TZ) {
3385
- $ENV{TZ} = $old_env_TZ;
3386
- } else {
3387
- delete $ENV{TZ};
3388
- }
3389
- } else {
3390
- $parsed_date = "+0000 $Y-$m-$d $H:$M:$S";
3391
- }
3392
-
3393
- return $parsed_date;
3394
-}
3395
-
3396
-sub other_gs {
3397
- my ($self, $new_url, $url,
3398
- $branch_from, $r, $old_ref_id) = @_;
3399
- my $gs = Git::SVN->find_by_url($new_url, $url, $branch_from);
3400
- unless ($gs) {
3401
- my $ref_id = $old_ref_id;
3402
- $ref_id =~ s/\@\d+-*$//;
3403
- $ref_id .= "\@$r";
3404
- # just grow a tail if we're not unique enough :x
3405
- $ref_id .= '-' while find_ref($ref_id);
3406
- my ($u, $p, $repo_id) = ($new_url, '', $ref_id);
3407
- if ($u =~ s#^\Q$url\E(/|$)##) {
3408
- $p = $u;
3409
- $u = $url;
3410
- $repo_id = $self->{repo_id};
3411
- }
3412
- while (1) {
3413
- # It is possible to tag two different subdirectories at
3414
- # the same revision. If the url for an existing ref
3415
- # does not match, we must either find a ref with a
3416
- # matching url or create a new ref by growing a tail.
3417
- $gs = Git::SVN->init($u, $p, $repo_id, $ref_id, 1);
3418
- my (undef, $max_commit) = $gs->rev_map_max(1);
3419
- last if (!$max_commit);
3420
- my ($url) = ::cmt_metadata($max_commit);
3421
- last if ($url eq $gs->metadata_url);
3422
- $ref_id .= '-';
3423
- }
3424
- print STDERR "Initializing parent: $ref_id\n" unless $::_q > 1;
3425
- }
3426
- $gs
3427
-}
3428
-
3429
-sub call_authors_prog {
3430
- my ($orig_author) = @_;
3431
- $orig_author = command_oneline('rev-parse', '--sq-quote', $orig_author);
3432
- my $author = `$::_authors_prog $orig_author`;
3433
- if ($? != 0) {
3434
- die "$::_authors_prog failed with exit code $?\n"
3435
- }
3436
- if ($author =~ /^\s*(.+?)\s*<(.*)>\s*$/) {
3437
- my ($name, $email) = ($1, $2);
3438
- $email = undef if length $2 == 0;
3439
- return [$name, $email];
3440
- } else {
3441
- die "Author: $orig_author: $::_authors_prog returned "
3442
- . "invalid author format: $author\n";
3443
- }
3444
-}
3445
-
3446
-sub check_author {
3447
- my ($author) = @_;
3448
- if (!defined $author || length $author == 0) {
3449
- $author = '(no author)';
3450
- }
3451
- if (!defined $::users{$author}) {
3452
- if (defined $::_authors_prog) {
3453
- $::users{$author} = call_authors_prog($author);
3454
- } elsif (defined $::_authors) {
3455
- die "Author: $author not defined in $::_authors file\n";
3456
- }
3457
- }
3458
- $author;
3459
-}
3460
-
3461
-sub find_extra_svk_parents {
3462
- my ($self, $ed, $tickets, $parents) = @_;
3463
- # aha! svk:merge property changed...
3464
- my @tickets = split "\n", $tickets;
3465
- my @known_parents;
3466
- for my $ticket ( @tickets ) {
3467
- my ($uuid, $path, $rev) = split /:/, $ticket;
3468
- if ( $uuid eq $self->ra_uuid ) {
3469
- my $url = $self->{url};
3470
- my $repos_root = $url;
3471
- my $branch_from = $path;
3472
- $branch_from =~ s{^/}{};
3473
- my $gs = $self->other_gs($repos_root."/".$branch_from,
3474
- $url,
3475
- $branch_from,
3476
- $rev,
3477
- $self->{ref_id});
3478
- if ( my $commit = $gs->rev_map_get($rev, $uuid) ) {
3479
- # wahey! we found it, but it might be
3480
- # an old one (!)
3481
- push @known_parents, [ $rev, $commit ];
3482
- }
3483
- }
3484
- }
3485
- # Ordering matters; highest-numbered commit merge tickets
3486
- # first, as they may account for later merge ticket additions
3487
- # or changes.
3488
- @known_parents = map {$_->[1]} sort {$b->[0] <=> $a->[0]} @known_parents;
3489
- for my $parent ( @known_parents ) {
3490
- my @cmd = ('rev-list', $parent, map { "^$_" } @$parents );
3491
- my ($msg_fh, $ctx) = command_output_pipe(@cmd);
3492
- my $new;
3493
- while ( <$msg_fh> ) {
3494
- $new=1;last;
3495
- }
3496
- command_close_pipe($msg_fh, $ctx);
3497
- if ( $new ) {
3498
- print STDERR
3499
- "Found merge parent (svk:merge ticket): $parent\n";
3500
- push @$parents, $parent;
3501
- }
3502
- }
3503
-}
3504
-
3505
-sub lookup_svn_merge {
3506
- my $uuid = shift;
3507
- my $url = shift;
3508
- my $merge = shift;
3509
-
3510
- my ($source, $revs) = split ":", $merge;
3511
- my $path = $source;
3512
- $path =~ s{^/}{};
3513
- my $gs = Git::SVN->find_by_url($url.$source, $url, $path);
3514
- if ( !$gs ) {
3515
- warn "Couldn't find revmap for $url$source\n";
3516
- return;
3517
- }
3518
- my @ranges = split ",", $revs;
3519
- my ($tip, $tip_commit);
3520
- my @merged_commit_ranges;
3521
- # find the tip
3522
- for my $range ( @ranges ) {
3523
- my ($bottom, $top) = split "-", $range;
3524
- $top ||= $bottom;
3525
- my $bottom_commit = $gs->find_rev_after( $bottom, 1, $top );
3526
- my $top_commit = $gs->find_rev_before( $top, 1, $bottom );
3527
-
3528
- unless ($top_commit and $bottom_commit) {
3529
- warn "W:unknown path/rev in svn:mergeinfo "
3530
- ."dirprop: $source:$range\n";
3531
- next;
3532
- }
3533
-
3534
- if (scalar(command('rev-parse', "$bottom_commit^@"))) {
3535
- push @merged_commit_ranges,
3536
- "$bottom_commit^..$top_commit";
3537
- } else {
3538
- push @merged_commit_ranges, "$top_commit";
3539
- }
3540
-
3541
- if ( !defined $tip or $top > $tip ) {
3542
- $tip = $top;
3543
- $tip_commit = $top_commit;
3544
- }
3545
- }
3546
- return ($tip_commit, @merged_commit_ranges);
3547
-}
3548
-
3549
-sub _rev_list {
3550
- my ($msg_fh, $ctx) = command_output_pipe(
3551
- "rev-list", @_,
3552
- );
3553
- my @rv;
3554
- while ( <$msg_fh> ) {
3555
- chomp;
3556
- push @rv, $_;
3557
- }
3558
- command_close_pipe($msg_fh, $ctx);
3559
- @rv;
3560
-}
3561
-
3562
-sub check_cherry_pick {
3563
- my $base = shift;
3564
- my $tip = shift;
3565
- my $parents = shift;
3566
- my @ranges = @_;
3567
- my %commits = map { $_ => 1 }
3568
- _rev_list("--no-merges", $tip, "--not", $base, @$parents, "--");
3569
- for my $range ( @ranges ) {
3570
- delete @commits{_rev_list($range, "--")};
3571
- }
3572
- for my $commit (keys %commits) {
3573
- if (has_no_changes($commit)) {
3574
- delete $commits{$commit};
3575
- }
3576
- }
3577
- return (keys %commits);
3578
-}
3579
-
3580
-sub has_no_changes {
3581
- my $commit = shift;
3582
-
3583
- my @revs = split / /, command_oneline(
3584
- qw(rev-list --parents -1 -m), $commit);
3585
-
3586
- # Commits with no parents, e.g. the start of a partial branch,
3587
- # have changes by definition.
3588
- return 1 if (@revs < 2);
3589
-
3590
- # Commits with multiple parents, e.g a merge, have no changes
3591
- # by definition.
3592
- return 0 if (@revs > 2);
3593
-
3594
- return (command_oneline("rev-parse", "$commit^{tree}") eq
3595
- command_oneline("rev-parse", "$commit~1^{tree}"));
3596
-}
3597
-
3598
-sub tie_for_persistent_memoization {
3599
- my $hash = shift;
3600
- my $path = shift;
3601
-
3602
- if ($can_use_yaml) {
3603
- tie %$hash => 'Git::SVN::Memoize::YAML', "$path.yaml";
3604
- } else {
3605
- tie %$hash => 'Memoize::Storable', "$path.db", 'nstore';
3606
- }
3607
-}
3608
-
3609
-# The GIT_DIR environment variable is not always set until after the command
3610
-# line arguments are processed, so we can't memoize in a BEGIN block.
3611
-{
3612
- my $memoized = 0;
3613
-
3614
- sub memoize_svn_mergeinfo_functions {
3615
- return if $memoized;
3616
- $memoized = 1;
3617
-
3618
- my $cache_path = "$ENV{GIT_DIR}/svn/.caches/";
3619
- mkpath([$cache_path]) unless -d $cache_path;
3620
-
3621
- my %lookup_svn_merge_cache;
3622
- my %check_cherry_pick_cache;
3623
- my %has_no_changes_cache;
3624
-
3625
- tie_for_persistent_memoization(\%lookup_svn_merge_cache,
3626
- "$cache_path/lookup_svn_merge");
3627
- memoize 'lookup_svn_merge',
3628
- SCALAR_CACHE => 'FAULT',
3629
- LIST_CACHE => ['HASH' => \%lookup_svn_merge_cache],
3630
- ;
3631
-
3632
- tie_for_persistent_memoization(\%check_cherry_pick_cache,
3633
- "$cache_path/check_cherry_pick");
3634
- memoize 'check_cherry_pick',
3635
- SCALAR_CACHE => 'FAULT',
3636
- LIST_CACHE => ['HASH' => \%check_cherry_pick_cache],
3637
- ;
3638
-
3639
- tie_for_persistent_memoization(\%has_no_changes_cache,
3640
- "$cache_path/has_no_changes");
3641
- memoize 'has_no_changes',
3642
- SCALAR_CACHE => ['HASH' => \%has_no_changes_cache],
3643
- LIST_CACHE => 'FAULT',
3644
- ;
3645
- }
3646
-
3647
- sub unmemoize_svn_mergeinfo_functions {
3648
- return if not $memoized;
3649
- $memoized = 0;
3650
-
3651
- Memoize::unmemoize 'lookup_svn_merge';
3652
- Memoize::unmemoize 'check_cherry_pick';
3653
- Memoize::unmemoize 'has_no_changes';
3654
- }
3655
-
3656
- Memoize::memoize 'Git::SVN::repos_root';
3657
-}
3658
-
3659
-END {
3660
- # Force cache writeout explicitly instead of waiting for
3661
- # global destruction to avoid segfault in Storable:
3662
- # http://rt.cpan.org/Public/Bug/Display.html?id=36087
3663
- unmemoize_svn_mergeinfo_functions();
3664
-}
3665
-
3666
-sub parents_exclude {
3667
- my $parents = shift;
3668
- my @commits = @_;
3669
- return unless @commits;
3670
-
3671
- my @excluded;
3672
- my $excluded;
3673
- do {
3674
- my @cmd = ('rev-list', "-1", @commits, "--not", @$parents );
3675
- $excluded = command_oneline(@cmd);
3676
- if ( $excluded ) {
3677
- my @new;
3678
- my $found;
3679
- for my $commit ( @commits ) {
3680
- if ( $commit eq $excluded ) {
3681
- push @excluded, $commit;
3682
- $found++;
3683
- last;
3684
- }
3685
- else {
3686
- push @new, $commit;
3687
- }
3688
- }
3689
- die "saw commit '$excluded' in rev-list output, "
3690
- ."but we didn't ask for that commit (wanted: @commits --not @$parents)"
3691
- unless $found;
3692
- @commits = @new;
3693
- }
3694
- }
3695
- while ($excluded and @commits);
3696
-
3697
- return @excluded;
3698
-}
3699
-
3700
-
3701
-# note: this function should only be called if the various dirprops
3702
-# have actually changed
3703
-sub find_extra_svn_parents {
3704
- my ($self, $ed, $mergeinfo, $parents) = @_;
3705
- # aha! svk:merge property changed...
3706
-
3707
- memoize_svn_mergeinfo_functions();
3708
-
3709
- # We first search for merged tips which are not in our
3710
- # history. Then, we figure out which git revisions are in
3711
- # that tip, but not this revision. If all of those revisions
3712
- # are now marked as merge, we can add the tip as a parent.
3713
- my @merges = split "\n", $mergeinfo;
3714
- my @merge_tips;
3715
- my $url = $self->{url};
3716
- my $uuid = $self->ra_uuid;
3717
- my %ranges;
3718
- for my $merge ( @merges ) {
3719
- my ($tip_commit, @ranges) =
3720
- lookup_svn_merge( $uuid, $url, $merge );
3721
- unless (!$tip_commit or
3722
- grep { $_ eq $tip_commit } @$parents ) {
3723
- push @merge_tips, $tip_commit;
3724
- $ranges{$tip_commit} = \@ranges;
3725
- } else {
3726
- push @merge_tips, undef;
3727
- }
3728
- }
3729
-
3730
- my %excluded = map { $_ => 1 }
3731
- parents_exclude($parents, grep { defined } @merge_tips);
3732
-
3733
- # check merge tips for new parents
3734
- my @new_parents;
3735
- for my $merge_tip ( @merge_tips ) {
3736
- my $spec = shift @merges;
3737
- next unless $merge_tip and $excluded{$merge_tip};
3738
-
3739
- my $ranges = $ranges{$merge_tip};
3740
-
3741
- # check out 'new' tips
3742
- my $merge_base;
3743
- eval {
3744
- $merge_base = command_oneline(
3745
- "merge-base",
3746
- @$parents, $merge_tip,
3747
- );
3748
- };
3749
- if ($@) {
3750
- die "An error occurred during merge-base"
3751
- unless $@->isa("Git::Error::Command");
3752
-
3753
- warn "W: Cannot find common ancestor between ".
3754
- "@$parents and $merge_tip. Ignoring merge info.\n";
3755
- next;
3756
- }
3757
-
3758
- # double check that there are no missing non-merge commits
3759
- my (@incomplete) = check_cherry_pick(
3760
- $merge_base, $merge_tip,
3761
- $parents,
3762
- @$ranges,
3763
- );
3764
-
3765
- if ( @incomplete ) {
3766
- warn "W:svn cherry-pick ignored ($spec) - missing "
3767
- .@incomplete." commit(s) (eg $incomplete[0])\n";
3768
- } else {
3769
- warn
3770
- "Found merge parent (svn:mergeinfo prop): ",
3771
- $merge_tip, "\n";
3772
- push @new_parents, $merge_tip;
3773
- }
3774
- }
3775
-
3776
- # cater for merges which merge commits from multiple branches
3777
- if ( @new_parents > 1 ) {
3778
- for ( my $i = 0; $i <= $#new_parents; $i++ ) {
3779
- for ( my $j = 0; $j <= $#new_parents; $j++ ) {
3780
- next if $i == $j;
3781
- next unless $new_parents[$i];
3782
- next unless $new_parents[$j];
3783
- my $revs = command_oneline(
3784
- "rev-list", "-1",
3785
- "$new_parents[$i]..$new_parents[$j]",
3786
- );
3787
- if ( !$revs ) {
3788
- undef($new_parents[$j]);
3789
- }
3790
- }
3791
- }
3792
- }
3793
- push @$parents, grep { defined } @new_parents;
3794
-}
3795
-
3796
-sub make_log_entry {
3797
- my ($self, $rev, $parents, $ed) = @_;
3798
- my $untracked = $self->get_untracked($ed);
3799
-
3800
- my @parents = @$parents;
3801
- my $ps = $ed->{path_strip} || "";
3802
- for my $path ( grep { m/$ps/ } %{$ed->{dir_prop}} ) {
3803
- my $props = $ed->{dir_prop}{$path};
3804
- if ( $props->{"svk:merge"} ) {
3805
- $self->find_extra_svk_parents
3806
- ($ed, $props->{"svk:merge"}, \@parents);
3807
- }
3808
- if ( $props->{"svn:mergeinfo"} ) {
3809
- $self->find_extra_svn_parents
3810
- ($ed,
3811
- $props->{"svn:mergeinfo"},
3812
- \@parents);
3813
- }
3814
- }
3815
-
3816
- open my $un, '>>', "$self->{dir}/unhandled.log" or croak $!;
3817
- print $un "r$rev\n" or croak $!;
3818
- print $un $_, "\n" foreach @$untracked;
3819
- my %log_entry = ( parents => \@parents, revision => $rev,
3820
- log => '');
3821
-
3822
- my $headrev;
3823
- my $logged = delete $self->{logged_rev_props};
3824
- if (!$logged || $self->{-want_revprops}) {
3825
- my $rp = $self->ra->rev_proplist($rev);
3826
- foreach (sort keys %$rp) {
3827
- my $v = $rp->{$_};
3828
- if (/^svn:(author|date|log)$/) {
3829
- $log_entry{$1} = $v;
3830
- } elsif ($_ eq 'svm:headrev') {
3831
- $headrev = $v;
3832
- } else {
3833
- print $un " rev_prop: ", uri_encode($_), ' ',
3834
- uri_encode($v), "\n";
3835
- }
3836
- }
3837
- } else {
3838
- map { $log_entry{$_} = $logged->{$_} } keys %$logged;
3839
- }
3840
- close $un or croak $!;
3841
-
3842
- $log_entry{date} = parse_svn_date($log_entry{date});
3843
- $log_entry{log} .= "\n";
3844
- my $author = $log_entry{author} = check_author($log_entry{author});
3845
- my ($name, $email) = defined $::users{$author} ? @{$::users{$author}}
3846
- : ($author, undef);
3847
-
3848
- my ($commit_name, $commit_email) = ($name, $email);
3849
- if ($_use_log_author) {
3850
- my $name_field;
3851
- if ($log_entry{log} =~ /From:\s+(.*\S)\s*\n/i) {
3852
- $name_field = $1;
3853
- } elsif ($log_entry{log} =~ /Signed-off-by:\s+(.*\S)\s*\n/i) {
3854
- $name_field = $1;
3855
- }
3856
- if (!defined $name_field) {
3857
- if (!defined $email) {
3858
- $email = $name;
3859
- }
3860
- } elsif ($name_field =~ /(.*?)\s+<(.*)>/) {
3861
- ($name, $email) = ($1, $2);
3862
- } elsif ($name_field =~ /(.*)@/) {
3863
- ($name, $email) = ($1, $name_field);
3864
- } else {
3865
- ($name, $email) = ($name_field, $name_field);
3866
- }
3867
- }
3868
- if (defined $headrev && $self->use_svm_props) {
3869
- if ($self->rewrite_root) {
3870
- die "Can't have both 'useSvmProps' and 'rewriteRoot' ",
3871
- "options set!\n";
3872
- }
3873
- if ($self->rewrite_uuid) {
3874
- die "Can't have both 'useSvmProps' and 'rewriteUUID' ",
3875
- "options set!\n";
3876
- }
3877
- my ($uuid, $r) = $headrev =~ m{^([a-f\d\-]{30,}):(\d+)$}i;
3878
- # we don't want "SVM: initializing mirror for junk" ...
3879
- return undef if $r == 0;
3880
- my $svm = $self->svm;
3881
- if ($uuid ne $svm->{uuid}) {
3882
- die "UUID mismatch on SVM path:\n",
3883
- "expected: $svm->{uuid}\n",
3884
- " got: $uuid\n";
3885
- }
3886
- my $full_url = $self->full_url;
3887
- $full_url =~ s#^\Q$svm->{replace}\E(/|$)#$svm->{source}$1# or
3888
- die "Failed to replace '$svm->{replace}' with ",
3889
- "'$svm->{source}' in $full_url\n";
3890
- # throw away username for storing in records
3891
- remove_username($full_url);
3892
- $log_entry{metadata} = "$full_url\@$r $uuid";
3893
- $log_entry{svm_revision} = $r;
3894
- $email ||= "$author\@$uuid";
3895
- $commit_email ||= "$author\@$uuid";
3896
- } elsif ($self->use_svnsync_props) {
3897
- my $full_url = $self->svnsync->{url};
3898
- $full_url .= "/$self->{path}" if length $self->{path};
3899
- remove_username($full_url);
3900
- my $uuid = $self->svnsync->{uuid};
3901
- $log_entry{metadata} = "$full_url\@$rev $uuid";
3902
- $email ||= "$author\@$uuid";
3903
- $commit_email ||= "$author\@$uuid";
3904
- } else {
3905
- my $url = $self->metadata_url;
3906
- remove_username($url);
3907
- my $uuid = $self->rewrite_uuid || $self->ra->get_uuid;
3908
- $log_entry{metadata} = "$url\@$rev " . $uuid;
3909
- $email ||= "$author\@" . $uuid;
3910
- $commit_email ||= "$author\@" . $uuid;
3911
- }
3912
- $log_entry{name} = $name;
3913
- $log_entry{email} = $email;
3914
- $log_entry{commit_name} = $commit_name;
3915
- $log_entry{commit_email} = $commit_email;
3916
- \%log_entry;
3917
-}
3918
-
3919
-sub fetch {
3920
- my ($self, $min_rev, $max_rev, @parents) = @_;
3921
- my ($last_rev, $last_commit) = $self->last_rev_commit;
3922
- my ($base, $head) = $self->get_fetch_range($min_rev, $max_rev);
3923
- $self->ra->gs_fetch_loop_common($base, $head, [$self]);
3924
-}
3925
-
3926
-sub set_tree_cb {
3927
- my ($self, $log_entry, $tree, $rev, $date, $author) = @_;
3928
- $self->{inject_parents} = { $rev => $tree };
3929
- $self->fetch(undef, undef);
3930
-}
3931
-
3932
-sub set_tree {
3933
- my ($self, $tree) = (shift, shift);
3934
- my $log_entry = ::get_commit_entry($tree);
3935
- unless ($self->{last_rev}) {
3936
- fatal("Must have an existing revision to commit");
3937
- }
3938
- my %ed_opts = ( r => $self->{last_rev},
3939
- log => $log_entry->{log},
3940
- ra => $self->ra,
3941
- tree_a => $self->{last_commit},
3942
- tree_b => $tree,
3943
- editor_cb => sub {
3944
- $self->set_tree_cb($log_entry, $tree, @_) },
3945
- svn_path => $self->{path} );
3946
- if (!Git::SVN::Editor->new(\%ed_opts)->apply_diff) {
3947
- print "No changes\nr$self->{last_rev} = $tree\n";
3948
- }
3949
-}
3950
-
3951
-sub rebuild_from_rev_db {
3952
- my ($self, $path) = @_;
3953
- my $r = -1;
3954
- open my $fh, '<', $path or croak "open: $!";
3955
- binmode $fh or croak "binmode: $!";
3956
- while (<$fh>) {
3957
- length($_) == 41 or croak "inconsistent size in ($_) != 41";
3958
- chomp($_);
3959
- ++$r;
3960
- next if $_ eq ('0' x 40);
3961
- $self->rev_map_set($r, $_);
3962
- print "r$r = $_\n";
3963
- }
3964
- close $fh or croak "close: $!";
3965
- unlink $path or croak "unlink: $!";
3966
-}
3967
-
3968
-sub rebuild {
3969
- my ($self) = @_;
3970
- my $map_path = $self->map_path;
3971
- my $partial = (-e $map_path && ! -z $map_path);
3972
- return unless ::verify_ref($self->refname.'^0');
3973
- if (!$partial && ($self->use_svm_props || $self->no_metadata)) {
3974
- my $rev_db = $self->rev_db_path;
3975
- $self->rebuild_from_rev_db($rev_db);
3976
- if ($self->use_svm_props) {
3977
- my $svm_rev_db = $self->rev_db_path($self->svm_uuid);
3978
- $self->rebuild_from_rev_db($svm_rev_db);
3979
- }
3980
- $self->unlink_rev_db_symlink;
3981
- return;
3982
- }
3983
- print "Rebuilding $map_path ...\n" if (!$partial);
3984
- my ($base_rev, $head) = ($partial ? $self->rev_map_max_norebuild(1) :
3985
- (undef, undef));
3986
- my ($log, $ctx) =
3987
- command_output_pipe(qw/rev-list --pretty=raw --reverse/,
3988
- ($head ? "$head.." : "") . $self->refname,
3989
- '--');
3990
- my $metadata_url = $self->metadata_url;
3991
- remove_username($metadata_url);
3992
- my $svn_uuid = $self->rewrite_uuid || $self->ra_uuid;
3993
- my $c;
3994
- while (<$log>) {
3995
- if ( m{^commit ($::sha1)$} ) {
3996
- $c = $1;
3997
- next;
3998
- }
3999
- next unless s{^\s*(git-svn-id:)}{$1};
4000
- my ($url, $rev, $uuid) = ::extract_metadata($_);
4001
- remove_username($url);
4002
-
4003
- # ignore merges (from set-tree)
4004
- next if (!defined $rev || !$uuid);
4005
-
4006
- # if we merged or otherwise started elsewhere, this is
4007
- # how we break out of it
4008
- if (($uuid ne $svn_uuid) ||
4009
- ($metadata_url && $url && ($url ne $metadata_url))) {
4010
- next;
4011
- }
4012
- if ($partial && $head) {
4013
- print "Partial-rebuilding $map_path ...\n";
4014
- print "Currently at $base_rev = $head\n";
4015
- $head = undef;
4016
- }
4017
-
4018
- $self->rev_map_set($rev, $c);
4019
- print "r$rev = $c\n";
4020
- }
4021
- command_close_pipe($log, $ctx);
4022
- print "Done rebuilding $map_path\n" if (!$partial || !$head);
4023
- my $rev_db_path = $self->rev_db_path;
4024
- if (-f $self->rev_db_path) {
4025
- unlink $self->rev_db_path or croak "unlink: $!";
4026
- }
4027
- $self->unlink_rev_db_symlink;
4028
-}
4029
-
4030
-# rev_map:
4031
-# Tie::File seems to be prone to offset errors if revisions get sparse,
4032
-# it's not that fast, either. Tie::File is also not in Perl 5.6. So
4033
-# one of my favorite modules is out :< Next up would be one of the DBM
4034
-# modules, but I'm not sure which is most portable...
4035
-#
4036
-# This is the replacement for the rev_db format, which was too big
4037
-# and inefficient for large repositories with a lot of sparse history
4038
-# (mainly tags)
4039
-#
4040
-# The format is this:
4041
-# - 24 bytes for every record,
4042
-# * 4 bytes for the integer representing an SVN revision number
4043
-# * 20 bytes representing the sha1 of a git commit
4044
-# - No empty padding records like the old format
4045
-# (except the last record, which can be overwritten)
4046
-# - new records are written append-only since SVN revision numbers
4047
-# increase monotonically
4048
-# - lookups on SVN revision number are done via a binary search
4049
-# - Piping the file to xxd -c24 is a good way of dumping it for
4050
-# viewing or editing (piped back through xxd -r), should the need
4051
-# ever arise.
4052
-# - The last record can be padding revision with an all-zero sha1
4053
-# This is used to optimize fetch performance when using multiple
4054
-# "fetch" directives in .git/config
4055
-#
4056
-# These files are disposable unless noMetadata or useSvmProps is set
4057
-
4058
-sub _rev_map_set {
4059
- my ($fh, $rev, $commit) = @_;
4060
-
4061
- binmode $fh or croak "binmode: $!";
4062
- my $size = (stat($fh))[7];
4063
- ($size % 24) == 0 or croak "inconsistent size: $size";
4064
-
4065
- my $wr_offset = 0;
4066
- if ($size > 0) {
4067
- sysseek($fh, -24, SEEK_END) or croak "seek: $!";
4068
- my $read = sysread($fh, my $buf, 24) or croak "read: $!";
4069
- $read == 24 or croak "read only $read bytes (!= 24)";
4070
- my ($last_rev, $last_commit) = unpack(rev_map_fmt, $buf);
4071
- if ($last_commit eq ('0' x40)) {
4072
- if ($size >= 48) {
4073
- sysseek($fh, -48, SEEK_END) or croak "seek: $!";
4074
- $read = sysread($fh, $buf, 24) or
4075
- croak "read: $!";
4076
- $read == 24 or
4077
- croak "read only $read bytes (!= 24)";
4078
- ($last_rev, $last_commit) =
4079
- unpack(rev_map_fmt, $buf);
4080
- if ($last_commit eq ('0' x40)) {
4081
- croak "inconsistent .rev_map\n";
4082
- }
4083
- }
4084
- if ($last_rev >= $rev) {
4085
- croak "last_rev is higher!: $last_rev >= $rev";
4086
- }
4087
- $wr_offset = -24;
4088
- }
4089
- }
4090
- sysseek($fh, $wr_offset, SEEK_END) or croak "seek: $!";
4091
- syswrite($fh, pack(rev_map_fmt, $rev, $commit), 24) == 24 or
4092
- croak "write: $!";
4093
-}
4094
-
4095
-sub _rev_map_reset {
4096
- my ($fh, $rev, $commit) = @_;
4097
- my $c = _rev_map_get($fh, $rev);
4098
- $c eq $commit or die "_rev_map_reset(@_) commit $c does not match!\n";
4099
- my $offset = sysseek($fh, 0, SEEK_CUR) or croak "seek: $!";
4100
- truncate $fh, $offset or croak "truncate: $!";
4101
-}
4102
-
4103
-sub mkfile {
4104
- my ($path) = @_;
4105
- unless (-e $path) {
4106
- my ($dir, $base) = ($path =~ m#^(.*?)/?([^/]+)$#);
4107
- mkpath([$dir]) unless -d $dir;
4108
- open my $fh, '>>', $path or die "Couldn't create $path: $!\n";
4109
- close $fh or die "Couldn't close (create) $path: $!\n";
4110
- }
4111
-}
4112
-
4113
-sub rev_map_set {
4114
- my ($self, $rev, $commit, $update_ref, $uuid) = @_;
4115
- defined $commit or die "missing arg3\n";
4116
- length $commit == 40 or die "arg3 must be a full SHA1 hexsum\n";
4117
- my $db = $self->map_path($uuid);
4118
- my $db_lock = "$db.lock";
4119
- my $sigmask;
4120
- $update_ref ||= 0;
4121
- if ($update_ref) {
4122
- $sigmask = POSIX::SigSet->new();
4123
- my $signew = POSIX::SigSet->new(SIGINT, SIGHUP, SIGTERM,
4124
- SIGALRM, SIGUSR1, SIGUSR2);
4125
- sigprocmask(SIG_BLOCK, $signew, $sigmask) or
4126
- croak "Can't block signals: $!";
4127
- }
4128
- mkfile($db);
4129
-
4130
- $LOCKFILES{$db_lock} = 1;
4131
- my $sync;
4132
- # both of these options make our .rev_db file very, very important
4133
- # and we can't afford to lose it because rebuild() won't work
4134
- if ($self->use_svm_props || $self->no_metadata) {
4135
- $sync = 1;
4136
- copy($db, $db_lock) or die "rev_map_set(@_): ",
4137
- "Failed to copy: ",
4138
- "$db => $db_lock ($!)\n";
4139
- } else {
4140
- rename $db, $db_lock or die "rev_map_set(@_): ",
4141
- "Failed to rename: ",
4142
- "$db => $db_lock ($!)\n";
4143
- }
4144
-
4145
- sysopen(my $fh, $db_lock, O_RDWR | O_CREAT)
4146
- or croak "Couldn't open $db_lock: $!\n";
4147
- $update_ref eq 'reset' ? _rev_map_reset($fh, $rev, $commit) :
4148
- _rev_map_set($fh, $rev, $commit);
4149
- if ($sync) {
4150
- $fh->flush or die "Couldn't flush $db_lock: $!\n";
4151
- $fh->sync or die "Couldn't sync $db_lock: $!\n";
4152
- }
4153
- close $fh or croak $!;
4154
- if ($update_ref) {
4155
- $_head = $self;
4156
- my $note = "";
4157
- $note = " ($update_ref)" if ($update_ref !~ /^\d*$/);
4158
- command_noisy('update-ref', '-m', "r$rev$note",
4159
- $self->refname, $commit);
4160
- }
4161
- rename $db_lock, $db or die "rev_map_set(@_): ", "Failed to rename: ",
4162
- "$db_lock => $db ($!)\n";
4163
- delete $LOCKFILES{$db_lock};
4164
- if ($update_ref) {
4165
- sigprocmask(SIG_SETMASK, $sigmask) or
4166
- croak "Can't restore signal mask: $!";
4167
- }
4168
-}
4169
-
4170
-# If want_commit, this will return an array of (rev, commit) where
4171
-# commit _must_ be a valid commit in the archive.
4172
-# Otherwise, it'll return the max revision (whether or not the
4173
-# commit is valid or just a 0x40 placeholder).
4174
-sub rev_map_max {
4175
- my ($self, $want_commit) = @_;
4176
- $self->rebuild;
4177
- my ($r, $c) = $self->rev_map_max_norebuild($want_commit);
4178
- $want_commit ? ($r, $c) : $r;
4179
-}
4180
-
4181
-sub rev_map_max_norebuild {
4182
- my ($self, $want_commit) = @_;
4183
- my $map_path = $self->map_path;
4184
- stat $map_path or return $want_commit ? (0, undef) : 0;
4185
- sysopen(my $fh, $map_path, O_RDONLY) or croak "open: $!";
4186
- binmode $fh or croak "binmode: $!";
4187
- my $size = (stat($fh))[7];
4188
- ($size % 24) == 0 or croak "inconsistent size: $size";
4189
-
4190
- if ($size == 0) {
4191
- close $fh or croak "close: $!";
4192
- return $want_commit ? (0, undef) : 0;
4193
- }
4194
-
4195
- sysseek($fh, -24, SEEK_END) or croak "seek: $!";
4196
- sysread($fh, my $buf, 24) == 24 or croak "read: $!";
4197
- my ($r, $c) = unpack(rev_map_fmt, $buf);
4198
- if ($want_commit && $c eq ('0' x40)) {
4199
- if ($size < 48) {
4200
- return $want_commit ? (0, undef) : 0;
4201
- }
4202
- sysseek($fh, -48, SEEK_END) or croak "seek: $!";
4203
- sysread($fh, $buf, 24) == 24 or croak "read: $!";
4204
- ($r, $c) = unpack(rev_map_fmt, $buf);
4205
- if ($c eq ('0'x40)) {
4206
- croak "Penultimate record is all-zeroes in $map_path";
4207
- }
4208
- }
4209
- close $fh or croak "close: $!";
4210
- $want_commit ? ($r, $c) : $r;
4211
-}
4212
-
4213
-sub rev_map_get {
4214
- my ($self, $rev, $uuid) = @_;
4215
- my $map_path = $self->map_path($uuid);
4216
- return undef unless -e $map_path;
4217
-
4218
- sysopen(my $fh, $map_path, O_RDONLY) or croak "open: $!";
4219
- my $c = _rev_map_get($fh, $rev);
4220
- close($fh) or croak "close: $!";
4221
- $c
4222
-}
4223
-
4224
-sub _rev_map_get {
4225
- my ($fh, $rev) = @_;
4226
-
4227
- binmode $fh or croak "binmode: $!";
4228
- my $size = (stat($fh))[7];
4229
- ($size % 24) == 0 or croak "inconsistent size: $size";
4230
-
4231
- if ($size == 0) {
4232
- return undef;
4233
- }
4234
-
4235
- my ($l, $u) = (0, $size - 24);
4236
- my ($r, $c, $buf);
4237
-
4238
- while ($l <= $u) {
4239
- my $i = int(($l/24 + $u/24) / 2) * 24;
4240
- sysseek($fh, $i, SEEK_SET) or croak "seek: $!";
4241
- sysread($fh, my $buf, 24) == 24 or croak "read: $!";
4242
- my ($r, $c) = unpack(rev_map_fmt, $buf);
4243
-
4244
- if ($r < $rev) {
4245
- $l = $i + 24;
4246
- } elsif ($r > $rev) {
4247
- $u = $i - 24;
4248
- } else { # $r == $rev
4249
- return $c eq ('0' x 40) ? undef : $c;
4250
- }
4251
- }
4252
- undef;
4253
-}
4254
-
4255
-# Finds the first svn revision that exists on (if $eq_ok is true) or
4256
-# before $rev for the current branch. It will not search any lower
4257
-# than $min_rev. Returns the git commit hash and svn revision number
4258
-# if found, else (undef, undef).
4259
-sub find_rev_before {
4260
- my ($self, $rev, $eq_ok, $min_rev) = @_;
4261
- --$rev unless $eq_ok;
4262
- $min_rev ||= 1;
4263
- my $max_rev = $self->rev_map_max;
4264
- $rev = $max_rev if ($rev > $max_rev);
4265
- while ($rev >= $min_rev) {
4266
- if (my $c = $self->rev_map_get($rev)) {
4267
- return ($rev, $c);
4268
- }
4269
- --$rev;
4270
- }
4271
- return (undef, undef);
4272
-}
4273
-
4274
-# Finds the first svn revision that exists on (if $eq_ok is true) or
4275
-# after $rev for the current branch. It will not search any higher
4276
-# than $max_rev. Returns the git commit hash and svn revision number
4277
-# if found, else (undef, undef).
4278
-sub find_rev_after {
4279
- my ($self, $rev, $eq_ok, $max_rev) = @_;
4280
- ++$rev unless $eq_ok;
4281
- $max_rev ||= $self->rev_map_max;
4282
- while ($rev <= $max_rev) {
4283
- if (my $c = $self->rev_map_get($rev)) {
4284
- return ($rev, $c);
4285
- }
4286
- ++$rev;
4287
- }
4288
- return (undef, undef);
4289
-}
4290
-
4291
-sub _new {
4292
- my ($class, $repo_id, $ref_id, $path) = @_;
4293
- unless (defined $repo_id && length $repo_id) {
4294
- $repo_id = $default_repo_id;
4295
- }
4296
- unless (defined $ref_id && length $ref_id) {
4297
- # Access the prefix option from the git-svn main program if it's loaded.
4298
- my $prefix = defined &::opt_prefix ? ::opt_prefix() : "";
4299
- $_[2] = $ref_id =
4300
- "refs/remotes/$prefix$default_ref_id";
4301
- }
4302
- $_[1] = $repo_id;
4303
- my $dir = "$ENV{GIT_DIR}/svn/$ref_id";
4304
-
4305
- # Older repos imported by us used $GIT_DIR/svn/foo instead of
4306
- # $GIT_DIR/svn/refs/remotes/foo when tracking refs/remotes/foo
4307
- if ($ref_id =~ m{^refs/remotes/(.*)}) {
4308
- my $old_dir = "$ENV{GIT_DIR}/svn/$1";
4309
- if (-d $old_dir && ! -d $dir) {
4310
- $dir = $old_dir;
4311
- }
4312
- }
4313
-
4314
- $_[3] = $path = '' unless (defined $path);
4315
- mkpath([$dir]);
4316
- bless {
4317
- ref_id => $ref_id, dir => $dir, index => "$dir/index",
4318
- path => $path, config => "$ENV{GIT_DIR}/svn/config",
4319
- map_root => "$dir/.rev_map", repo_id => $repo_id }, $class;
4320
-}
4321
-
4322
-# for read-only access of old .rev_db formats
4323
-sub unlink_rev_db_symlink {
4324
- my ($self) = @_;
4325
- my $link = $self->rev_db_path;
4326
- $link =~ s/\.[\w-]+$// or croak "missing UUID at the end of $link";
4327
- if (-l $link) {
4328
- unlink $link or croak "unlink: $link failed!";
4329
- }
4330
-}
4331
-
4332
-sub rev_db_path {
4333
- my ($self, $uuid) = @_;
4334
- my $db_path = $self->map_path($uuid);
4335
- $db_path =~ s{/\.rev_map\.}{/\.rev_db\.}
4336
- or croak "map_path: $db_path does not contain '/.rev_map.' !";
4337
- $db_path;
4338
-}
4339
-
4340
-# the new replacement for .rev_db
4341
-sub map_path {
4342
- my ($self, $uuid) = @_;
4343
- $uuid ||= $self->ra_uuid;
4344
- "$self->{map_root}.$uuid";
4345
-}
4346
-
4347
-sub uri_encode {
4348
- my ($f) = @_;
4349
- $f =~ s#([^a-zA-Z0-9\*!\:_\./\-])#uc sprintf("%%%02x",ord($1))#eg;
4350
- $f
4351
-}
4352
-
4353
-sub uri_decode {
4354
- my ($f) = @_;
4355
- $f =~ s#%([0-9a-fA-F]{2})#chr(hex($1))#eg;
4356
- $f
4357
-}
4358
-
4359
-sub remove_username {
4360
- $_[0] =~ s{^([^:]*://)[^@]+@}{$1};
4361
-}
4362
-}
2045
2046
package Git::SVN::Log;
2047
use strict;
perl/Git/SVN.pm
new
+2319
@@ -0,0 +1,2319 @@
1
+package Git::SVN;
2
+use strict;
3
+use warnings;
4
+use Fcntl qw/:DEFAULT :seek/;
5
+use constant rev_map_fmt => 'NH40';
6
+use vars qw/$default_repo_id $default_ref_id $_no_metadata $_follow_parent
7
+ $_repack $_repack_flags $_use_svm_props $_head
8
+ $_use_svnsync_props $no_reuse_existing $_minimize_url
9
+ $_use_log_author $_add_author_from $_localtime/;
10
+use Carp qw/croak/;
11
+use File::Path qw/mkpath/;
12
+use File::Copy qw/copy/;
13
+use IPC::Open3;
14
+use Time::Local;
15
+use Memoize; # core since 5.8.0, Jul 2002
16
+use Memoize::Storable;
17
+use POSIX qw(:signal_h);
18
+
19
+use Git qw(
20
+ command
21
+ command_oneline
22
+ command_noisy
23
+ command_output_pipe
24
+ command_close_pipe
25
+);
26
+use Git::SVN::Utils qw(fatal can_compress);
27
+
28
+my $can_use_yaml;
29
+BEGIN {
30
+ $can_use_yaml = eval { require Git::SVN::Memoize::YAML; 1};
31
+}
32
+
33
+my ($_gc_nr, $_gc_period);
34
+
35
+# properties that we do not log:
36
+my %SKIP_PROP;
37
+BEGIN {
38
+ %SKIP_PROP = map { $_ => 1 } qw/svn:wc:ra_dav:version-url
39
+ svn:special svn:executable
40
+ svn:entry:committed-rev
41
+ svn:entry:last-author
42
+ svn:entry:uuid
43
+ svn:entry:committed-date/;
44
+
45
+ # some options are read globally, but can be overridden locally
46
+ # per [svn-remote "..."] section. Command-line options will *NOT*
47
+ # override options set in an [svn-remote "..."] section
48
+ no strict 'refs';
49
+ for my $option (qw/follow_parent no_metadata use_svm_props
50
+ use_svnsync_props/) {
51
+ my $key = $option;
52
+ $key =~ tr/_//d;
53
+ my $prop = "-$option";
54
+ *$option = sub {
55
+ my ($self) = @_;
56
+ return $self->{$prop} if exists $self->{$prop};
57
+ my $k = "svn-remote.$self->{repo_id}.$key";
58
+ eval { command_oneline(qw/config --get/, $k) };
59
+ if ($@) {
60
+ $self->{$prop} = ${"Git::SVN::_$option"};
61
+ } else {
62
+ my $v = command_oneline(qw/config --bool/,$k);
63
+ $self->{$prop} = $v eq 'false' ? 0 : 1;
64
+ }
65
+ return $self->{$prop};
66
+ }
67
+ }
68
+}
69
+
70
+
71
+my (%LOCKFILES, %INDEX_FILES);
72
+END {
73
+ unlink keys %LOCKFILES if %LOCKFILES;
74
+ unlink keys %INDEX_FILES if %INDEX_FILES;
75
+}
76
+
77
+sub resolve_local_globs {
78
+ my ($url, $fetch, $glob_spec) = @_;
79
+ return unless defined $glob_spec;
80
+ my $ref = $glob_spec->{ref};
81
+ my $path = $glob_spec->{path};
82
+ foreach (command(qw#for-each-ref --format=%(refname) refs/#)) {
83
+ next unless m#^$ref->{regex}$#;
84
+ my $p = $1;
85
+ my $pathname = desanitize_refname($path->full_path($p));
86
+ my $refname = desanitize_refname($ref->full_path($p));
87
+ if (my $existing = $fetch->{$pathname}) {
88
+ if ($existing ne $refname) {
89
+ die "Refspec conflict:\n",
90
+ "existing: $existing\n",
91
+ " globbed: $refname\n";
92
+ }
93
+ my $u = (::cmt_metadata("$refname"))[0];
94
+ $u =~ s!^\Q$url\E(/|$)!! or die
95
+ "$refname: '$url' not found in '$u'\n";
96
+ if ($pathname ne $u) {
97
+ warn "W: Refspec glob conflict ",
98
+ "(ref: $refname):\n",
99
+ "expected path: $pathname\n",
100
+ " real path: $u\n",
101
+ "Continuing ahead with $u\n";
102
+ next;
103
+ }
104
+ } else {
105
+ $fetch->{$pathname} = $refname;
106
+ }
107
+ }
108
+}
109
+
110
+sub parse_revision_argument {
111
+ my ($base, $head) = @_;
112
+ if (!defined $::_revision || $::_revision eq 'BASE:HEAD') {
113
+ return ($base, $head);
114
+ }
115
+ return ($1, $2) if ($::_revision =~ /^(\d+):(\d+)$/);
116
+ return ($::_revision, $::_revision) if ($::_revision =~ /^\d+$/);
117
+ return ($head, $head) if ($::_revision eq 'HEAD');
118
+ return ($base, $1) if ($::_revision =~ /^BASE:(\d+)$/);
119
+ return ($1, $head) if ($::_revision =~ /^(\d+):HEAD$/);
120
+ die "revision argument: $::_revision not understood by git-svn\n";
121
+}
122
+
123
+sub fetch_all {
124
+ my ($repo_id, $remotes) = @_;
125
+ if (ref $repo_id) {
126
+ my $gs = $repo_id;
127
+ $repo_id = undef;
128
+ $repo_id = $gs->{repo_id};
129
+ }
130
+ $remotes ||= read_all_remotes();
131
+ my $remote = $remotes->{$repo_id} or
132
+ die "[svn-remote \"$repo_id\"] unknown\n";
133
+ my $fetch = $remote->{fetch};
134
+ my $url = $remote->{url} or die "svn-remote.$repo_id.url not defined\n";
135
+ my (@gs, @globs);
136
+ my $ra = Git::SVN::Ra->new($url);
137
+ my $uuid = $ra->get_uuid;
138
+ my $head = $ra->get_latest_revnum;
139
+
140
+ # ignore errors, $head revision may not even exist anymore
141
+ eval { $ra->get_log("", $head, 0, 1, 0, 1, sub { $head = $_[1] }) };
142
+ warn "W: $@\n" if $@;
143
+
144
+ my $base = defined $fetch ? $head : 0;
145
+
146
+ # read the max revs for wildcard expansion (branches/*, tags/*)
147
+ foreach my $t (qw/branches tags/) {
148
+ defined $remote->{$t} or next;
149
+ push @globs, @{$remote->{$t}};
150
+
151
+ my $max_rev = eval { tmp_config(qw/--int --get/,
152
+ "svn-remote.$repo_id.${t}-maxRev") };
153
+ if (defined $max_rev && ($max_rev < $base)) {
154
+ $base = $max_rev;
155
+ } elsif (!defined $max_rev) {
156
+ $base = 0;
157
+ }
158
+ }
159
+
160
+ if ($fetch) {
161
+ foreach my $p (sort keys %$fetch) {
162
+ my $gs = Git::SVN->new($fetch->{$p}, $repo_id, $p);
163
+ my $lr = $gs->rev_map_max;
164
+ if (defined $lr) {
165
+ $base = $lr if ($lr < $base);
166
+ }
167
+ push @gs, $gs;
168
+ }
169
+ }
170
+
171
+ ($base, $head) = parse_revision_argument($base, $head);
172
+ $ra->gs_fetch_loop_common($base, $head, \@gs, \@globs);
173
+}
174
+
175
+sub read_all_remotes {
176
+ my $r = {};
177
+ my $use_svm_props = eval { command_oneline(qw/config --bool
178
+ svn.useSvmProps/) };
179
+ $use_svm_props = $use_svm_props eq 'true' if $use_svm_props;
180
+ my $svn_refspec = qr{\s*(.*?)\s*:\s*(.+?)\s*};
181
+ foreach (grep { s/^svn-remote\.// } command(qw/config -l/)) {
182
+ if (m!^(.+)\.fetch=$svn_refspec$!) {
183
+ my ($remote, $local_ref, $remote_ref) = ($1, $2, $3);
184
+ die("svn-remote.$remote: remote ref '$remote_ref' "
185
+ . "must start with 'refs/'\n")
186
+ unless $remote_ref =~ m{^refs/};
187
+ $local_ref = uri_decode($local_ref);
188
+ $r->{$remote}->{fetch}->{$local_ref} = $remote_ref;
189
+ $r->{$remote}->{svm} = {} if $use_svm_props;
190
+ } elsif (m!^(.+)\.usesvmprops=\s*(.*)\s*$!) {
191
+ $r->{$1}->{svm} = {};
192
+ } elsif (m!^(.+)\.url=\s*(.*)\s*$!) {
193
+ $r->{$1}->{url} = $2;
194
+ } elsif (m!^(.+)\.pushurl=\s*(.*)\s*$!) {
195
+ $r->{$1}->{pushurl} = $2;
196
+ } elsif (m!^(.+)\.ignore-refs=\s*(.*)\s*$!) {
197
+ $r->{$1}->{ignore_refs_regex} = $2;
198
+ } elsif (m!^(.+)\.(branches|tags)=$svn_refspec$!) {
199
+ my ($remote, $t, $local_ref, $remote_ref) =
200
+ ($1, $2, $3, $4);
201
+ die("svn-remote.$remote: remote ref '$remote_ref' ($t) "
202
+ . "must start with 'refs/'\n")
203
+ unless $remote_ref =~ m{^refs/};
204
+ $local_ref = uri_decode($local_ref);
205
+ my $rs = {
206
+ t => $t,
207
+ remote => $remote,
208
+ path => Git::SVN::GlobSpec->new($local_ref, 1),
209
+ ref => Git::SVN::GlobSpec->new($remote_ref, 0) };
210
+ if (length($rs->{ref}->{right}) != 0) {
211
+ die "The '*' glob character must be the last ",
212
+ "character of '$remote_ref'\n";
213
+ }
214
+ push @{ $r->{$remote}->{$t} }, $rs;
215
+ }
216
+ }
217
+
218
+ map {
219
+ if (defined $r->{$_}->{svm}) {
220
+ my $svm;
221
+ eval {
222
+ my $section = "svn-remote.$_";
223
+ $svm = {
224
+ source => tmp_config('--get',
225
+ "$section.svm-source"),
226
+ replace => tmp_config('--get',
227
+ "$section.svm-replace"),
228
+ }
229
+ };
230
+ $r->{$_}->{svm} = $svm;
231
+ }
232
+ } keys %$r;
233
+
234
+ foreach my $remote (keys %$r) {
235
+ foreach ( grep { defined $_ }
236
+ map { $r->{$remote}->{$_} } qw(branches tags) ) {
237
+ foreach my $rs ( @$_ ) {
238
+ $rs->{ignore_refs_regex} =
239
+ $r->{$remote}->{ignore_refs_regex};
240
+ }
241
+ }
242
+ }
243
+
244
+ $r;
245
+}
246
+
247
+sub init_vars {
248
+ $_gc_nr = $_gc_period = 1000;
249
+ if (defined $_repack || defined $_repack_flags) {
250
+ warn "Repack options are obsolete; they have no effect.\n";
251
+ }
252
+}
253
+
254
+sub verify_remotes_sanity {
255
+ return unless -d $ENV{GIT_DIR};
256
+ my %seen;
257
+ foreach (command(qw/config -l/)) {
258
+ if (m!^svn-remote\.(?:.+)\.fetch=.*:refs/remotes/(\S+)\s*$!) {
259
+ if ($seen{$1}) {
260
+ die "Remote ref refs/remote/$1 is tracked by",
261
+ "\n \"$_\"\nand\n \"$seen{$1}\"\n",
262
+ "Please resolve this ambiguity in ",
263
+ "your git configuration file before ",
264
+ "continuing\n";
265
+ }
266
+ $seen{$1} = $_;
267
+ }
268
+ }
269
+}
270
+
271
+sub find_existing_remote {
272
+ my ($url, $remotes) = @_;
273
+ return undef if $no_reuse_existing;
274
+ my $existing;
275
+ foreach my $repo_id (keys %$remotes) {
276
+ my $u = $remotes->{$repo_id}->{url} or next;
277
+ next if $u ne $url;
278
+ $existing = $repo_id;
279
+ last;
280
+ }
281
+ $existing;
282
+}
283
+
284
+sub init_remote_config {
285
+ my ($self, $url, $no_write) = @_;
286
+ $url =~ s!/+$!!; # strip trailing slash
287
+ my $r = read_all_remotes();
288
+ my $existing = find_existing_remote($url, $r);
289
+ if ($existing) {
290
+ unless ($no_write) {
291
+ print STDERR "Using existing ",
292
+ "[svn-remote \"$existing\"]\n";
293
+ }
294
+ $self->{repo_id} = $existing;
295
+ } elsif ($_minimize_url) {
296
+ my $min_url = Git::SVN::Ra->new($url)->minimize_url;
297
+ $existing = find_existing_remote($min_url, $r);
298
+ if ($existing) {
299
+ unless ($no_write) {
300
+ print STDERR "Using existing ",
301
+ "[svn-remote \"$existing\"]\n";
302
+ }
303
+ $self->{repo_id} = $existing;
304
+ }
305
+ if ($min_url ne $url) {
306
+ unless ($no_write) {
307
+ print STDERR "Using higher level of URL: ",
308
+ "$url => $min_url\n";
309
+ }
310
+ my $old_path = $self->{path};
311
+ $self->{path} = $url;
312
+ $self->{path} =~ s!^\Q$min_url\E(/|$)!!;
313
+ if (length $old_path) {
314
+ $self->{path} .= "/$old_path";
315
+ }
316
+ $url = $min_url;
317
+ }
318
+ }
319
+ my $orig_url;
320
+ if (!$existing) {
321
+ # verify that we aren't overwriting anything:
322
+ $orig_url = eval {
323
+ command_oneline('config', '--get',
324
+ "svn-remote.$self->{repo_id}.url")
325
+ };
326
+ if ($orig_url && ($orig_url ne $url)) {
327
+ die "svn-remote.$self->{repo_id}.url already set: ",
328
+ "$orig_url\nwanted to set to: $url\n";
329
+ }
330
+ }
331
+ my ($xrepo_id, $xpath) = find_ref($self->refname);
332
+ if (!$no_write && defined $xpath) {
333
+ die "svn-remote.$xrepo_id.fetch already set to track ",
334
+ "$xpath:", $self->refname, "\n";
335
+ }
336
+ unless ($no_write) {
337
+ command_noisy('config',
338
+ "svn-remote.$self->{repo_id}.url", $url);
339
+ $self->{path} =~ s{^/}{};
340
+ $self->{path} =~ s{%([0-9A-F]{2})}{chr hex($1)}ieg;
341
+ command_noisy('config', '--add',
342
+ "svn-remote.$self->{repo_id}.fetch",
343
+ "$self->{path}:".$self->refname);
344
+ }
345
+ $self->{url} = $url;
346
+}
347
+
348
+sub find_by_url { # repos_root and, path are optional
349
+ my ($class, $full_url, $repos_root, $path) = @_;
350
+
351
+ return undef unless defined $full_url;
352
+ remove_username($full_url);
353
+ remove_username($repos_root) if defined $repos_root;
354
+ my $remotes = read_all_remotes();
355
+ if (defined $full_url && defined $repos_root && !defined $path) {
356
+ $path = $full_url;
357
+ $path =~ s#^\Q$repos_root\E(?:/|$)##;
358
+ }
359
+ foreach my $repo_id (keys %$remotes) {
360
+ my $u = $remotes->{$repo_id}->{url} or next;
361
+ remove_username($u);
362
+ next if defined $repos_root && $repos_root ne $u;
363
+
364
+ my $fetch = $remotes->{$repo_id}->{fetch} || {};
365
+ foreach my $t (qw/branches tags/) {
366
+ foreach my $globspec (@{$remotes->{$repo_id}->{$t}}) {
367
+ resolve_local_globs($u, $fetch, $globspec);
368
+ }
369
+ }
370
+ my $p = $path;
371
+ my $rwr = rewrite_root({repo_id => $repo_id});
372
+ my $svm = $remotes->{$repo_id}->{svm}
373
+ if defined $remotes->{$repo_id}->{svm};
374
+ unless (defined $p) {
375
+ $p = $full_url;
376
+ my $z = $u;
377
+ my $prefix = '';
378
+ if ($rwr) {
379
+ $z = $rwr;
380
+ remove_username($z);
381
+ } elsif (defined $svm) {
382
+ $z = $svm->{source};
383
+ $prefix = $svm->{replace};
384
+ $prefix =~ s#^\Q$u\E(?:/|$)##;
385
+ $prefix =~ s#/$##;
386
+ }
387
+ $p =~ s#^\Q$z\E(?:/|$)#$prefix# or next;
388
+ }
389
+ foreach my $f (keys %$fetch) {
390
+ next if $f ne $p;
391
+ return Git::SVN->new($fetch->{$f}, $repo_id, $f);
392
+ }
393
+ }
394
+ undef;
395
+}
396
+
397
+sub init {
398
+ my ($class, $url, $path, $repo_id, $ref_id, $no_write) = @_;
399
+ my $self = _new($class, $repo_id, $ref_id, $path);
400
+ if (defined $url) {
401
+ $self->init_remote_config($url, $no_write);
402
+ }
403
+ $self;
404
+}
405
+
406
+sub find_ref {
407
+ my ($ref_id) = @_;
408
+ foreach (command(qw/config -l/)) {
409
+ next unless m!^svn-remote\.(.+)\.fetch=
410
+ \s*(.*?)\s*:\s*(.+?)\s*$!x;
411
+ my ($repo_id, $path, $ref) = ($1, $2, $3);
412
+ if ($ref eq $ref_id) {
413
+ $path = '' if ($path =~ m#^\./?#);
414
+ return ($repo_id, $path);
415
+ }
416
+ }
417
+ (undef, undef, undef);
418
+}
419
+
420
+sub new {
421
+ my ($class, $ref_id, $repo_id, $path) = @_;
422
+ if (defined $ref_id && !defined $repo_id && !defined $path) {
423
+ ($repo_id, $path) = find_ref($ref_id);
424
+ if (!defined $repo_id) {
425
+ die "Could not find a \"svn-remote.*.fetch\" key ",
426
+ "in the repository configuration matching: ",
427
+ "$ref_id\n";
428
+ }
429
+ }
430
+ my $self = _new($class, $repo_id, $ref_id, $path);
431
+ if (!defined $self->{path} || !length $self->{path}) {
432
+ my $fetch = command_oneline('config', '--get',
433
+ "svn-remote.$repo_id.fetch",
434
+ ":$ref_id\$") or
435
+ die "Failed to read \"svn-remote.$repo_id.fetch\" ",
436
+ "\":$ref_id\$\" in config\n";
437
+ ($self->{path}, undef) = split(/\s*:\s*/, $fetch);
438
+ }
439
+ $self->{path} =~ s{/+}{/}g;
440
+ $self->{path} =~ s{\A/}{};
441
+ $self->{path} =~ s{/\z}{};
442
+ $self->{url} = command_oneline('config', '--get',
443
+ "svn-remote.$repo_id.url") or
444
+ die "Failed to read \"svn-remote.$repo_id.url\" in config\n";
445
+ $self->{pushurl} = eval { command_oneline('config', '--get',
446
+ "svn-remote.$repo_id.pushurl") };
447
+ $self->rebuild;
448
+ $self;
449
+}
450
+
451
+sub refname {
452
+ my ($refname) = $_[0]->{ref_id} ;
453
+
454
+ # It cannot end with a slash /, we'll throw up on this because
455
+ # SVN can't have directories with a slash in their name, either:
456
+ if ($refname =~ m{/$}) {
457
+ die "ref: '$refname' ends with a trailing slash, this is ",
458
+ "not permitted by git nor Subversion\n";
459
+ }
460
+
461
+ # It cannot have ASCII control character space, tilde ~, caret ^,
462
+ # colon :, question-mark ?, asterisk *, space, or open bracket [
463
+ # anywhere.
464
+ #
465
+ # Additionally, % must be escaped because it is used for escaping
466
+ # and we want our escaped refname to be reversible
467
+ $refname =~ s{([ \%~\^:\?\*\[\t])}{uc sprintf('%%%02x',ord($1))}eg;
468
+
469
+ # no slash-separated component can begin with a dot .
470
+ # /.* becomes /%2E*
471
+ $refname =~ s{/\.}{/%2E}g;
472
+
473
+ # It cannot have two consecutive dots .. anywhere
474
+ # .. becomes %2E%2E
475
+ $refname =~ s{\.\.}{%2E%2E}g;
476
+
477
+ # trailing dots and .lock are not allowed
478
+ # .$ becomes %2E and .lock becomes %2Elock
479
+ $refname =~ s{\.(?=$|lock$)}{%2E};
480
+
481
+ # the sequence @{ is used to access the reflog
482
+ # @{ becomes %40{
483
+ $refname =~ s{\@\{}{%40\{}g;
484
+
485
+ return $refname;
486
+}
487
+
488
+sub desanitize_refname {
489
+ my ($refname) = @_;
490
+ $refname =~ s{%(?:([0-9A-F]{2}))}{chr hex($1)}eg;
491
+ return $refname;
492
+}
493
+
494
+sub svm_uuid {
495
+ my ($self) = @_;
496
+ return $self->{svm}->{uuid} if $self->svm;
497
+ $self->ra;
498
+ unless ($self->{svm}) {
499
+ die "SVM UUID not cached, and reading remotely failed\n";
500
+ }
501
+ $self->{svm}->{uuid};
502
+}
503
+
504
+sub svm {
505
+ my ($self) = @_;
506
+ return $self->{svm} if $self->{svm};
507
+ my $svm;
508
+ # see if we have it in our config, first:
509
+ eval {
510
+ my $section = "svn-remote.$self->{repo_id}";
511
+ $svm = {
512
+ source => tmp_config('--get', "$section.svm-source"),
513
+ uuid => tmp_config('--get', "$section.svm-uuid"),
514
+ replace => tmp_config('--get', "$section.svm-replace"),
515
+ }
516
+ };
517
+ if ($svm && $svm->{source} && $svm->{uuid} && $svm->{replace}) {
518
+ $self->{svm} = $svm;
519
+ }
520
+ $self->{svm};
521
+}
522
+
523
+sub _set_svm_vars {
524
+ my ($self, $ra) = @_;
525
+ return $ra if $self->svm;
526
+
527
+ my @err = ( "useSvmProps set, but failed to read SVM properties\n",
528
+ "(svm:source, svm:uuid) ",
529
+ "from the following URLs:\n" );
530
+ sub read_svm_props {
531
+ my ($self, $ra, $path, $r) = @_;
532
+ my $props = ($ra->get_dir($path, $r))[2];
533
+ my $src = $props->{'svm:source'};
534
+ my $uuid = $props->{'svm:uuid'};
535
+ return undef if (!$src || !$uuid);
536
+
537
+ chomp($src, $uuid);
538
+
539
+ $uuid =~ m{^[0-9a-f\-]{30,}$}i
540
+ or die "doesn't look right - svm:uuid is '$uuid'\n";
541
+
542
+ # the '!' is used to mark the repos_root!/relative/path
543
+ $src =~ s{/?!/?}{/};
544
+ $src =~ s{/+$}{}; # no trailing slashes please
545
+ # username is of no interest
546
+ $src =~ s{(^[a-z\+]*://)[^/@]*@}{$1};
547
+
548
+ my $replace = $ra->{url};
549
+ $replace .= "/$path" if length $path;
550
+
551
+ my $section = "svn-remote.$self->{repo_id}";
552
+ tmp_config("$section.svm-source", $src);
553
+ tmp_config("$section.svm-replace", $replace);
554
+ tmp_config("$section.svm-uuid", $uuid);
555
+ $self->{svm} = {
556
+ source => $src,
557
+ uuid => $uuid,
558
+ replace => $replace
559
+ };
560
+ }
561
+
562
+ my $r = $ra->get_latest_revnum;
563
+ my $path = $self->{path};
564
+ my %tried;
565
+ while (length $path) {
566
+ unless ($tried{"$self->{url}/$path"}) {
567
+ return $ra if $self->read_svm_props($ra, $path, $r);
568
+ $tried{"$self->{url}/$path"} = 1;
569
+ }
570
+ $path =~ s#/?[^/]+$##;
571
+ }
572
+ die "Path: '$path' should be ''\n" if $path ne '';
573
+ return $ra if $self->read_svm_props($ra, $path, $r);
574
+ $tried{"$self->{url}/$path"} = 1;
575
+
576
+ if ($ra->{repos_root} eq $self->{url}) {
577
+ die @err, (map { " $_\n" } keys %tried), "\n";
578
+ }
579
+
580
+ # nope, make sure we're connected to the repository root:
581
+ my $ok;
582
+ my @tried_b;
583
+ $path = $ra->{svn_path};
584
+ $ra = Git::SVN::Ra->new($ra->{repos_root});
585
+ while (length $path) {
586
+ unless ($tried{"$ra->{url}/$path"}) {
587
+ $ok = $self->read_svm_props($ra, $path, $r);
588
+ last if $ok;
589
+ $tried{"$ra->{url}/$path"} = 1;
590
+ }
591
+ $path =~ s#/?[^/]+$##;
592
+ }
593
+ die "Path: '$path' should be ''\n" if $path ne '';
594
+ $ok ||= $self->read_svm_props($ra, $path, $r);
595
+ $tried{"$ra->{url}/$path"} = 1;
596
+ if (!$ok) {
597
+ die @err, (map { " $_\n" } keys %tried), "\n";
598
+ }
599
+ Git::SVN::Ra->new($self->{url});
600
+}
601
+
602
+sub svnsync {
603
+ my ($self) = @_;
604
+ return $self->{svnsync} if $self->{svnsync};
605
+
606
+ if ($self->no_metadata) {
607
+ die "Can't have both 'noMetadata' and ",
608
+ "'useSvnsyncProps' options set!\n";
609
+ }
610
+ if ($self->rewrite_root) {
611
+ die "Can't have both 'useSvnsyncProps' and 'rewriteRoot' ",
612
+ "options set!\n";
613
+ }
614
+ if ($self->rewrite_uuid) {
615
+ die "Can't have both 'useSvnsyncProps' and 'rewriteUUID' ",
616
+ "options set!\n";
617
+ }
618
+
619
+ my $svnsync;
620
+ # see if we have it in our config, first:
621
+ eval {
622
+ my $section = "svn-remote.$self->{repo_id}";
623
+
624
+ my $url = tmp_config('--get', "$section.svnsync-url");
625
+ ($url) = ($url =~ m{^([a-z\+]+://\S+)$}) or
626
+ die "doesn't look right - svn:sync-from-url is '$url'\n";
627
+
628
+ my $uuid = tmp_config('--get', "$section.svnsync-uuid");
629
+ ($uuid) = ($uuid =~ m{^([0-9a-f\-]{30,})$}i) or
630
+ die "doesn't look right - svn:sync-from-uuid is '$uuid'\n";
631
+
632
+ $svnsync = { url => $url, uuid => $uuid }
633
+ };
634
+ if ($svnsync && $svnsync->{url} && $svnsync->{uuid}) {
635
+ return $self->{svnsync} = $svnsync;
636
+ }
637
+
638
+ my $err = "useSvnsyncProps set, but failed to read " .
639
+ "svnsync property: svn:sync-from-";
640
+ my $rp = $self->ra->rev_proplist(0);
641
+
642
+ my $url = $rp->{'svn:sync-from-url'} or die $err . "url\n";
643
+ ($url) = ($url =~ m{^([a-z\+]+://\S+)$}) or
644
+ die "doesn't look right - svn:sync-from-url is '$url'\n";
645
+
646
+ my $uuid = $rp->{'svn:sync-from-uuid'} or die $err . "uuid\n";
647
+ ($uuid) = ($uuid =~ m{^([0-9a-f\-]{30,})$}i) or
648
+ die "doesn't look right - svn:sync-from-uuid is '$uuid'\n";
649
+
650
+ my $section = "svn-remote.$self->{repo_id}";
651
+ tmp_config('--add', "$section.svnsync-uuid", $uuid);
652
+ tmp_config('--add', "$section.svnsync-url", $url);
653
+ return $self->{svnsync} = { url => $url, uuid => $uuid };
654
+}
655
+
656
+# this allows us to memoize our SVN::Ra UUID locally and avoid a
657
+# remote lookup (useful for 'git svn log').
658
+sub ra_uuid {
659
+ my ($self) = @_;
660
+ unless ($self->{ra_uuid}) {
661
+ my $key = "svn-remote.$self->{repo_id}.uuid";
662
+ my $uuid = eval { tmp_config('--get', $key) };
663
+ if (!$@ && $uuid && $uuid =~ /^([a-f\d\-]{30,})$/i) {
664
+ $self->{ra_uuid} = $uuid;
665
+ } else {
666
+ die "ra_uuid called without URL\n" unless $self->{url};
667
+ $self->{ra_uuid} = $self->ra->get_uuid;
668
+ tmp_config('--add', $key, $self->{ra_uuid});
669
+ }
670
+ }
671
+ $self->{ra_uuid};
672
+}
673
+
674
+sub _set_repos_root {
675
+ my ($self, $repos_root) = @_;
676
+ my $k = "svn-remote.$self->{repo_id}.reposRoot";
677
+ $repos_root ||= $self->ra->{repos_root};
678
+ tmp_config($k, $repos_root);
679
+ $repos_root;
680
+}
681
+
682
+sub repos_root {
683
+ my ($self) = @_;
684
+ my $k = "svn-remote.$self->{repo_id}.reposRoot";
685
+ eval { tmp_config('--get', $k) } || $self->_set_repos_root;
686
+}
687
+
688
+sub ra {
689
+ my ($self) = shift;
690
+ my $ra = Git::SVN::Ra->new($self->{url});
691
+ $self->_set_repos_root($ra->{repos_root});
692
+ if ($self->use_svm_props && !$self->{svm}) {
693
+ if ($self->no_metadata) {
694
+ die "Can't have both 'noMetadata' and ",
695
+ "'useSvmProps' options set!\n";
696
+ } elsif ($self->use_svnsync_props) {
697
+ die "Can't have both 'useSvnsyncProps' and ",
698
+ "'useSvmProps' options set!\n";
699
+ }
700
+ $ra = $self->_set_svm_vars($ra);
701
+ $self->{-want_revprops} = 1;
702
+ }
703
+ $ra;
704
+}
705
+
706
+# prop_walk(PATH, REV, SUB)
707
+# -------------------------
708
+# Recursively traverse PATH at revision REV and invoke SUB for each
709
+# directory that contains a SVN property. SUB will be invoked as
710
+# follows: &SUB(gs, path, props); where `gs' is this instance of
711
+# Git::SVN, `path' the path to the directory where the properties
712
+# `props' were found. The `path' will be relative to point of checkout,
713
+# that is, if url://repo/trunk is the current Git branch, and that
714
+# directory contains a sub-directory `d', SUB will be invoked with `/d/'
715
+# as `path' (note the trailing `/').
716
+sub prop_walk {
717
+ my ($self, $path, $rev, $sub) = @_;
718
+
719
+ $path =~ s#^/##;
720
+ my ($dirent, undef, $props) = $self->ra->get_dir($path, $rev);
721
+ $path =~ s#^/*#/#g;
722
+ my $p = $path;
723
+ # Strip the irrelevant part of the path.
724
+ $p =~ s#^/+\Q$self->{path}\E(/|$)#/#;
725
+ # Ensure the path is terminated by a `/'.
726
+ $p =~ s#/*$#/#;
727
+
728
+ # The properties contain all the internal SVN stuff nobody
729
+ # (usually) cares about.
730
+ my $interesting_props = 0;
731
+ foreach (keys %{$props}) {
732
+ # If it doesn't start with `svn:', it must be a
733
+ # user-defined property.
734
+ ++$interesting_props and next if $_ !~ /^svn:/;
735
+ # FIXME: Fragile, if SVN adds new public properties,
736
+ # this needs to be updated.
737
+ ++$interesting_props if /^svn:(?:ignore|keywords|executable
738
+ |eol-style|mime-type
739
+ |externals|needs-lock)$/x;
740
+ }
741
+ &$sub($self, $p, $props) if $interesting_props;
742
+
743
+ foreach (sort keys %$dirent) {
744
+ next if $dirent->{$_}->{kind} != $SVN::Node::dir;
745
+ $self->prop_walk($self->{path} . $p . $_, $rev, $sub);
746
+ }
747
+}
748
+
749
+sub last_rev { ($_[0]->last_rev_commit)[0] }
750
+sub last_commit { ($_[0]->last_rev_commit)[1] }
751
+
752
+# returns the newest SVN revision number and newest commit SHA1
753
+sub last_rev_commit {
754
+ my ($self) = @_;
755
+ if (defined $self->{last_rev} && defined $self->{last_commit}) {
756
+ return ($self->{last_rev}, $self->{last_commit});
757
+ }
758
+ my $c = ::verify_ref($self->refname.'^0');
759
+ if ($c && !$self->use_svm_props && !$self->no_metadata) {
760
+ my $rev = (::cmt_metadata($c))[1];
761
+ if (defined $rev) {
762
+ ($self->{last_rev}, $self->{last_commit}) = ($rev, $c);
763
+ return ($rev, $c);
764
+ }
765
+ }
766
+ my $map_path = $self->map_path;
767
+ unless (-e $map_path) {
768
+ ($self->{last_rev}, $self->{last_commit}) = (undef, undef);
769
+ return (undef, undef);
770
+ }
771
+ my ($rev, $commit) = $self->rev_map_max(1);
772
+ ($self->{last_rev}, $self->{last_commit}) = ($rev, $commit);
773
+ return ($rev, $commit);
774
+}
775
+
776
+sub get_fetch_range {
777
+ my ($self, $min, $max) = @_;
778
+ $max ||= $self->ra->get_latest_revnum;
779
+ $min ||= $self->rev_map_max;
780
+ (++$min, $max);
781
+}
782
+
783
+sub tmp_config {
784
+ my (@args) = @_;
785
+ my $old_def_config = "$ENV{GIT_DIR}/svn/config";
786
+ my $config = "$ENV{GIT_DIR}/svn/.metadata";
787
+ if (! -f $config && -f $old_def_config) {
788
+ rename $old_def_config, $config or
789
+ die "Failed rename $old_def_config => $config: $!\n";
790
+ }
791
+ my $old_config = $ENV{GIT_CONFIG};
792
+ $ENV{GIT_CONFIG} = $config;
793
+ $@ = undef;
794
+ my @ret = eval {
795
+ unless (-f $config) {
796
+ mkfile($config);
797
+ open my $fh, '>', $config or
798
+ die "Can't open $config: $!\n";
799
+ print $fh "; This file is used internally by ",
800
+ "git-svn\n" or die
801
+ "Couldn't write to $config: $!\n";
802
+ print $fh "; You should not have to edit it\n" or
803
+ die "Couldn't write to $config: $!\n";
804
+ close $fh or die "Couldn't close $config: $!\n";
805
+ }
806
+ command('config', @args);
807
+ };
808
+ my $err = $@;
809
+ if (defined $old_config) {
810
+ $ENV{GIT_CONFIG} = $old_config;
811
+ } else {
812
+ delete $ENV{GIT_CONFIG};
813
+ }
814
+ die $err if $err;
815
+ wantarray ? @ret : $ret[0];
816
+}
817
+
818
+sub tmp_index_do {
819
+ my ($self, $sub) = @_;
820
+ my $old_index = $ENV{GIT_INDEX_FILE};
821
+ $ENV{GIT_INDEX_FILE} = $self->{index};
822
+ $@ = undef;
823
+ my @ret = eval {
824
+ my ($dir, $base) = ($self->{index} =~ m#^(.*?)/?([^/]+)$#);
825
+ mkpath([$dir]) unless -d $dir;
826
+ &$sub;
827
+ };
828
+ my $err = $@;
829
+ if (defined $old_index) {
830
+ $ENV{GIT_INDEX_FILE} = $old_index;
831
+ } else {
832
+ delete $ENV{GIT_INDEX_FILE};
833
+ }
834
+ die $err if $err;
835
+ wantarray ? @ret : $ret[0];
836
+}
837
+
838
+sub assert_index_clean {
839
+ my ($self, $treeish) = @_;
840
+
841
+ $self->tmp_index_do(sub {
842
+ command_noisy('read-tree', $treeish) unless -e $self->{index};
843
+ my $x = command_oneline('write-tree');
844
+ my ($y) = (command(qw/cat-file commit/, $treeish) =~
845
+ /^tree ($::sha1)/mo);
846
+ return if $y eq $x;
847
+
848
+ warn "Index mismatch: $y != $x\nrereading $treeish\n";
849
+ unlink $self->{index} or die "unlink $self->{index}: $!\n";
850
+ command_noisy('read-tree', $treeish);
851
+ $x = command_oneline('write-tree');
852
+ if ($y ne $x) {
853
+ fatal "trees ($treeish) $y != $x\n",
854
+ "Something is seriously wrong...";
855
+ }
856
+ });
857
+}
858
+
859
+sub get_commit_parents {
860
+ my ($self, $log_entry) = @_;
861
+ my (%seen, @ret, @tmp);
862
+ # legacy support for 'set-tree'; this is only used by set_tree_cb:
863
+ if (my $ip = $self->{inject_parents}) {
864
+ if (my $commit = delete $ip->{$log_entry->{revision}}) {
865
+ push @tmp, $commit;
866
+ }
867
+ }
868
+ if (my $cur = ::verify_ref($self->refname.'^0')) {
869
+ push @tmp, $cur;
870
+ }
871
+ if (my $ipd = $self->{inject_parents_dcommit}) {
872
+ if (my $commit = delete $ipd->{$log_entry->{revision}}) {
873
+ push @tmp, @$commit;
874
+ }
875
+ }
876
+ push @tmp, $_ foreach (@{$log_entry->{parents}}, @tmp);
877
+ while (my $p = shift @tmp) {
878
+ next if $seen{$p};
879
+ $seen{$p} = 1;
880
+ push @ret, $p;
881
+ }
882
+ @ret;
883
+}
884
+
885
+sub rewrite_root {
886
+ my ($self) = @_;
887
+ return $self->{-rewrite_root} if exists $self->{-rewrite_root};
888
+ my $k = "svn-remote.$self->{repo_id}.rewriteRoot";
889
+ my $rwr = eval { command_oneline(qw/config --get/, $k) };
890
+ if ($rwr) {
891
+ $rwr =~ s#/+$##;
892
+ if ($rwr !~ m#^[a-z\+]+://#) {
893
+ die "$rwr is not a valid URL (key: $k)\n";
894
+ }
895
+ }
896
+ $self->{-rewrite_root} = $rwr;
897
+}
898
+
899
+sub rewrite_uuid {
900
+ my ($self) = @_;
901
+ return $self->{-rewrite_uuid} if exists $self->{-rewrite_uuid};
902
+ my $k = "svn-remote.$self->{repo_id}.rewriteUUID";
903
+ my $rwid = eval { command_oneline(qw/config --get/, $k) };
904
+ if ($rwid) {
905
+ $rwid =~ s#/+$##;
906
+ if ($rwid !~ m#^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$#) {
907
+ die "$rwid is not a valid UUID (key: $k)\n";
908
+ }
909
+ }
910
+ $self->{-rewrite_uuid} = $rwid;
911
+}
912
+
913
+sub metadata_url {
914
+ my ($self) = @_;
915
+ ($self->rewrite_root || $self->{url}) .
916
+ (length $self->{path} ? '/' . $self->{path} : '');
917
+}
918
+
919
+sub full_url {
920
+ my ($self) = @_;
921
+ $self->{url} . (length $self->{path} ? '/' . $self->{path} : '');
922
+}
923
+
924
+sub full_pushurl {
925
+ my ($self) = @_;
926
+ if ($self->{pushurl}) {
927
+ return $self->{pushurl} . (length $self->{path} ? '/' .
928
+ $self->{path} : '');
929
+ } else {
930
+ return $self->full_url;
931
+ }
932
+}
933
+
934
+sub set_commit_header_env {
935
+ my ($log_entry) = @_;
936
+ my %env;
937
+ foreach my $ned (qw/NAME EMAIL DATE/) {
938
+ foreach my $ac (qw/AUTHOR COMMITTER/) {
939
+ $env{"GIT_${ac}_${ned}"} = $ENV{"GIT_${ac}_${ned}"};
940
+ }
941
+ }
942
+
943
+ $ENV{GIT_AUTHOR_NAME} = $log_entry->{name};
944
+ $ENV{GIT_AUTHOR_EMAIL} = $log_entry->{email};
945
+ $ENV{GIT_AUTHOR_DATE} = $ENV{GIT_COMMITTER_DATE} = $log_entry->{date};
946
+
947
+ $ENV{GIT_COMMITTER_NAME} = (defined $log_entry->{commit_name})
948
+ ? $log_entry->{commit_name}
949
+ : $log_entry->{name};
950
+ $ENV{GIT_COMMITTER_EMAIL} = (defined $log_entry->{commit_email})
951
+ ? $log_entry->{commit_email}
952
+ : $log_entry->{email};
953
+ \%env;
954
+}
955
+
956
+sub restore_commit_header_env {
957
+ my ($env) = @_;
958
+ foreach my $ned (qw/NAME EMAIL DATE/) {
959
+ foreach my $ac (qw/AUTHOR COMMITTER/) {
960
+ my $k = "GIT_${ac}_${ned}";
961
+ if (defined $env->{$k}) {
962
+ $ENV{$k} = $env->{$k};
963
+ } else {
964
+ delete $ENV{$k};
965
+ }
966
+ }
967
+ }
968
+}
969
+
970
+sub gc {
971
+ command_noisy('gc', '--auto');
972
+};
973
+
974
+sub do_git_commit {
975
+ my ($self, $log_entry) = @_;
976
+ my $lr = $self->last_rev;
977
+ if (defined $lr && $lr >= $log_entry->{revision}) {
978
+ die "Last fetched revision of ", $self->refname,
979
+ " was r$lr, but we are about to fetch: ",
980
+ "r$log_entry->{revision}!\n";
981
+ }
982
+ if (my $c = $self->rev_map_get($log_entry->{revision})) {
983
+ croak "$log_entry->{revision} = $c already exists! ",
984
+ "Why are we refetching it?\n";
985
+ }
986
+ my $old_env = set_commit_header_env($log_entry);
987
+ my $tree = $log_entry->{tree};
988
+ if (!defined $tree) {
989
+ $tree = $self->tmp_index_do(sub {
990
+ command_oneline('write-tree') });
991
+ }
992
+ die "Tree is not a valid sha1: $tree\n" if $tree !~ /^$::sha1$/o;
993
+
994
+ my @exec = ('git', 'commit-tree', $tree);
995
+ foreach ($self->get_commit_parents($log_entry)) {
996
+ push @exec, '-p', $_;
997
+ }
998
+ defined(my $pid = open3(my $msg_fh, my $out_fh, '>&STDERR', @exec))
999
+ or croak $!;
1000
+ binmode $msg_fh;
1001
+
1002
+ # we always get UTF-8 from SVN, but we may want our commits in
1003
+ # a different encoding.
1004
+ if (my $enc = Git::config('i18n.commitencoding')) {
1005
+ require Encode;
1006
+ Encode::from_to($log_entry->{log}, 'UTF-8', $enc);
1007
+ }
1008
+ print $msg_fh $log_entry->{log} or croak $!;
1009
+ restore_commit_header_env($old_env);
1010
+ unless ($self->no_metadata) {
1011
+ print $msg_fh "\ngit-svn-id: $log_entry->{metadata}\n"
1012
+ or croak $!;
1013
+ }
1014
+ $msg_fh->flush == 0 or croak $!;
1015
+ close $msg_fh or croak $!;
1016
+ chomp(my $commit = do { local $/; <$out_fh> });
1017
+ close $out_fh or croak $!;
1018
+ waitpid $pid, 0;
1019
+ croak $? if $?;
1020
+ if ($commit !~ /^$::sha1$/o) {
1021
+ die "Failed to commit, invalid sha1: $commit\n";
1022
+ }
1023
+
1024
+ $self->rev_map_set($log_entry->{revision}, $commit, 1);
1025
+
1026
+ $self->{last_rev} = $log_entry->{revision};
1027
+ $self->{last_commit} = $commit;
1028
+ print "r$log_entry->{revision}" unless $::_q > 1;
1029
+ if (defined $log_entry->{svm_revision}) {
1030
+ print " (\@$log_entry->{svm_revision})" unless $::_q > 1;
1031
+ $self->rev_map_set($log_entry->{svm_revision}, $commit,
1032
+ 0, $self->svm_uuid);
1033
+ }
1034
+ print " = $commit ($self->{ref_id})\n" unless $::_q > 1;
1035
+ if (--$_gc_nr == 0) {
1036
+ $_gc_nr = $_gc_period;
1037
+ gc();
1038
+ }
1039
+ return $commit;
1040
+}
1041
+
1042
+sub match_paths {
1043
+ my ($self, $paths, $r) = @_;
1044
+ return 1 if $self->{path} eq '';
1045
+ if (my $path = $paths->{"/$self->{path}"}) {
1046
+ return ($path->{action} eq 'D') ? 0 : 1;
1047
+ }
1048
+ $self->{path_regex} ||= qr/^\/\Q$self->{path}\E\//;
1049
+ if (grep /$self->{path_regex}/, keys %$paths) {
1050
+ return 1;
1051
+ }
1052
+ my $c = '';
1053
+ foreach (split m#/#, $self->{path}) {
1054
+ $c .= "/$_";
1055
+ next unless ($paths->{$c} &&
1056
+ ($paths->{$c}->{action} =~ /^[AR]$/));
1057
+ if ($self->ra->check_path($self->{path}, $r) ==
1058
+ $SVN::Node::dir) {
1059
+ return 1;
1060
+ }
1061
+ }
1062
+ return 0;
1063
+}
1064
+
1065
+sub find_parent_branch {
1066
+ my ($self, $paths, $rev) = @_;
1067
+ return undef unless $self->follow_parent;
1068
+ unless (defined $paths) {
1069
+ my $err_handler = $SVN::Error::handler;
1070
+ $SVN::Error::handler = \&Git::SVN::Ra::skip_unknown_revs;
1071
+ $self->ra->get_log([$self->{path}], $rev, $rev, 0, 1, 1,
1072
+ sub { $paths = $_[0] });
1073
+ $SVN::Error::handler = $err_handler;
1074
+ }
1075
+ return undef unless defined $paths;
1076
+
1077
+ # look for a parent from another branch:
1078
+ my @b_path_components = split m#/#, $self->{path};
1079
+ my @a_path_components;
1080
+ my $i;
1081
+ while (@b_path_components) {
1082
+ $i = $paths->{'/'.join('/', @b_path_components)};
1083
+ last if $i && defined $i->{copyfrom_path};
1084
+ unshift(@a_path_components, pop(@b_path_components));
1085
+ }
1086
+ return undef unless defined $i && defined $i->{copyfrom_path};
1087
+ my $branch_from = $i->{copyfrom_path};
1088
+ if (@a_path_components) {
1089
+ print STDERR "branch_from: $branch_from => ";
1090
+ $branch_from .= '/'.join('/', @a_path_components);
1091
+ print STDERR $branch_from, "\n";
1092
+ }
1093
+ my $r = $i->{copyfrom_rev};
1094
+ my $repos_root = $self->ra->{repos_root};
1095
+ my $url = $self->ra->{url};
1096
+ my $new_url = $url . $branch_from;
1097
+ print STDERR "Found possible branch point: ",
1098
+ "$new_url => ", $self->full_url, ", $r\n"
1099
+ unless $::_q > 1;
1100
+ $branch_from =~ s#^/##;
1101
+ my $gs = $self->other_gs($new_url, $url,
1102
+ $branch_from, $r, $self->{ref_id});
1103
+ my ($r0, $parent) = $gs->find_rev_before($r, 1);
1104
+ {
1105
+ my ($base, $head);
1106
+ if (!defined $r0 || !defined $parent) {
1107
+ ($base, $head) = parse_revision_argument(0, $r);
1108
+ } else {
1109
+ if ($r0 < $r) {
1110
+ $gs->ra->get_log([$gs->{path}], $r0 + 1, $r, 1,
1111
+ 0, 1, sub { $base = $_[1] - 1 });
1112
+ }
1113
+ }
1114
+ if (defined $base && $base <= $r) {
1115
+ $gs->fetch($base, $r);
1116
+ }
1117
+ ($r0, $parent) = $gs->find_rev_before($r, 1);
1118
+ }
1119
+ if (defined $r0 && defined $parent) {
1120
+ print STDERR "Found branch parent: ($self->{ref_id}) $parent\n"
1121
+ unless $::_q > 1;
1122
+ my $ed;
1123
+ if ($self->ra->can_do_switch) {
1124
+ $self->assert_index_clean($parent);
1125
+ print STDERR "Following parent with do_switch\n"
1126
+ unless $::_q > 1;
1127
+ # do_switch works with svn/trunk >= r22312, but that
1128
+ # is not included with SVN 1.4.3 (the latest version
1129
+ # at the moment), so we can't rely on it
1130
+ $self->{last_rev} = $r0;
1131
+ $self->{last_commit} = $parent;
1132
+ $ed = Git::SVN::Fetcher->new($self, $gs->{path});
1133
+ $gs->ra->gs_do_switch($r0, $rev, $gs,
1134
+ $self->full_url, $ed)
1135
+ or die "SVN connection failed somewhere...\n";
1136
+ } elsif ($self->ra->trees_match($new_url, $r0,
1137
+ $self->full_url, $rev)) {
1138
+ print STDERR "Trees match:\n",
1139
+ " $new_url\@$r0\n",
1140
+ " ${\$self->full_url}\@$rev\n",
1141
+ "Following parent with no changes\n"
1142
+ unless $::_q > 1;
1143
+ $self->tmp_index_do(sub {
1144
+ command_noisy('read-tree', $parent);
1145
+ });
1146
+ $self->{last_commit} = $parent;
1147
+ } else {
1148
+ print STDERR "Following parent with do_update\n"
1149
+ unless $::_q > 1;
1150
+ $ed = Git::SVN::Fetcher->new($self);
1151
+ $self->ra->gs_do_update($rev, $rev, $self, $ed)
1152
+ or die "SVN connection failed somewhere...\n";
1153
+ }
1154
+ print STDERR "Successfully followed parent\n" unless $::_q > 1;
1155
+ return $self->make_log_entry($rev, [$parent], $ed);
1156
+ }
1157
+ return undef;
1158
+}
1159
+
1160
+sub do_fetch {
1161
+ my ($self, $paths, $rev) = @_;
1162
+ my $ed;
1163
+ my ($last_rev, @parents);
1164
+ if (my $lc = $self->last_commit) {
1165
+ # we can have a branch that was deleted, then re-added
1166
+ # under the same name but copied from another path, in
1167
+ # which case we'll have multiple parents (we don't
1168
+ # want to break the original ref, nor lose copypath info):
1169
+ if (my $log_entry = $self->find_parent_branch($paths, $rev)) {
1170
+ push @{$log_entry->{parents}}, $lc;
1171
+ return $log_entry;
1172
+ }
1173
+ $ed = Git::SVN::Fetcher->new($self);
1174
+ $last_rev = $self->{last_rev};
1175
+ $ed->{c} = $lc;
1176
+ @parents = ($lc);
1177
+ } else {
1178
+ $last_rev = $rev;
1179
+ if (my $log_entry = $self->find_parent_branch($paths, $rev)) {
1180
+ return $log_entry;
1181
+ }
1182
+ $ed = Git::SVN::Fetcher->new($self);
1183
+ }
1184
+ unless ($self->ra->gs_do_update($last_rev, $rev, $self, $ed)) {
1185
+ die "SVN connection failed somewhere...\n";
1186
+ }
1187
+ $self->make_log_entry($rev, \@parents, $ed);
1188
+}
1189
+
1190
+sub mkemptydirs {
1191
+ my ($self, $r) = @_;
1192
+
1193
+ sub scan {
1194
+ my ($r, $empty_dirs, $line) = @_;
1195
+ if (defined $r && $line =~ /^r(\d+)$/) {
1196
+ return 0 if $1 > $r;
1197
+ } elsif ($line =~ /^ \+empty_dir: (.+)$/) {
1198
+ $empty_dirs->{$1} = 1;
1199
+ } elsif ($line =~ /^ \-empty_dir: (.+)$/) {
1200
+ my @d = grep {m[^\Q$1\E(/|$)]} (keys %$empty_dirs);
1201
+ delete @$empty_dirs{@d};
1202
+ }
1203
+ 1; # continue
1204
+ };
1205
+
1206
+ my %empty_dirs = ();
1207
+ my $gz_file = "$self->{dir}/unhandled.log.gz";
1208
+ if (-f $gz_file) {
1209
+ if (!can_compress()) {
1210
+ warn "Compress::Zlib could not be found; ",
1211
+ "empty directories in $gz_file will not be read\n";
1212
+ } else {
1213
+ my $gz = Compress::Zlib::gzopen($gz_file, "rb") or
1214
+ die "Unable to open $gz_file: $!\n";
1215
+ my $line;
1216
+ while ($gz->gzreadline($line) > 0) {
1217
+ scan($r, \%empty_dirs, $line) or last;
1218
+ }
1219
+ $gz->gzclose;
1220
+ }
1221
+ }
1222
+
1223
+ if (open my $fh, '<', "$self->{dir}/unhandled.log") {
1224
+ binmode $fh or croak "binmode: $!";
1225
+ while (<$fh>) {
1226
+ scan($r, \%empty_dirs, $_) or last;
1227
+ }
1228
+ close $fh;
1229
+ }
1230
+
1231
+ my $strip = qr/\A\Q$self->{path}\E(?:\/|$)/;
1232
+ foreach my $d (sort keys %empty_dirs) {
1233
+ $d = uri_decode($d);
1234
+ $d =~ s/$strip//;
1235
+ next unless length($d);
1236
+ next if -d $d;
1237
+ if (-e $d) {
1238
+ warn "$d exists but is not a directory\n";
1239
+ } else {
1240
+ print "creating empty directory: $d\n";
1241
+ mkpath([$d]);
1242
+ }
1243
+ }
1244
+}
1245
+
1246
+sub get_untracked {
1247
+ my ($self, $ed) = @_;
1248
+ my @out;
1249
+ my $h = $ed->{empty};
1250
+ foreach (sort keys %$h) {
1251
+ my $act = $h->{$_} ? '+empty_dir' : '-empty_dir';
1252
+ push @out, " $act: " . uri_encode($_);
1253
+ warn "W: $act: $_\n";
1254
+ }
1255
+ foreach my $t (qw/dir_prop file_prop/) {
1256
+ $h = $ed->{$t} or next;
1257
+ foreach my $path (sort keys %$h) {
1258
+ my $ppath = $path eq '' ? '.' : $path;
1259
+ foreach my $prop (sort keys %{$h->{$path}}) {
1260
+ next if $SKIP_PROP{$prop};
1261
+ my $v = $h->{$path}->{$prop};
1262
+ my $t_ppath_prop = "$t: " .
1263
+ uri_encode($ppath) . ' ' .
1264
+ uri_encode($prop);
1265
+ if (defined $v) {
1266
+ push @out, " +$t_ppath_prop " .
1267
+ uri_encode($v);
1268
+ } else {
1269
+ push @out, " -$t_ppath_prop";
1270
+ }
1271
+ }
1272
+ }
1273
+ }
1274
+ foreach my $t (qw/absent_file absent_directory/) {
1275
+ $h = $ed->{$t} or next;
1276
+ foreach my $parent (sort keys %$h) {
1277
+ foreach my $path (sort @{$h->{$parent}}) {
1278
+ push @out, " $t: " .
1279
+ uri_encode("$parent/$path");
1280
+ warn "W: $t: $parent/$path ",
1281
+ "Insufficient permissions?\n";
1282
+ }
1283
+ }
1284
+ }
1285
+ \@out;
1286
+}
1287
+
1288
+sub get_tz {
1289
+ # some systmes don't handle or mishandle %z, so be creative.
1290
+ my $t = shift || time;
1291
+ my $gm = timelocal(gmtime($t));
1292
+ my $sign = qw( + + - )[ $t <=> $gm ];
1293
+ return sprintf("%s%02d%02d", $sign, (gmtime(abs($t - $gm)))[2,1]);
1294
+}
1295
+
1296
+# parse_svn_date(DATE)
1297
+# --------------------
1298
+# Given a date (in UTC) from Subversion, return a string in the format
1299
+# "<TZ Offset> <local date/time>" that Git will use.
1300
+#
1301
+# By default the parsed date will be in UTC; if $Git::SVN::_localtime
1302
+# is true we'll convert it to the local timezone instead.
1303
+sub parse_svn_date {
1304
+ my $date = shift || return '+0000 1970-01-01 00:00:00';
1305
+ my ($Y,$m,$d,$H,$M,$S) = ($date =~ /^(\d{4})\-(\d\d)\-(\d\d)T
1306
+ (\d\d)\:(\d\d)\:(\d\d)\.\d*Z$/x) or
1307
+ croak "Unable to parse date: $date\n";
1308
+ my $parsed_date; # Set next.
1309
+
1310
+ if ($Git::SVN::_localtime) {
1311
+ # Translate the Subversion datetime to an epoch time.
1312
+ # Begin by switching ourselves to $date's timezone, UTC.
1313
+ my $old_env_TZ = $ENV{TZ};
1314
+ $ENV{TZ} = 'UTC';
1315
+
1316
+ my $epoch_in_UTC =
1317
+ POSIX::strftime('%s', $S, $M, $H, $d, $m - 1, $Y - 1900);
1318
+
1319
+ # Determine our local timezone (including DST) at the
1320
+ # time of $epoch_in_UTC. $Git::SVN::Log::TZ stored the
1321
+ # value of TZ, if any, at the time we were run.
1322
+ if (defined $Git::SVN::Log::TZ) {
1323
+ $ENV{TZ} = $Git::SVN::Log::TZ;
1324
+ } else {
1325
+ delete $ENV{TZ};
1326
+ }
1327
+
1328
+ my $our_TZ = get_tz();
1329
+
1330
+ # This converts $epoch_in_UTC into our local timezone.
1331
+ my ($sec, $min, $hour, $mday, $mon, $year,
1332
+ $wday, $yday, $isdst) = localtime($epoch_in_UTC);
1333
+
1334
+ $parsed_date = sprintf('%s %04d-%02d-%02d %02d:%02d:%02d',
1335
+ $our_TZ, $year + 1900, $mon + 1,
1336
+ $mday, $hour, $min, $sec);
1337
+
1338
+ # Reset us to the timezone in effect when we entered
1339
+ # this routine.
1340
+ if (defined $old_env_TZ) {
1341
+ $ENV{TZ} = $old_env_TZ;
1342
+ } else {
1343
+ delete $ENV{TZ};
1344
+ }
1345
+ } else {
1346
+ $parsed_date = "+0000 $Y-$m-$d $H:$M:$S";
1347
+ }
1348
+
1349
+ return $parsed_date;
1350
+}
1351
+
1352
+sub other_gs {
1353
+ my ($self, $new_url, $url,
1354
+ $branch_from, $r, $old_ref_id) = @_;
1355
+ my $gs = Git::SVN->find_by_url($new_url, $url, $branch_from);
1356
+ unless ($gs) {
1357
+ my $ref_id = $old_ref_id;
1358
+ $ref_id =~ s/\@\d+-*$//;
1359
+ $ref_id .= "\@$r";
1360
+ # just grow a tail if we're not unique enough :x
1361
+ $ref_id .= '-' while find_ref($ref_id);
1362
+ my ($u, $p, $repo_id) = ($new_url, '', $ref_id);
1363
+ if ($u =~ s#^\Q$url\E(/|$)##) {
1364
+ $p = $u;
1365
+ $u = $url;
1366
+ $repo_id = $self->{repo_id};
1367
+ }
1368
+ while (1) {
1369
+ # It is possible to tag two different subdirectories at
1370
+ # the same revision. If the url for an existing ref
1371
+ # does not match, we must either find a ref with a
1372
+ # matching url or create a new ref by growing a tail.
1373
+ $gs = Git::SVN->init($u, $p, $repo_id, $ref_id, 1);
1374
+ my (undef, $max_commit) = $gs->rev_map_max(1);
1375
+ last if (!$max_commit);
1376
+ my ($url) = ::cmt_metadata($max_commit);
1377
+ last if ($url eq $gs->metadata_url);
1378
+ $ref_id .= '-';
1379
+ }
1380
+ print STDERR "Initializing parent: $ref_id\n" unless $::_q > 1;
1381
+ }
1382
+ $gs
1383
+}
1384
+
1385
+sub call_authors_prog {
1386
+ my ($orig_author) = @_;
1387
+ $orig_author = command_oneline('rev-parse', '--sq-quote', $orig_author);
1388
+ my $author = `$::_authors_prog $orig_author`;
1389
+ if ($? != 0) {
1390
+ die "$::_authors_prog failed with exit code $?\n"
1391
+ }
1392
+ if ($author =~ /^\s*(.+?)\s*<(.*)>\s*$/) {
1393
+ my ($name, $email) = ($1, $2);
1394
+ $email = undef if length $2 == 0;
1395
+ return [$name, $email];
1396
+ } else {
1397
+ die "Author: $orig_author: $::_authors_prog returned "
1398
+ . "invalid author format: $author\n";
1399
+ }
1400
+}
1401
+
1402
+sub check_author {
1403
+ my ($author) = @_;
1404
+ if (!defined $author || length $author == 0) {
1405
+ $author = '(no author)';
1406
+ }
1407
+ if (!defined $::users{$author}) {
1408
+ if (defined $::_authors_prog) {
1409
+ $::users{$author} = call_authors_prog($author);
1410
+ } elsif (defined $::_authors) {
1411
+ die "Author: $author not defined in $::_authors file\n";
1412
+ }
1413
+ }
1414
+ $author;
1415
+}
1416
+
1417
+sub find_extra_svk_parents {
1418
+ my ($self, $ed, $tickets, $parents) = @_;
1419
+ # aha! svk:merge property changed...
1420
+ my @tickets = split "\n", $tickets;
1421
+ my @known_parents;
1422
+ for my $ticket ( @tickets ) {
1423
+ my ($uuid, $path, $rev) = split /:/, $ticket;
1424
+ if ( $uuid eq $self->ra_uuid ) {
1425
+ my $url = $self->{url};
1426
+ my $repos_root = $url;
1427
+ my $branch_from = $path;
1428
+ $branch_from =~ s{^/}{};
1429
+ my $gs = $self->other_gs($repos_root."/".$branch_from,
1430
+ $url,
1431
+ $branch_from,
1432
+ $rev,
1433
+ $self->{ref_id});
1434
+ if ( my $commit = $gs->rev_map_get($rev, $uuid) ) {
1435
+ # wahey! we found it, but it might be
1436
+ # an old one (!)
1437
+ push @known_parents, [ $rev, $commit ];
1438
+ }
1439
+ }
1440
+ }
1441
+ # Ordering matters; highest-numbered commit merge tickets
1442
+ # first, as they may account for later merge ticket additions
1443
+ # or changes.
1444
+ @known_parents = map {$_->[1]} sort {$b->[0] <=> $a->[0]} @known_parents;
1445
+ for my $parent ( @known_parents ) {
1446
+ my @cmd = ('rev-list', $parent, map { "^$_" } @$parents );
1447
+ my ($msg_fh, $ctx) = command_output_pipe(@cmd);
1448
+ my $new;
1449
+ while ( <$msg_fh> ) {
1450
+ $new=1;last;
1451
+ }
1452
+ command_close_pipe($msg_fh, $ctx);
1453
+ if ( $new ) {
1454
+ print STDERR
1455
+ "Found merge parent (svk:merge ticket): $parent\n";
1456
+ push @$parents, $parent;
1457
+ }
1458
+ }
1459
+}
1460
+
1461
+sub lookup_svn_merge {
1462
+ my $uuid = shift;
1463
+ my $url = shift;
1464
+ my $merge = shift;
1465
+
1466
+ my ($source, $revs) = split ":", $merge;
1467
+ my $path = $source;
1468
+ $path =~ s{^/}{};
1469
+ my $gs = Git::SVN->find_by_url($url.$source, $url, $path);
1470
+ if ( !$gs ) {
1471
+ warn "Couldn't find revmap for $url$source\n";
1472
+ return;
1473
+ }
1474
+ my @ranges = split ",", $revs;
1475
+ my ($tip, $tip_commit);
1476
+ my @merged_commit_ranges;
1477
+ # find the tip
1478
+ for my $range ( @ranges ) {
1479
+ my ($bottom, $top) = split "-", $range;
1480
+ $top ||= $bottom;
1481
+ my $bottom_commit = $gs->find_rev_after( $bottom, 1, $top );
1482
+ my $top_commit = $gs->find_rev_before( $top, 1, $bottom );
1483
+
1484
+ unless ($top_commit and $bottom_commit) {
1485
+ warn "W:unknown path/rev in svn:mergeinfo "
1486
+ ."dirprop: $source:$range\n";
1487
+ next;
1488
+ }
1489
+
1490
+ if (scalar(command('rev-parse', "$bottom_commit^@"))) {
1491
+ push @merged_commit_ranges,
1492
+ "$bottom_commit^..$top_commit";
1493
+ } else {
1494
+ push @merged_commit_ranges, "$top_commit";
1495
+ }
1496
+
1497
+ if ( !defined $tip or $top > $tip ) {
1498
+ $tip = $top;
1499
+ $tip_commit = $top_commit;
1500
+ }
1501
+ }
1502
+ return ($tip_commit, @merged_commit_ranges);
1503
+}
1504
+
1505
+sub _rev_list {
1506
+ my ($msg_fh, $ctx) = command_output_pipe(
1507
+ "rev-list", @_,
1508
+ );
1509
+ my @rv;
1510
+ while ( <$msg_fh> ) {
1511
+ chomp;
1512
+ push @rv, $_;
1513
+ }
1514
+ command_close_pipe($msg_fh, $ctx);
1515
+ @rv;
1516
+}
1517
+
1518
+sub check_cherry_pick {
1519
+ my $base = shift;
1520
+ my $tip = shift;
1521
+ my $parents = shift;
1522
+ my @ranges = @_;
1523
+ my %commits = map { $_ => 1 }
1524
+ _rev_list("--no-merges", $tip, "--not", $base, @$parents, "--");
1525
+ for my $range ( @ranges ) {
1526
+ delete @commits{_rev_list($range, "--")};
1527
+ }
1528
+ for my $commit (keys %commits) {
1529
+ if (has_no_changes($commit)) {
1530
+ delete $commits{$commit};
1531
+ }
1532
+ }
1533
+ return (keys %commits);
1534
+}
1535
+
1536
+sub has_no_changes {
1537
+ my $commit = shift;
1538
+
1539
+ my @revs = split / /, command_oneline(
1540
+ qw(rev-list --parents -1 -m), $commit);
1541
+
1542
+ # Commits with no parents, e.g. the start of a partial branch,
1543
+ # have changes by definition.
1544
+ return 1 if (@revs < 2);
1545
+
1546
+ # Commits with multiple parents, e.g a merge, have no changes
1547
+ # by definition.
1548
+ return 0 if (@revs > 2);
1549
+
1550
+ return (command_oneline("rev-parse", "$commit^{tree}") eq
1551
+ command_oneline("rev-parse", "$commit~1^{tree}"));
1552
+}
1553
+
1554
+sub tie_for_persistent_memoization {
1555
+ my $hash = shift;
1556
+ my $path = shift;
1557
+
1558
+ if ($can_use_yaml) {
1559
+ tie %$hash => 'Git::SVN::Memoize::YAML', "$path.yaml";
1560
+ } else {
1561
+ tie %$hash => 'Memoize::Storable', "$path.db", 'nstore';
1562
+ }
1563
+}
1564
+
1565
+# The GIT_DIR environment variable is not always set until after the command
1566
+# line arguments are processed, so we can't memoize in a BEGIN block.
1567
+{
1568
+ my $memoized = 0;
1569
+
1570
+ sub memoize_svn_mergeinfo_functions {
1571
+ return if $memoized;
1572
+ $memoized = 1;
1573
+
1574
+ my $cache_path = "$ENV{GIT_DIR}/svn/.caches/";
1575
+ mkpath([$cache_path]) unless -d $cache_path;
1576
+
1577
+ my %lookup_svn_merge_cache;
1578
+ my %check_cherry_pick_cache;
1579
+ my %has_no_changes_cache;
1580
+
1581
+ tie_for_persistent_memoization(\%lookup_svn_merge_cache,
1582
+ "$cache_path/lookup_svn_merge");
1583
+ memoize 'lookup_svn_merge',
1584
+ SCALAR_CACHE => 'FAULT',
1585
+ LIST_CACHE => ['HASH' => \%lookup_svn_merge_cache],
1586
+ ;
1587
+
1588
+ tie_for_persistent_memoization(\%check_cherry_pick_cache,
1589
+ "$cache_path/check_cherry_pick");
1590
+ memoize 'check_cherry_pick',
1591
+ SCALAR_CACHE => 'FAULT',
1592
+ LIST_CACHE => ['HASH' => \%check_cherry_pick_cache],
1593
+ ;
1594
+
1595
+ tie_for_persistent_memoization(\%has_no_changes_cache,
1596
+ "$cache_path/has_no_changes");
1597
+ memoize 'has_no_changes',
1598
+ SCALAR_CACHE => ['HASH' => \%has_no_changes_cache],
1599
+ LIST_CACHE => 'FAULT',
1600
+ ;
1601
+ }
1602
+
1603
+ sub unmemoize_svn_mergeinfo_functions {
1604
+ return if not $memoized;
1605
+ $memoized = 0;
1606
+
1607
+ Memoize::unmemoize 'lookup_svn_merge';
1608
+ Memoize::unmemoize 'check_cherry_pick';
1609
+ Memoize::unmemoize 'has_no_changes';
1610
+ }
1611
+
1612
+ Memoize::memoize 'Git::SVN::repos_root';
1613
+}
1614
+
1615
+END {
1616
+ # Force cache writeout explicitly instead of waiting for
1617
+ # global destruction to avoid segfault in Storable:
1618
+ # http://rt.cpan.org/Public/Bug/Display.html?id=36087
1619
+ unmemoize_svn_mergeinfo_functions();
1620
+}
1621
+
1622
+sub parents_exclude {
1623
+ my $parents = shift;
1624
+ my @commits = @_;
1625
+ return unless @commits;
1626
+
1627
+ my @excluded;
1628
+ my $excluded;
1629
+ do {
1630
+ my @cmd = ('rev-list', "-1", @commits, "--not", @$parents );
1631
+ $excluded = command_oneline(@cmd);
1632
+ if ( $excluded ) {
1633
+ my @new;
1634
+ my $found;
1635
+ for my $commit ( @commits ) {
1636
+ if ( $commit eq $excluded ) {
1637
+ push @excluded, $commit;
1638
+ $found++;
1639
+ last;
1640
+ }
1641
+ else {
1642
+ push @new, $commit;
1643
+ }
1644
+ }
1645
+ die "saw commit '$excluded' in rev-list output, "
1646
+ ."but we didn't ask for that commit (wanted: @commits --not @$parents)"
1647
+ unless $found;
1648
+ @commits = @new;
1649
+ }
1650
+ }
1651
+ while ($excluded and @commits);
1652
+
1653
+ return @excluded;
1654
+}
1655
+
1656
+
1657
+# note: this function should only be called if the various dirprops
1658
+# have actually changed
1659
+sub find_extra_svn_parents {
1660
+ my ($self, $ed, $mergeinfo, $parents) = @_;
1661
+ # aha! svk:merge property changed...
1662
+
1663
+ memoize_svn_mergeinfo_functions();
1664
+
1665
+ # We first search for merged tips which are not in our
1666
+ # history. Then, we figure out which git revisions are in
1667
+ # that tip, but not this revision. If all of those revisions
1668
+ # are now marked as merge, we can add the tip as a parent.
1669
+ my @merges = split "\n", $mergeinfo;
1670
+ my @merge_tips;
1671
+ my $url = $self->{url};
1672
+ my $uuid = $self->ra_uuid;
1673
+ my %ranges;
1674
+ for my $merge ( @merges ) {
1675
+ my ($tip_commit, @ranges) =
1676
+ lookup_svn_merge( $uuid, $url, $merge );
1677
+ unless (!$tip_commit or
1678
+ grep { $_ eq $tip_commit } @$parents ) {
1679
+ push @merge_tips, $tip_commit;
1680
+ $ranges{$tip_commit} = \@ranges;
1681
+ } else {
1682
+ push @merge_tips, undef;
1683
+ }
1684
+ }
1685
+
1686
+ my %excluded = map { $_ => 1 }
1687
+ parents_exclude($parents, grep { defined } @merge_tips);
1688
+
1689
+ # check merge tips for new parents
1690
+ my @new_parents;
1691
+ for my $merge_tip ( @merge_tips ) {
1692
+ my $spec = shift @merges;
1693
+ next unless $merge_tip and $excluded{$merge_tip};
1694
+
1695
+ my $ranges = $ranges{$merge_tip};
1696
+
1697
+ # check out 'new' tips
1698
+ my $merge_base;
1699
+ eval {
1700
+ $merge_base = command_oneline(
1701
+ "merge-base",
1702
+ @$parents, $merge_tip,
1703
+ );
1704
+ };
1705
+ if ($@) {
1706
+ die "An error occurred during merge-base"
1707
+ unless $@->isa("Git::Error::Command");
1708
+
1709
+ warn "W: Cannot find common ancestor between ".
1710
+ "@$parents and $merge_tip. Ignoring merge info.\n";
1711
+ next;
1712
+ }
1713
+
1714
+ # double check that there are no missing non-merge commits
1715
+ my (@incomplete) = check_cherry_pick(
1716
+ $merge_base, $merge_tip,
1717
+ $parents,
1718
+ @$ranges,
1719
+ );
1720
+
1721
+ if ( @incomplete ) {
1722
+ warn "W:svn cherry-pick ignored ($spec) - missing "
1723
+ .@incomplete." commit(s) (eg $incomplete[0])\n";
1724
+ } else {
1725
+ warn
1726
+ "Found merge parent (svn:mergeinfo prop): ",
1727
+ $merge_tip, "\n";
1728
+ push @new_parents, $merge_tip;
1729
+ }
1730
+ }
1731
+
1732
+ # cater for merges which merge commits from multiple branches
1733
+ if ( @new_parents > 1 ) {
1734
+ for ( my $i = 0; $i <= $#new_parents; $i++ ) {
1735
+ for ( my $j = 0; $j <= $#new_parents; $j++ ) {
1736
+ next if $i == $j;
1737
+ next unless $new_parents[$i];
1738
+ next unless $new_parents[$j];
1739
+ my $revs = command_oneline(
1740
+ "rev-list", "-1",
1741
+ "$new_parents[$i]..$new_parents[$j]",
1742
+ );
1743
+ if ( !$revs ) {
1744
+ undef($new_parents[$j]);
1745
+ }
1746
+ }
1747
+ }
1748
+ }
1749
+ push @$parents, grep { defined } @new_parents;
1750
+}
1751
+
1752
+sub make_log_entry {
1753
+ my ($self, $rev, $parents, $ed) = @_;
1754
+ my $untracked = $self->get_untracked($ed);
1755
+
1756
+ my @parents = @$parents;
1757
+ my $ps = $ed->{path_strip} || "";
1758
+ for my $path ( grep { m/$ps/ } %{$ed->{dir_prop}} ) {
1759
+ my $props = $ed->{dir_prop}{$path};
1760
+ if ( $props->{"svk:merge"} ) {
1761
+ $self->find_extra_svk_parents
1762
+ ($ed, $props->{"svk:merge"}, \@parents);
1763
+ }
1764
+ if ( $props->{"svn:mergeinfo"} ) {
1765
+ $self->find_extra_svn_parents
1766
+ ($ed,
1767
+ $props->{"svn:mergeinfo"},
1768
+ \@parents);
1769
+ }
1770
+ }
1771
+
1772
+ open my $un, '>>', "$self->{dir}/unhandled.log" or croak $!;
1773
+ print $un "r$rev\n" or croak $!;
1774
+ print $un $_, "\n" foreach @$untracked;
1775
+ my %log_entry = ( parents => \@parents, revision => $rev,
1776
+ log => '');
1777
+
1778
+ my $headrev;
1779
+ my $logged = delete $self->{logged_rev_props};
1780
+ if (!$logged || $self->{-want_revprops}) {
1781
+ my $rp = $self->ra->rev_proplist($rev);
1782
+ foreach (sort keys %$rp) {
1783
+ my $v = $rp->{$_};
1784
+ if (/^svn:(author|date|log)$/) {
1785
+ $log_entry{$1} = $v;
1786
+ } elsif ($_ eq 'svm:headrev') {
1787
+ $headrev = $v;
1788
+ } else {
1789
+ print $un " rev_prop: ", uri_encode($_), ' ',
1790
+ uri_encode($v), "\n";
1791
+ }
1792
+ }
1793
+ } else {
1794
+ map { $log_entry{$_} = $logged->{$_} } keys %$logged;
1795
+ }
1796
+ close $un or croak $!;
1797
+
1798
+ $log_entry{date} = parse_svn_date($log_entry{date});
1799
+ $log_entry{log} .= "\n";
1800
+ my $author = $log_entry{author} = check_author($log_entry{author});
1801
+ my ($name, $email) = defined $::users{$author} ? @{$::users{$author}}
1802
+ : ($author, undef);
1803
+
1804
+ my ($commit_name, $commit_email) = ($name, $email);
1805
+ if ($_use_log_author) {
1806
+ my $name_field;
1807
+ if ($log_entry{log} =~ /From:\s+(.*\S)\s*\n/i) {
1808
+ $name_field = $1;
1809
+ } elsif ($log_entry{log} =~ /Signed-off-by:\s+(.*\S)\s*\n/i) {
1810
+ $name_field = $1;
1811
+ }
1812
+ if (!defined $name_field) {
1813
+ if (!defined $email) {
1814
+ $email = $name;
1815
+ }
1816
+ } elsif ($name_field =~ /(.*?)\s+<(.*)>/) {
1817
+ ($name, $email) = ($1, $2);
1818
+ } elsif ($name_field =~ /(.*)@/) {
1819
+ ($name, $email) = ($1, $name_field);
1820
+ } else {
1821
+ ($name, $email) = ($name_field, $name_field);
1822
+ }
1823
+ }
1824
+ if (defined $headrev && $self->use_svm_props) {
1825
+ if ($self->rewrite_root) {
1826
+ die "Can't have both 'useSvmProps' and 'rewriteRoot' ",
1827
+ "options set!\n";
1828
+ }
1829
+ if ($self->rewrite_uuid) {
1830
+ die "Can't have both 'useSvmProps' and 'rewriteUUID' ",
1831
+ "options set!\n";
1832
+ }
1833
+ my ($uuid, $r) = $headrev =~ m{^([a-f\d\-]{30,}):(\d+)$}i;
1834
+ # we don't want "SVM: initializing mirror for junk" ...
1835
+ return undef if $r == 0;
1836
+ my $svm = $self->svm;
1837
+ if ($uuid ne $svm->{uuid}) {
1838
+ die "UUID mismatch on SVM path:\n",
1839
+ "expected: $svm->{uuid}\n",
1840
+ " got: $uuid\n";
1841
+ }
1842
+ my $full_url = $self->full_url;
1843
+ $full_url =~ s#^\Q$svm->{replace}\E(/|$)#$svm->{source}$1# or
1844
+ die "Failed to replace '$svm->{replace}' with ",
1845
+ "'$svm->{source}' in $full_url\n";
1846
+ # throw away username for storing in records
1847
+ remove_username($full_url);
1848
+ $log_entry{metadata} = "$full_url\@$r $uuid";
1849
+ $log_entry{svm_revision} = $r;
1850
+ $email ||= "$author\@$uuid";
1851
+ $commit_email ||= "$author\@$uuid";
1852
+ } elsif ($self->use_svnsync_props) {
1853
+ my $full_url = $self->svnsync->{url};
1854
+ $full_url .= "/$self->{path}" if length $self->{path};
1855
+ remove_username($full_url);
1856
+ my $uuid = $self->svnsync->{uuid};
1857
+ $log_entry{metadata} = "$full_url\@$rev $uuid";
1858
+ $email ||= "$author\@$uuid";
1859
+ $commit_email ||= "$author\@$uuid";
1860
+ } else {
1861
+ my $url = $self->metadata_url;
1862
+ remove_username($url);
1863
+ my $uuid = $self->rewrite_uuid || $self->ra->get_uuid;
1864
+ $log_entry{metadata} = "$url\@$rev " . $uuid;
1865
+ $email ||= "$author\@" . $uuid;
1866
+ $commit_email ||= "$author\@" . $uuid;
1867
+ }
1868
+ $log_entry{name} = $name;
1869
+ $log_entry{email} = $email;
1870
+ $log_entry{commit_name} = $commit_name;
1871
+ $log_entry{commit_email} = $commit_email;
1872
+ \%log_entry;
1873
+}
1874
+
1875
+sub fetch {
1876
+ my ($self, $min_rev, $max_rev, @parents) = @_;
1877
+ my ($last_rev, $last_commit) = $self->last_rev_commit;
1878
+ my ($base, $head) = $self->get_fetch_range($min_rev, $max_rev);
1879
+ $self->ra->gs_fetch_loop_common($base, $head, [$self]);
1880
+}
1881
+
1882
+sub set_tree_cb {
1883
+ my ($self, $log_entry, $tree, $rev, $date, $author) = @_;
1884
+ $self->{inject_parents} = { $rev => $tree };
1885
+ $self->fetch(undef, undef);
1886
+}
1887
+
1888
+sub set_tree {
1889
+ my ($self, $tree) = (shift, shift);
1890
+ my $log_entry = ::get_commit_entry($tree);
1891
+ unless ($self->{last_rev}) {
1892
+ fatal("Must have an existing revision to commit");
1893
+ }
1894
+ my %ed_opts = ( r => $self->{last_rev},
1895
+ log => $log_entry->{log},
1896
+ ra => $self->ra,
1897
+ tree_a => $self->{last_commit},
1898
+ tree_b => $tree,
1899
+ editor_cb => sub {
1900
+ $self->set_tree_cb($log_entry, $tree, @_) },
1901
+ svn_path => $self->{path} );
1902
+ if (!Git::SVN::Editor->new(\%ed_opts)->apply_diff) {
1903
+ print "No changes\nr$self->{last_rev} = $tree\n";
1904
+ }
1905
+}
1906
+
1907
+sub rebuild_from_rev_db {
1908
+ my ($self, $path) = @_;
1909
+ my $r = -1;
1910
+ open my $fh, '<', $path or croak "open: $!";
1911
+ binmode $fh or croak "binmode: $!";
1912
+ while (<$fh>) {
1913
+ length($_) == 41 or croak "inconsistent size in ($_) != 41";
1914
+ chomp($_);
1915
+ ++$r;
1916
+ next if $_ eq ('0' x 40);
1917
+ $self->rev_map_set($r, $_);
1918
+ print "r$r = $_\n";
1919
+ }
1920
+ close $fh or croak "close: $!";
1921
+ unlink $path or croak "unlink: $!";
1922
+}
1923
+
1924
+sub rebuild {
1925
+ my ($self) = @_;
1926
+ my $map_path = $self->map_path;
1927
+ my $partial = (-e $map_path && ! -z $map_path);
1928
+ return unless ::verify_ref($self->refname.'^0');
1929
+ if (!$partial && ($self->use_svm_props || $self->no_metadata)) {
1930
+ my $rev_db = $self->rev_db_path;
1931
+ $self->rebuild_from_rev_db($rev_db);
1932
+ if ($self->use_svm_props) {
1933
+ my $svm_rev_db = $self->rev_db_path($self->svm_uuid);
1934
+ $self->rebuild_from_rev_db($svm_rev_db);
1935
+ }
1936
+ $self->unlink_rev_db_symlink;
1937
+ return;
1938
+ }
1939
+ print "Rebuilding $map_path ...\n" if (!$partial);
1940
+ my ($base_rev, $head) = ($partial ? $self->rev_map_max_norebuild(1) :
1941
+ (undef, undef));
1942
+ my ($log, $ctx) =
1943
+ command_output_pipe(qw/rev-list --pretty=raw --reverse/,
1944
+ ($head ? "$head.." : "") . $self->refname,
1945
+ '--');
1946
+ my $metadata_url = $self->metadata_url;
1947
+ remove_username($metadata_url);
1948
+ my $svn_uuid = $self->rewrite_uuid || $self->ra_uuid;
1949
+ my $c;
1950
+ while (<$log>) {
1951
+ if ( m{^commit ($::sha1)$} ) {
1952
+ $c = $1;
1953
+ next;
1954
+ }
1955
+ next unless s{^\s*(git-svn-id:)}{$1};
1956
+ my ($url, $rev, $uuid) = ::extract_metadata($_);
1957
+ remove_username($url);
1958
+
1959
+ # ignore merges (from set-tree)
1960
+ next if (!defined $rev || !$uuid);
1961
+
1962
+ # if we merged or otherwise started elsewhere, this is
1963
+ # how we break out of it
1964
+ if (($uuid ne $svn_uuid) ||
1965
+ ($metadata_url && $url && ($url ne $metadata_url))) {
1966
+ next;
1967
+ }
1968
+ if ($partial && $head) {
1969
+ print "Partial-rebuilding $map_path ...\n";
1970
+ print "Currently at $base_rev = $head\n";
1971
+ $head = undef;
1972
+ }
1973
+
1974
+ $self->rev_map_set($rev, $c);
1975
+ print "r$rev = $c\n";
1976
+ }
1977
+ command_close_pipe($log, $ctx);
1978
+ print "Done rebuilding $map_path\n" if (!$partial || !$head);
1979
+ my $rev_db_path = $self->rev_db_path;
1980
+ if (-f $self->rev_db_path) {
1981
+ unlink $self->rev_db_path or croak "unlink: $!";
1982
+ }
1983
+ $self->unlink_rev_db_symlink;
1984
+}
1985
+
1986
+# rev_map:
1987
+# Tie::File seems to be prone to offset errors if revisions get sparse,
1988
+# it's not that fast, either. Tie::File is also not in Perl 5.6. So
1989
+# one of my favorite modules is out :< Next up would be one of the DBM
1990
+# modules, but I'm not sure which is most portable...
1991
+#
1992
+# This is the replacement for the rev_db format, which was too big
1993
+# and inefficient for large repositories with a lot of sparse history
1994
+# (mainly tags)
1995
+#
1996
+# The format is this:
1997
+# - 24 bytes for every record,
1998
+# * 4 bytes for the integer representing an SVN revision number
1999
+# * 20 bytes representing the sha1 of a git commit
2000
+# - No empty padding records like the old format
2001
+# (except the last record, which can be overwritten)
2002
+# - new records are written append-only since SVN revision numbers
2003
+# increase monotonically
2004
+# - lookups on SVN revision number are done via a binary search
2005
+# - Piping the file to xxd -c24 is a good way of dumping it for
2006
+# viewing or editing (piped back through xxd -r), should the need
2007
+# ever arise.
2008
+# - The last record can be padding revision with an all-zero sha1
2009
+# This is used to optimize fetch performance when using multiple
2010
+# "fetch" directives in .git/config
2011
+#
2012
+# These files are disposable unless noMetadata or useSvmProps is set
2013
+
2014
+sub _rev_map_set {
2015
+ my ($fh, $rev, $commit) = @_;
2016
+
2017
+ binmode $fh or croak "binmode: $!";
2018
+ my $size = (stat($fh))[7];
2019
+ ($size % 24) == 0 or croak "inconsistent size: $size";
2020
+
2021
+ my $wr_offset = 0;
2022
+ if ($size > 0) {
2023
+ sysseek($fh, -24, SEEK_END) or croak "seek: $!";
2024
+ my $read = sysread($fh, my $buf, 24) or croak "read: $!";
2025
+ $read == 24 or croak "read only $read bytes (!= 24)";
2026
+ my ($last_rev, $last_commit) = unpack(rev_map_fmt, $buf);
2027
+ if ($last_commit eq ('0' x40)) {
2028
+ if ($size >= 48) {
2029
+ sysseek($fh, -48, SEEK_END) or croak "seek: $!";
2030
+ $read = sysread($fh, $buf, 24) or
2031
+ croak "read: $!";
2032
+ $read == 24 or
2033
+ croak "read only $read bytes (!= 24)";
2034
+ ($last_rev, $last_commit) =
2035
+ unpack(rev_map_fmt, $buf);
2036
+ if ($last_commit eq ('0' x40)) {
2037
+ croak "inconsistent .rev_map\n";
2038
+ }
2039
+ }
2040
+ if ($last_rev >= $rev) {
2041
+ croak "last_rev is higher!: $last_rev >= $rev";
2042
+ }
2043
+ $wr_offset = -24;
2044
+ }
2045
+ }
2046
+ sysseek($fh, $wr_offset, SEEK_END) or croak "seek: $!";
2047
+ syswrite($fh, pack(rev_map_fmt, $rev, $commit), 24) == 24 or
2048
+ croak "write: $!";
2049
+}
2050
+
2051
+sub _rev_map_reset {
2052
+ my ($fh, $rev, $commit) = @_;
2053
+ my $c = _rev_map_get($fh, $rev);
2054
+ $c eq $commit or die "_rev_map_reset(@_) commit $c does not match!\n";
2055
+ my $offset = sysseek($fh, 0, SEEK_CUR) or croak "seek: $!";
2056
+ truncate $fh, $offset or croak "truncate: $!";
2057
+}
2058
+
2059
+sub mkfile {
2060
+ my ($path) = @_;
2061
+ unless (-e $path) {
2062
+ my ($dir, $base) = ($path =~ m#^(.*?)/?([^/]+)$#);
2063
+ mkpath([$dir]) unless -d $dir;
2064
+ open my $fh, '>>', $path or die "Couldn't create $path: $!\n";
2065
+ close $fh or die "Couldn't close (create) $path: $!\n";
2066
+ }
2067
+}
2068
+
2069
+sub rev_map_set {
2070
+ my ($self, $rev, $commit, $update_ref, $uuid) = @_;
2071
+ defined $commit or die "missing arg3\n";
2072
+ length $commit == 40 or die "arg3 must be a full SHA1 hexsum\n";
2073
+ my $db = $self->map_path($uuid);
2074
+ my $db_lock = "$db.lock";
2075
+ my $sigmask;
2076
+ $update_ref ||= 0;
2077
+ if ($update_ref) {
2078
+ $sigmask = POSIX::SigSet->new();
2079
+ my $signew = POSIX::SigSet->new(SIGINT, SIGHUP, SIGTERM,
2080
+ SIGALRM, SIGUSR1, SIGUSR2);
2081
+ sigprocmask(SIG_BLOCK, $signew, $sigmask) or
2082
+ croak "Can't block signals: $!";
2083
+ }
2084
+ mkfile($db);
2085
+
2086
+ $LOCKFILES{$db_lock} = 1;
2087
+ my $sync;
2088
+ # both of these options make our .rev_db file very, very important
2089
+ # and we can't afford to lose it because rebuild() won't work
2090
+ if ($self->use_svm_props || $self->no_metadata) {
2091
+ $sync = 1;
2092
+ copy($db, $db_lock) or die "rev_map_set(@_): ",
2093
+ "Failed to copy: ",
2094
+ "$db => $db_lock ($!)\n";
2095
+ } else {
2096
+ rename $db, $db_lock or die "rev_map_set(@_): ",
2097
+ "Failed to rename: ",
2098
+ "$db => $db_lock ($!)\n";
2099
+ }
2100
+
2101
+ sysopen(my $fh, $db_lock, O_RDWR | O_CREAT)
2102
+ or croak "Couldn't open $db_lock: $!\n";
2103
+ $update_ref eq 'reset' ? _rev_map_reset($fh, $rev, $commit) :
2104
+ _rev_map_set($fh, $rev, $commit);
2105
+ if ($sync) {
2106
+ $fh->flush or die "Couldn't flush $db_lock: $!\n";
2107
+ $fh->sync or die "Couldn't sync $db_lock: $!\n";
2108
+ }
2109
+ close $fh or croak $!;
2110
+ if ($update_ref) {
2111
+ $_head = $self;
2112
+ my $note = "";
2113
+ $note = " ($update_ref)" if ($update_ref !~ /^\d*$/);
2114
+ command_noisy('update-ref', '-m', "r$rev$note",
2115
+ $self->refname, $commit);
2116
+ }
2117
+ rename $db_lock, $db or die "rev_map_set(@_): ", "Failed to rename: ",
2118
+ "$db_lock => $db ($!)\n";
2119
+ delete $LOCKFILES{$db_lock};
2120
+ if ($update_ref) {
2121
+ sigprocmask(SIG_SETMASK, $sigmask) or
2122
+ croak "Can't restore signal mask: $!";
2123
+ }
2124
+}
2125
+
2126
+# If want_commit, this will return an array of (rev, commit) where
2127
+# commit _must_ be a valid commit in the archive.
2128
+# Otherwise, it'll return the max revision (whether or not the
2129
+# commit is valid or just a 0x40 placeholder).
2130
+sub rev_map_max {
2131
+ my ($self, $want_commit) = @_;
2132
+ $self->rebuild;
2133
+ my ($r, $c) = $self->rev_map_max_norebuild($want_commit);
2134
+ $want_commit ? ($r, $c) : $r;
2135
+}
2136
+
2137
+sub rev_map_max_norebuild {
2138
+ my ($self, $want_commit) = @_;
2139
+ my $map_path = $self->map_path;
2140
+ stat $map_path or return $want_commit ? (0, undef) : 0;
2141
+ sysopen(my $fh, $map_path, O_RDONLY) or croak "open: $!";
2142
+ binmode $fh or croak "binmode: $!";
2143
+ my $size = (stat($fh))[7];
2144
+ ($size % 24) == 0 or croak "inconsistent size: $size";
2145
+
2146
+ if ($size == 0) {
2147
+ close $fh or croak "close: $!";
2148
+ return $want_commit ? (0, undef) : 0;
2149
+ }
2150
+
2151
+ sysseek($fh, -24, SEEK_END) or croak "seek: $!";
2152
+ sysread($fh, my $buf, 24) == 24 or croak "read: $!";
2153
+ my ($r, $c) = unpack(rev_map_fmt, $buf);
2154
+ if ($want_commit && $c eq ('0' x40)) {
2155
+ if ($size < 48) {
2156
+ return $want_commit ? (0, undef) : 0;
2157
+ }
2158
+ sysseek($fh, -48, SEEK_END) or croak "seek: $!";
2159
+ sysread($fh, $buf, 24) == 24 or croak "read: $!";
2160
+ ($r, $c) = unpack(rev_map_fmt, $buf);
2161
+ if ($c eq ('0'x40)) {
2162
+ croak "Penultimate record is all-zeroes in $map_path";
2163
+ }
2164
+ }
2165
+ close $fh or croak "close: $!";
2166
+ $want_commit ? ($r, $c) : $r;
2167
+}
2168
+
2169
+sub rev_map_get {
2170
+ my ($self, $rev, $uuid) = @_;
2171
+ my $map_path = $self->map_path($uuid);
2172
+ return undef unless -e $map_path;
2173
+
2174
+ sysopen(my $fh, $map_path, O_RDONLY) or croak "open: $!";
2175
+ my $c = _rev_map_get($fh, $rev);
2176
+ close($fh) or croak "close: $!";
2177
+ $c
2178
+}
2179
+
2180
+sub _rev_map_get {
2181
+ my ($fh, $rev) = @_;
2182
+
2183
+ binmode $fh or croak "binmode: $!";
2184
+ my $size = (stat($fh))[7];
2185
+ ($size % 24) == 0 or croak "inconsistent size: $size";
2186
+
2187
+ if ($size == 0) {
2188
+ return undef;
2189
+ }
2190
+
2191
+ my ($l, $u) = (0, $size - 24);
2192
+ my ($r, $c, $buf);
2193
+
2194
+ while ($l <= $u) {
2195
+ my $i = int(($l/24 + $u/24) / 2) * 24;
2196
+ sysseek($fh, $i, SEEK_SET) or croak "seek: $!";
2197
+ sysread($fh, my $buf, 24) == 24 or croak "read: $!";
2198
+ my ($r, $c) = unpack(rev_map_fmt, $buf);
2199
+
2200
+ if ($r < $rev) {
2201
+ $l = $i + 24;
2202
+ } elsif ($r > $rev) {
2203
+ $u = $i - 24;
2204
+ } else { # $r == $rev
2205
+ return $c eq ('0' x 40) ? undef : $c;
2206
+ }
2207
+ }
2208
+ undef;
2209
+}
2210
+
2211
+# Finds the first svn revision that exists on (if $eq_ok is true) or
2212
+# before $rev for the current branch. It will not search any lower
2213
+# than $min_rev. Returns the git commit hash and svn revision number
2214
+# if found, else (undef, undef).
2215
+sub find_rev_before {
2216
+ my ($self, $rev, $eq_ok, $min_rev) = @_;
2217
+ --$rev unless $eq_ok;
2218
+ $min_rev ||= 1;
2219
+ my $max_rev = $self->rev_map_max;
2220
+ $rev = $max_rev if ($rev > $max_rev);
2221
+ while ($rev >= $min_rev) {
2222
+ if (my $c = $self->rev_map_get($rev)) {
2223
+ return ($rev, $c);
2224
+ }
2225
+ --$rev;
2226
+ }
2227
+ return (undef, undef);
2228
+}
2229
+
2230
+# Finds the first svn revision that exists on (if $eq_ok is true) or
2231
+# after $rev for the current branch. It will not search any higher
2232
+# than $max_rev. Returns the git commit hash and svn revision number
2233
+# if found, else (undef, undef).
2234
+sub find_rev_after {
2235
+ my ($self, $rev, $eq_ok, $max_rev) = @_;
2236
+ ++$rev unless $eq_ok;
2237
+ $max_rev ||= $self->rev_map_max;
2238
+ while ($rev <= $max_rev) {
2239
+ if (my $c = $self->rev_map_get($rev)) {
2240
+ return ($rev, $c);
2241
+ }
2242
+ ++$rev;
2243
+ }
2244
+ return (undef, undef);
2245
+}
2246
+
2247
+sub _new {
2248
+ my ($class, $repo_id, $ref_id, $path) = @_;
2249
+ unless (defined $repo_id && length $repo_id) {
2250
+ $repo_id = $default_repo_id;
2251
+ }
2252
+ unless (defined $ref_id && length $ref_id) {
2253
+ # Access the prefix option from the git-svn main program if it's loaded.
2254
+ my $prefix = defined &::opt_prefix ? ::opt_prefix() : "";
2255
+ $_[2] = $ref_id =
2256
+ "refs/remotes/$prefix$default_ref_id";
2257
+ }
2258
+ $_[1] = $repo_id;
2259
+ my $dir = "$ENV{GIT_DIR}/svn/$ref_id";
2260
+
2261
+ # Older repos imported by us used $GIT_DIR/svn/foo instead of
2262
+ # $GIT_DIR/svn/refs/remotes/foo when tracking refs/remotes/foo
2263
+ if ($ref_id =~ m{^refs/remotes/(.*)}) {
2264
+ my $old_dir = "$ENV{GIT_DIR}/svn/$1";
2265
+ if (-d $old_dir && ! -d $dir) {
2266
+ $dir = $old_dir;
2267
+ }
2268
+ }
2269
+
2270
+ $_[3] = $path = '' unless (defined $path);
2271
+ mkpath([$dir]);
2272
+ bless {
2273
+ ref_id => $ref_id, dir => $dir, index => "$dir/index",
2274
+ path => $path, config => "$ENV{GIT_DIR}/svn/config",
2275
+ map_root => "$dir/.rev_map", repo_id => $repo_id }, $class;
2276
+}
2277
+
2278
+# for read-only access of old .rev_db formats
2279
+sub unlink_rev_db_symlink {
2280
+ my ($self) = @_;
2281
+ my $link = $self->rev_db_path;
2282
+ $link =~ s/\.[\w-]+$// or croak "missing UUID at the end of $link";
2283
+ if (-l $link) {
2284
+ unlink $link or croak "unlink: $link failed!";
2285
+ }
2286
+}
2287
+
2288
+sub rev_db_path {
2289
+ my ($self, $uuid) = @_;
2290
+ my $db_path = $self->map_path($uuid);
2291
+ $db_path =~ s{/\.rev_map\.}{/\.rev_db\.}
2292
+ or croak "map_path: $db_path does not contain '/.rev_map.' !";
2293
+ $db_path;
2294
+}
2295
+
2296
+# the new replacement for .rev_db
2297
+sub map_path {
2298
+ my ($self, $uuid) = @_;
2299
+ $uuid ||= $self->ra_uuid;
2300
+ "$self->{map_root}.$uuid";
2301
+}
2302
+
2303
+sub uri_encode {
2304
+ my ($f) = @_;
2305
+ $f =~ s#([^a-zA-Z0-9\*!\:_\./\-])#uc sprintf("%%%02x",ord($1))#eg;
2306
+ $f
2307
+}
2308
+
2309
+sub uri_decode {
2310
+ my ($f) = @_;
2311
+ $f =~ s#%([0-9a-fA-F]{2})#chr(hex($1))#eg;
2312
+ $f
2313
+}
2314
+
2315
+sub remove_username {
2316
+ $_[0] =~ s{^([^:]*://)[^@]+@}{$1};
2317
+}
2318
+
2319
+1;
perl/Makefile
+1
@@ -29,6 +29,7 @@ instdir_SQ = $(subst ','\'',$(prefix)/lib)
29
30
modules += Git
31
modules += Git/I18N
32
+modules += Git/SVN
33
modules += Git/SVN/Memoize/YAML
34
modules += Git/SVN/Fetcher
35
modules += Git/SVN/Editor