$ENV{GIT_DIR} ||= '.git';
$Git::SVN::default_repo_id = 'svn';
$Git::SVN::default_ref_id = $ENV{GIT_SVN_ID} || 'git-svn';
+$Git::SVN::Ra::_log_window_size = 100;
$Git::SVN::Log::TZ = $ENV{TZ};
$ENV{TZ} = 'UTC';
$_template, $_shared,
$_version, $_fetch_all,
$_merge, $_strategy, $_dry_run,
- $_prefix);
+ $_prefix, $_no_checkout, $_verbose);
$Git::SVN::_follow_parent = 1;
my %remote_opts = ( 'username=s' => \$Git::SVN::Prompt::_username,
'config-dir=s' => \$Git::SVN::Ra::config_dir,
'repack:i' => \$Git::SVN::_repack,
'noMetadata' => \$Git::SVN::_no_metadata,
'useSvmProps' => \$Git::SVN::_use_svm_props,
+ 'log-window-size=i' => \$Git::SVN::Ra::_log_window_size,
+ 'no-checkout' => \$_no_checkout,
'quiet|q' => \$_q,
'repack-flags|repack-args|repack-opts=s' =>
\$Git::SVN::_repack_flags,
%remote_opts );
my ($_trunk, $_tags, $_branches);
-my %multi_opts = ( 'trunk|T=s' => \$_trunk,
- 'tags|t=s' => \$_tags,
- 'branches|b=s' => \$_branches );
-my %init_opts = ( 'template=s' => \$_template, 'shared' => \$_shared );
+my %init_opts = ( 'template=s' => \$_template, 'shared:s' => \$_shared,
+ 'trunk|T=s' => \$_trunk, 'tags|t=s' => \$_tags,
+ 'branches|b=s' => \$_branches, 'prefix=s' => \$_prefix,
+ %remote_opts );
my %cmt_opts = ( 'edit|e' => \$_edit,
'rmdir' => \$SVN::Git::Editor::_rmdir,
'find-copies-harder' => \$SVN::Git::Editor::_find_copies_harder,
my %cmd = (
fetch => [ \&cmd_fetch, "Download new revisions from SVN",
{ 'revision|r=s' => \$_revision,
- 'all|a' => \$_fetch_all,
+ 'fetch-all|all' => \$_fetch_all,
%fc_opts } ],
+ clone => [ \&cmd_clone, "Initialize and fetch revisions",
+ { 'revision|r=s' => \$_revision,
+ %fc_opts, %init_opts } ],
init => [ \&cmd_init, "Initialize a repo for tracking" .
" (requires URL argument)",
\%init_opts ],
+ 'multi-init' => [ \&cmd_multi_init,
+ "Deprecated alias for ".
+ "'$0 init -T<trunk> -b<branches> -t<tags>'",
+ \%init_opts ],
dcommit => [ \&cmd_dcommit,
'Commit several diffs to merge with upstream',
{ 'merge|m|M' => \$_merge,
'strategy|s=s' => \$_strategy,
+ 'verbose|v' => \$_verbose,
'dry-run|n' => \$_dry_run,
+ 'fetch-all|all' => \$_fetch_all,
%cmt_opts, %fc_opts } ],
'set-tree' => [ \&cmd_set_tree,
"Set an SVN repository to a git tree-ish",
{ 'stdin|' => \$_stdin, %cmt_opts, %fc_opts, } ],
'show-ignore' => [ \&cmd_show_ignore, "Show svn:ignore listings",
{ 'revision|r=i' => \$_revision } ],
- 'multi-init' => [ \&cmd_multi_init,
- 'Initialize multiple trees (like git-svnimport)',
- { %multi_opts, %init_opts, %remote_opts,
- 'revision|r=i' => \$_revision,
- 'prefix=s' => \$_prefix,
- } ],
'multi-fetch' => [ \&cmd_multi_fetch,
"Deprecated alias for $0 fetch --all",
{ 'revision|r=s' => \$_revision, %fc_opts } ],
# no-op, we automatically run this anyways,
'Migrate configuration/metadata/layout from
previous versions of git-svn',
- \%remote_opts ],
+ { 'minimize' => \$Git::SVN::Migration::_minimize,
+ %remote_opts } ],
'log' => [ \&Git::SVN::Log::cmd_show_log, 'Show commit logs',
{ 'limit=i' => \$Git::SVN::Log::limit,
'revision|r=s' => \$_revision,
'color' => \$Git::SVN::Log::color,
'pager=s' => \$Git::SVN::Log::pager,
} ],
+ 'rebase' => [ \&cmd_rebase, "Fetch and rebase your working directory",
+ { 'merge|m|M' => \$_merge,
+ 'verbose|v' => \$_verbose,
+ 'strategy|s=s' => \$_strategy,
+ 'fetch-all|all' => \$_fetch_all,
+ %fc_opts } ],
'commit-diff' => [ \&cmd_commit_diff,
'Commit a diff between two trees',
{ 'message|m=s' => \$_message,
version() if $_version;
usage(1) unless defined $cmd;
load_authors() if $_authors;
-unless ($cmd =~ /^(?:init|multi-init|commit-diff)$/) {
+unless ($cmd =~ /^(?:clone|init|multi-init|commit-diff)$/) {
Git::SVN::Migration::migration_check();
}
Git::SVN::init_vars();
$cmd{$cmd}->[0]->(@ARGV);
};
fatal $@ if $@;
+post_fetch_checkout();
exit 0;
####################### primary functions ######################
foreach (sort keys %cmd) {
next if $cmd && $cmd ne $_;
+ next if /^multi-/; # don't show deprecated commands
print $fd ' ',pack('A17',$_),$cmd{$_}->[1],"\n";
foreach (keys %{$cmd{$_}->[2]}) {
# prints out arguments as they should be passed:
unless (-d $ENV{GIT_DIR}) {
my @init_db = ('init');
push @init_db, "--template=$_template" if defined $_template;
- push @init_db, "--shared" if defined $_shared;
+ if (defined $_shared) {
+ if ($_shared =~ /[a-z]/) {
+ push @init_db, "--shared=$_shared";
+ } else {
+ push @init_db, "--shared";
+ }
+ }
command_noisy(@init_db);
}
}
+sub init_subdir {
+ my $repo_path = shift or return;
+ mkpath([$repo_path]) unless -d $repo_path;
+ chdir $repo_path or die "Couldn't chdir to $repo_path: $!\n";
+ $ENV{GIT_DIR} = $repo_path . "/.git";
+}
+
+sub cmd_clone {
+ my ($url, $path) = @_;
+ if (!defined $path &&
+ (defined $_trunk || defined $_branches || defined $_tags) &&
+ $url !~ m#^[a-z\+]+://#) {
+ $path = $url;
+ }
+ warn "--path: $path\n" if defined $path;
+ $path = basename($url) if !defined $path || !length $path;
+ warn "++path: $path\n" if defined $path;
+ mkpath([$path]);
+ chdir $path or die "Couldn't chdir to $path\n";
+ cmd_init(@_);
+ Git::SVN::fetch_all($Git::SVN::default_repo_id);
+}
+
sub cmd_init {
- my $url = shift or die "SVN repository location required " .
- "as a command-line argument\n";
- if (my $repo_path = shift) {
- unless (-d $repo_path) {
- mkpath([$repo_path]);
- }
- chdir $repo_path or croak $!;
- $ENV{GIT_DIR} = $repo_path . "/.git";
+ if (defined $_trunk || defined $_branches || defined $_tags) {
+ return cmd_multi_init(@_);
}
+ my $url = shift or die "SVN repository location required ",
+ "as a command-line argument\n";
+ init_subdir(@_);
do_git_init_db();
Git::SVN->init($url);
}
my ($remote) = @_;
if (@_ > 1) {
- die "Usage: $0 fetch [--all|-a] [svn-remote]\n";
+ die "Usage: $0 fetch [--all] [svn-remote]\n";
}
$remote ||= $Git::SVN::default_repo_id;
if ($_fetch_all) {
sub cmd_dcommit {
my $head = shift;
$head ||= 'HEAD';
- my ($url, $rev, $uuid);
- my ($fh, $ctx) = command_output_pipe('rev-list', $head);
my @refs;
- my $c;
- while (<$fh>) {
- $c = $_;
- chomp $c;
- ($url, $rev, $uuid) = cmt_metadata($c);
- last if (defined $url && defined $rev && defined $uuid);
- unshift @refs, $c;
- }
- close $fh; # most likely breaking the pipe
+ my ($url, $rev, $uuid) = working_head_info($head, \@refs);
+ my $c = $refs[-1];
unless (defined $url && defined $rev && defined $uuid) {
die "Unable to determine upstream SVN information from ",
- "$head history:\n $ctx\n";
+ "$head history\n";
}
- my $gs = Git::SVN->find_by_url($url) or
- die "Can't determine fetch information for $url\n";
+ my $gs = Git::SVN->find_by_url($url);
my $last_rev;
foreach my $d (@refs) {
if (!verify_ref("$d~1")) {
}
}
return if $_dry_run;
- $gs->fetch;
+ unless ($gs) {
+ warn "Could not determine fetch information for $url\n",
+ "Will not attempt to fetch and rebase commits.\n",
+ "This probably means you have useSvmProps and should\n",
+ "now resync your SVN::Mirror repository.\n";
+ return;
+ }
+ $_fetch_all ? $gs->fetch_all : $gs->fetch;
# we always want to rebase against the current HEAD, not any
# head that was passed to us
my @diff = command('diff-tree', 'HEAD', $gs->refname, '--');
my @finish;
if (@diff) {
- @finish = qw/rebase/;
- push @finish, qw/--merge/ if $_merge;
- push @finish, "--strategy=$_strategy" if $_strategy;
+ @finish = rebase_cmd();
print STDERR "W: HEAD and ", $gs->refname, " differ, ",
"using @finish:\n", "@diff";
} else {
command_noisy(@finish, $gs->refname);
}
+sub cmd_rebase {
+ command_noisy(qw/update-index --refresh/);
+ my $url = (working_head_info('HEAD'))[0];
+ if (!defined $url) {
+ die "Unable to determine upstream SVN information from ",
+ "working tree history\n";
+ }
+
+ my $gs = Git::SVN->find_by_url($url);
+ if (command(qw/diff-index HEAD --/)) {
+ print STDERR "Cannot rebase with uncommited changes:\n";
+ command_noisy('status');
+ exit 1;
+ }
+ $_fetch_all ? $gs->fetch_all : $gs->fetch;
+ command_noisy(rebase_cmd(), $gs->refname);
+}
+
sub cmd_show_ignore {
my $gs = Git::SVN->new;
my $r = (defined $_revision ? $_revision : $gs->ra->get_latest_revnum);
}
do_git_init_db();
$_prefix = '' unless defined $_prefix;
- $url =~ s#/+$## if defined $url;
+ if (defined $url) {
+ $url =~ s#/+$##;
+ init_subdir(@_);
+ }
if (defined $_trunk) {
my $trunk_ref = $_prefix . 'trunk';
# try both old-style and new-style lookups:
########################### utility functions #########################
+sub rebase_cmd {
+ my @cmd = qw/rebase/;
+ push @cmd, '-v' if $_verbose;
+ push @cmd, qw/--merge/ if $_merge;
+ push @cmd, "--strategy=$_strategy" if $_strategy;
+ @cmd;
+}
+
+sub post_fetch_checkout {
+ return if $_no_checkout;
+ my $gs = $Git::SVN::_head or return;
+ return if verify_ref('refs/heads/master^0');
+
+ my $valid_head = verify_ref('HEAD^0');
+ command_noisy(qw(update-ref refs/heads/master), $gs->refname);
+ return if ($valid_head || !verify_ref('HEAD^0'));
+
+ return if $ENV{GIT_DIR} !~ m#^(?:.*/)?\.git$#;
+ my $index = $ENV{GIT_INDEX_FILE} || "$ENV{GIT_DIR}/index";
+ return if -f $index;
+
+ chomp(my $bare = `git config --bool --get core.bare`);
+ return if $bare eq 'true';
+ return if command_oneline(qw/rev-parse --is-inside-git-dir/) eq 'true';
+ command_noisy(qw/read-tree -m -u -v HEAD HEAD/);
+ print STDERR "Checked out HEAD:\n ",
+ $gs->full_url, " r", $gs->last_rev, "\n";
+}
+
sub complete_svn_url {
my ($url, $path) = @_;
$path =~ s#/+$##;
"and a separate URL is not specified\n");
}
}
- my $r = defined $_revision ? $_revision : $ra->get_latest_revnum;
- my ($dirent, undef, undef) = $ra->get_dir($repo_path, $r);
my $url = $ra->{url};
- my $remote_id;
- my $remote_path;
- foreach my $d (sort keys %$dirent) {
- next if ($dirent->{$d}->kind != $SVN::Node::dir);
- my $path = "$repo_path/$d";
- my $ref = "$pfx$d";
- my $gs = eval { Git::SVN->new($ref) };
- # don't try to init already existing refs
- unless ($gs) {
- print "init $url/$path => $ref\n";
- $gs = Git::SVN->init($url, $path, undef, $ref, 1);
- }
- if ($gs) {
- my $k = "svn-remote.$gs->{repo_id}.url";
- my $orig_url = eval {
- command_oneline(qw/config --get/, $k)
- };
- if ($orig_url && ($orig_url ne $gs->{url})) {
- die "$k already set: $orig_url\n",
- "wanted to set to: $gs->{url}\n";
- }
- unless ($orig_url) {
- command_oneline('config', $k, $gs->{url});
- }
- $remote_id = $gs->{repo_id};
- last;
- }
+ my $gs = Git::SVN->init($url, undef, undef, undef, 1);
+ my $k = "svn-remote.$gs->{repo_id}.url";
+ my $orig_url = eval { command_oneline(qw/config --get/, $k) };
+ if ($orig_url && ($orig_url ne $gs->{url})) {
+ die "$k already set: $orig_url\n",
+ "wanted to set to: $gs->{url}\n";
}
- if (defined $remote_id) {
- $remote_path = "$ra->{svn_path}/$repo_path/*";
- $remote_path =~ s#/+#/#g;
- $remote_path =~ s#^/##g;
- my ($n) = ($switch =~ /^--(\w+)/);
- if (length $pfx && $pfx !~ m#/$#) {
- die "--prefix='$pfx' must have a trailing slash '/'\n";
- }
- command_noisy('config', "svn-remote.$remote_id.$n",
- "$remote_path:refs/remotes/$pfx*");
+ command_oneline('config', $k, $gs->{url}) unless $orig_url;
+ my $remote_path = "$ra->{svn_path}/$repo_path/*";
+ $remote_path =~ s#/+#/#g;
+ $remote_path =~ s#^/##g;
+ my ($n) = ($switch =~ /^--(\w+)/);
+ if (length $pfx && $pfx !~ m#/$#) {
+ die "--prefix='$pfx' must have a trailing slash '/'\n";
}
+ command_noisy('config', "svn-remote.$gs->{repo_id}.$n",
+ "$remote_path:refs/remotes/$pfx*");
}
sub verify_ref {
command(qw/cat-file commit/, shift)))[-1]);
}
+sub working_head_info {
+ my ($head, $refs) = @_;
+ my ($url, $rev, $uuid);
+ my ($fh, $ctx) = command_output_pipe('rev-list', $head);
+ while (<$fh>) {
+ chomp;
+ ($url, $rev, $uuid) = cmt_metadata($_);
+ last if (defined $url && defined $rev && defined $uuid);
+ unshift @$refs, $_ if $refs;
+ }
+ close $fh; # break the pipe
+ ($url, $rev, $uuid);
+}
+
package Git::SVN;
use strict;
use warnings;
use vars qw/$default_repo_id $default_ref_id $_no_metadata $_follow_parent
- $_repack $_repack_flags $_use_svm_props/;
+ $_repack $_repack_flags $_use_svm_props $_head/;
use Carp qw/croak/;
use File::Path qw/mkpath/;
use File::Copy qw/copy/;
sub fetch_all {
my ($repo_id, $remotes) = @_;
- my $remote = $remotes->{$repo_id};
+ if (ref $repo_id) {
+ my $gs = $repo_id;
+ $repo_id = undef;
+ $repo_id = $gs->{repo_id};
+ }
+ $remotes ||= read_all_remotes();
+ my $remote = $remotes->{$repo_id} or
+ die "[svn-remote \"$repo_id\"] unknown\n";
my $fetch = $remote->{fetch};
- my $url = $remote->{url};
+ my $url = $remote->{url} or die "svn-remote.$repo_id.url not defined\n";
my (@gs, @globs);
my $ra = Git::SVN::Ra->new($url);
my $uuid = $ra->get_uuid;
my $head = $ra->get_latest_revnum;
- my $base = $head;
+ my $base = defined $fetch ? $head : 0;
# read the max revs for wildcard expansion (branches/*, tags/*)
foreach my $t (qw/branches tags/) {
"svn-remote.$repo_id.${t}-maxRev") };
if (defined $max_rev && ($max_rev < $base)) {
$base = $max_rev;
+ } elsif (!defined $max_rev) {
+ $base = 0;
}
}
$self->{url} = command_oneline('config', '--get',
"svn-remote.$repo_id.url") or
die "Failed to read \"svn-remote.$repo_id.url\" in config\n";
- if ((-z $self->db_path || ! -e $self->db_path) &&
- ::verify_ref($self->refname.'^0')) {
- $self->rebuild;
- }
+ $self->rebuild;
$self;
}
sub tmp_config {
my (@args) = @_;
- my $config = "$ENV{GIT_DIR}/svn/config";
- unless (-f $config) {
- open my $fh, '>', $config or
- die "Can't open $config: $!\n";
- print $fh "; This file is used internally by git-svn\n" or
- die "Couldn't write to $config: $!\n";
- print $fh "; You should not have to edit it\n" or
- die "Couldn't write to $config: $!\n";
- close $fh or die "Couldn't close $config: $!\n";
+ my $old_def_config = "$ENV{GIT_DIR}/svn/config";
+ my $config = "$ENV{GIT_DIR}/svn/.metadata";
+ if (-e $old_def_config && ! -e $config) {
+ rename $old_def_config, $config or
+ die "Failed rename $old_def_config => $config: $!\n";
}
my $old_config = $ENV{GIT_CONFIG};
$ENV{GIT_CONFIG} = $config;
$@ = undef;
- my @ret = eval { command('config', @args) };
+ my @ret = eval {
+ unless (-f $config) {
+ mkfile($config);
+ open my $fh, '>', $config or
+ die "Can't open $config: $!\n";
+ print $fh "; This file is used internally by ",
+ "git-svn\n" or die
+ "Couldn't write to $config: $!\n";
+ print $fh "; You should not have to edit it\n" or
+ die "Couldn't write to $config: $!\n";
+ close $fh or die "Couldn't close $config: $!\n";
+ }
+ command('config', @args);
+ };
my $err = $@;
if (defined $old_config) {
$ENV{GIT_CONFIG} = $old_config;
my $old_index = $ENV{GIT_INDEX_FILE};
$ENV{GIT_INDEX_FILE} = $self->{index};
$@ = undef;
- my @ret = eval { &$sub };
+ my @ret = eval {
+ my ($dir, $base) = ($self->{index} =~ m#^(.*?)/?([^/]+)$#);
+ mkpath([$dir]) unless -d $dir;
+ &$sub;
+ };
my $err = $@;
if (defined $old_index) {
$ENV{GIT_INDEX_FILE} = $old_index;
my $x = command_oneline('write-tree');
my ($y) = (command(qw/cat-file commit/, $treeish) =~
/^tree ($::sha1)/mo);
- if ($y ne $x) {
- unlink $self->{index} or croak $!;
- command_noisy('read-tree', $treeish);
- }
+ return if $y eq $x;
+
+ warn "Index mismatch: $y != $x\nrereading $treeish\n";
+ unlink $self->{index} or die "unlink $self->{index}: $!\n";
+ command_noisy('read-tree', $treeish);
$x = command_oneline('write-tree');
if ($y ne $x) {
::fatal "trees ($treeish) $y != $x\n",
sub rebuild {
my ($self) = @_;
my $db_path = $self->db_path;
+ return if (-e $db_path && ! -z $db_path);
+ return unless ::verify_ref($self->refname.'^0');
if (-f $self->{db_root}) {
rename $self->{db_root}, $db_path or die
"rename $self->{db_root} => $db_path failed: $!\n";
}
close $fh or croak $!;
if ($update_ref) {
+ $_head = $self;
command_noisy('update-ref', '-m', "r$rev",
$self->refname, $commit);
}
sub rev_db_max {
my ($self) = @_;
+ $self->rebuild;
my $db_path = $self->db_path;
my @stat = stat $db_path or return 0;
($stat[7] % 41) == 0 or die "$db_path inconsistent size: $stat[7]\n";
$_[1] = $repo_id = sanitize_remote_name($repo_id);
my $dir = "$ENV{GIT_DIR}/svn/$ref_id";
$_[3] = $path = '' unless (defined $path);
- mkpath([$dir]);
+ mkpath(["$ENV{GIT_DIR}/svn"]);
bless {
ref_id => $ref_id, dir => $dir, index => "$dir/index",
path => $path, config => "$ENV{GIT_DIR}/svn/config",
}
package Git::SVN::Ra;
-use vars qw/@ISA $config_dir/;
+use vars qw/@ISA $config_dir $_log_window_size/;
use strict;
use warnings;
my ($can_do_switch);
sub gs_fetch_loop_common {
my ($self, $base, $head, $gsv, $globs) = @_;
return if ($base > $head);
- my $inc = 1000;
+ my $inc = $_log_window_size;
my ($min, $max) = ($base, $head < $base + $inc ? $head : $base + $inc);
my %common;
my $common_max = scalar @$gsv;
foreach my $gs (@$gsv) {
- if (my $last_commit = $gs->last_commit) {
- $gs->assert_index_clean($last_commit);
- }
my @tmp = split m#/#, $gs->{path};
my $p = '';
foreach (@tmp) {
}
next unless $gs->match_paths($paths, $r);
$gs->{logged_rev_props} = $logged;
+ if (my $last_commit = $gs->last_commit) {
+ $gs->assert_index_clean($last_commit);
+ }
my $log_entry = $gs->do_fetch($paths, $r);
if ($log_entry) {
$gs->do_git_commit($log_entry);
}
}
foreach (keys %$paths) {
- if (/$g->{path}->{left_regex}/) {
+ if (/$g->{path}->{left_regex}/ &&
+ !/$g->{path}->{regex}/) {
next if $paths->{$_}->{action} !~ /^[AR]$/;
get_dir_check($self, $exists, $g, $r);
}
# 175007 - http(s):// (this repo required authorization, too...)
# More codes may be discovered later...
if ($errno == 175007 || $errno == 175002 || $errno == 160013) {
+ warn "W: Ignoring error from SVN, path probably ",
+ "does not exist: ($errno): ",
+ $err->expanded_message,"\n";
return;
}
die "Error from SVN, ($errno): ", $err->expanded_message,"\n";
last;
}
- my $url;
- my ($fh, $ctx) = command_output_pipe('rev-list', $head);
- while (<$fh>) {
- chomp;
- $url = (::cmt_metadata($_))[0];
- last if defined $url;
- }
- close $fh; # break the pipe
-
+ my $url = (::working_head_info($head))[0];
my $gs = Git::SVN->find_by_url($url) || Git::SVN->_new;
my @cmd = (qw/log --abbrev-commit --pretty=raw --default/,
$gs->refname);
print "\n";
}
- foreach my $x (qw/raw diff/) {
+ foreach my $x (qw/raw stat diff/) {
if ($c->{$x}) {
print "\n";
print $_ foreach @{$c->{$x}}
@args = (git_svn_log_cmd($r_min, $r_max, @args), @args);
my $log = command_output_pipe(@args);
run_pager();
- my (@k, $c, $d);
+ my (@k, $c, $d, $stat);
my $esc_color = qr/(?:\033\[(?:(?:\d+;)*\d*)?m)*/;
while (<$log>) {
if (/^${esc_color}commit ($::sha1_short)/o) {
push @{$c->{diff}}, $_;
} elsif ($d) {
push @{$c->{diff}}, $_;
+ } elsif (/^\ .+\ \|\s*\d+\ $esc_color[\+\-]*
+ $esc_color*[\+\-]*$esc_color$/x) {
+ $stat = 1;
+ push @{$c->{stat}}, $_;
+ } elsif ($stat && /^ \d+ files changed, \d+ insertions/) {
+ push @{$c->{stat}}, $_;
+ $stat = undef;
} elsif (/^${esc_color} (git-svn-id:.+)$/o) {
($c->{url}, $c->{r}, undef) = ::extract_metadata($1);
} elsif (s/^${esc_color} //o) {