use warnings;
use strict;
use vars qw/ $AUTHOR $VERSION
- $SVN_URL $SVN_INFO $SVN_WC
+ $SVN_URL $SVN_INFO $SVN_WC $SVN_UUID
$GIT_SVN_INDEX $GIT_SVN
$GIT_DIR $REV_DIR/;
$AUTHOR = 'Eric Wong <normalperson@yhbt.net>';
$VERSION = '0.10.0';
$GIT_DIR = $ENV{GIT_DIR} || "$ENV{PWD}/.git";
-$GIT_SVN = $ENV{GIT_SVN_ID} || 'git-svn';
-$GIT_SVN_INDEX = "$GIT_DIR/$GIT_SVN/index";
-$ENV{GIT_DIR} ||= $GIT_DIR;
-$SVN_URL = undef;
-$REV_DIR = "$GIT_DIR/$GIT_SVN/revs";
-$SVN_WC = "$GIT_DIR/$GIT_SVN/tree";
-
# make sure the svn binary gives consistent output between locales and TZs:
$ENV{TZ} = 'UTC';
$ENV{LC_ALL} = 'C';
# If SVN:: library support is added, please make the dependencies
# optional and preserve the capability to use the command-line client.
# use eval { require SVN::... } to make it lazy load
+# We don't use any modules not in the standard Perl distribution:
use Carp qw/croak/;
use IO::File qw//;
use File::Basename qw/dirname basename/;
my $sha1 = qr/[a-f\d]{40}/;
my $sha1_short = qr/[a-f\d]{4,40}/;
my ($_revision,$_stdin,$_no_ignore_ext,$_no_stop_copy,$_help,$_rmdir,$_edit,
- $_find_copies_harder, $_l, $_version, $_upgrade);
-my (@_branch_from, %tree_map);
-
-GetOptions( 'revision|r=s' => \$_revision,
- 'no-ignore-externals' => \$_no_ignore_ext,
- 'stdin|' => \$_stdin,
- 'edit|e' => \$_edit,
- 'rmdir' => \$_rmdir,
- 'upgrade' => \$_upgrade,
- 'help|H|h' => \$_help,
+ $_find_copies_harder, $_l, $_version, $_upgrade, $_authors);
+my (@_branch_from, %tree_map, %users);
+
+my %fc_opts = ( 'no-ignore-externals' => \$_no_ignore_ext,
'branch|b=s' => \@_branch_from,
- 'find-copies-harder' => \$_find_copies_harder,
- 'l=i' => \$_l,
- 'version|V' => \$_version,
- 'no-stop-on-copy' => \$_no_stop_copy );
+ 'authors-file|A=s' => \$_authors );
my %cmd = (
- fetch => [ \&fetch, "Download new revisions from SVN" ],
- init => [ \&init, "Initialize and fetch (import)"],
- commit => [ \&commit, "Commit git revisions to SVN" ],
- 'show-ignore' => [ \&show_ignore, "Show svn:ignore listings" ],
- rebuild => [ \&rebuild, "Rebuild git-svn metadata (after git clone)" ],
- help => [ \&usage, "Show help" ],
+ fetch => [ \&fetch, "Download new revisions from SVN",
+ { 'revision|r=s' => \$_revision, %fc_opts } ],
+ init => [ \&init, "Initialize and fetch (import)", { } ],
+ commit => [ \&commit, "Commit git revisions to SVN",
+ { 'stdin|' => \$_stdin,
+ 'edit|e' => \$_edit,
+ 'rmdir' => \$_rmdir,
+ 'find-copies-harder' => \$_find_copies_harder,
+ 'l=i' => \$_l,
+ %fc_opts,
+ } ],
+ 'show-ignore' => [ \&show_ignore, "Show svn:ignore listings", { } ],
+ rebuild => [ \&rebuild, "Rebuild git-svn metadata (after git clone)",
+ { 'no-ignore-externals' => \$_no_ignore_ext,
+ 'upgrade' => \$_upgrade } ],
);
my $cmd;
for (my $i = 0; $i < @ARGV; $i++) {
}
};
-# we may be called as git-svn-(command), or git-svn(command).
-foreach (keys %cmd) {
- if (/git\-svn\-?($_)(?:\.\w+)?$/) {
- $cmd = $1;
- last;
- }
-}
+my %opts = %{$cmd{$cmd}->[2]} if (defined $cmd);
+
+GetOptions(%opts, 'help|H|h' => \$_help,
+ 'version|V' => \$_version,
+ 'id|i=s' => \$GIT_SVN) or exit 1;
+
+$GIT_SVN ||= $ENV{GIT_SVN_ID} || 'git-svn';
+$GIT_SVN_INDEX = "$GIT_DIR/$GIT_SVN/index";
+$ENV{GIT_DIR} ||= $GIT_DIR;
+$SVN_URL = undef;
+$REV_DIR = "$GIT_DIR/$GIT_SVN/revs";
+$SVN_WC = "$GIT_DIR/$GIT_SVN/tree";
+
usage(0) if $_help;
version() if $_version;
-usage(1) unless (defined $cmd);
+usage(1) unless defined $cmd;
+load_authors() if $_authors;
svn_check_ignore_externals();
$cmd{$cmd}->[0]->(@ARGV);
exit 0;
print $fd <<"";
git-svn - bidirectional operations between a single Subversion tree and git
Usage: $0 <command> [options] [arguments]\n
-Available commands:
+
+ print $fd "Available commands:\n" unless $cmd;
foreach (sort keys %cmd) {
+ next if $cmd && $cmd ne $_;
print $fd ' ',pack('A13',$_),$cmd{$_}->[1],"\n";
+ foreach (keys %{$cmd{$_}->[2]}) {
+ # prints out arguments as they should be passed:
+ my $x = s#=s$## ? '<arg>' : s#=i$## ? '<num>' : '';
+ print $fd ' ' x 17, join(', ', map { length $_ > 1 ?
+ "--$_" : "-$_" }
+ split /\|/,$_)," $x\n";
+ }
}
print $fd <<"";
-\nGIT_SVN_ID may be set in the environment to an arbitrary identifier if
-you're tracking multiple SVN branches/repositories in one git repository
-and want to keep them separate. See git-svn(1) for more information.
+\nGIT_SVN_ID may be set in the environment or via the --id/-i switch to an
+arbitrary identifier if you're tracking multiple SVN branches/repositories in
+one git repository and want to keep them separate. See git-svn(1) for more
+information.
exit $exit;
}
sub rebuild {
$SVN_URL = shift or undef;
- my $repo_uuid;
my $newest_rev = 0;
if ($_upgrade) {
sys('git-update-ref',"refs/remotes/$GIT_SVN","$GIT_SVN-HEAD");
# if we merged or otherwise started elsewhere, this is
# how we break out of it
- next if (defined $repo_uuid && ($uuid ne $repo_uuid));
+ next if (defined $SVN_UUID && ($uuid ne $SVN_UUID));
next if (defined $SVN_URL && ($url ne $SVN_URL));
print "r$rev = $c\n";
croak "SVN repository location required: $url\n";
}
$SVN_URL ||= $url;
- $repo_uuid ||= setup_git_svn();
+ $SVN_UUID ||= setup_git_svn();
$latest = $rev;
}
assert_revision_eq_or_unknown($rev, $c);
sys(@svn_co, $SVN_URL, $SVN_WC);
chdir $SVN_WC or croak $!;
$last_commit = git_commit($base, @parents);
- unless (-f "$GIT_DIR/refs/heads/master") {
- sys(qw(git-update-ref refs/heads/master),$last_commit);
- }
assert_svn_wc_clean($base->{revision}, $last_commit);
} else {
chdir $SVN_WC or croak $!;
$last_commit = git_commit($log_msg, $last_commit, @parents);
}
assert_svn_wc_clean($last_rev, $last_commit);
+ unless (-e "$GIT_DIR/refs/heads/master") {
+ sys(qw(git-update-ref refs/heads/master),$last_commit);
+ }
return pop @$svn_log;
}
print "Reading from stdin...\n";
@commits = ();
while (<STDIN>) {
- if (/\b($sha1_short)\b/) {
+ if (/\b($sha1_short)\b/o) {
unshift @commits, $1;
}
}
mkpath(["$GIT_DIR/$GIT_SVN/info"]);
mkpath([$REV_DIR]);
s_to_file($SVN_URL,"$GIT_DIR/$GIT_SVN/info/url");
- my $uuid = svn_info($SVN_URL)->{'Repository UUID'} or
+ $SVN_UUID = svn_info($SVN_URL)->{'Repository UUID'} or
croak "Repository UUID unreadable\n";
- s_to_file($uuid,"$GIT_DIR/$GIT_SVN/info/uuid");
+ s_to_file($SVN_UUID,"$GIT_DIR/$GIT_SVN/info/uuid");
open my $fd, '>>', "$GIT_DIR/$GIT_SVN/info/exclude" or croak $!;
print $fd '.svn',"\n";
close $fd or croak $!;
- return $uuid;
+ return $SVN_UUID;
}
sub assert_svn_wc_clean {
my ($svn_rev, $treeish) = @_;
croak "$svn_rev is not an integer!\n" unless ($svn_rev =~ /^\d+$/);
croak "$treeish is not a sha1!\n" unless ($treeish =~ /^$sha1$/o);
- my $svn_info = svn_info('.');
- if ($svn_rev != $svn_info->{'Last Changed Rev'}) {
- croak "Expected r$svn_rev, got r",
- $svn_info->{'Last Changed Rev'},"\n";
+ my $lcr = svn_info('.')->{'Last Changed Rev'};
+ if ($svn_rev != $lcr) {
+ print STDERR "Checking for copy-tree ... ";
+ # use
+ my @diff = grep(/^Index: /,(safe_qx(qw(svn diff),
+ "-r$lcr:$svn_rev")));
+ if (@diff) {
+ croak "Nope! Expected r$svn_rev, got r$lcr\n";
+ } else {
+ print STDERR "OK!\n";
+ }
}
my @status = grep(!/^Performing status on external/,(`svn status`));
@status = grep(!/^\s*$/,@status);
author => $author,
lines => $lines,
msg => '' );
+ if (defined $_authors && ! defined $users{$author}) {
+ die "Author: $author not defined in ",
+ "$_authors file\n";
+ }
push @svn_log, \%log_msg;
$state = 'msg_start';
next;
my ($log_msg, @parents) = @_;
assert_revision_unknown($log_msg->{revision});
my $out_fh = IO::File->new_tmpfile or croak $!;
- my $info = svn_info('.');
- my $uuid = $info->{'Repository UUID'};
- defined $uuid or croak "Unable to get Repository UUID\n";
+ $SVN_UUID ||= svn_info('.')->{'Repository UUID'};
map_tree_joins() if (@_branch_from && !%tree_map);
my $msg_fh = IO::File->new_tmpfile or croak $!;
print $msg_fh $log_msg->{msg}, "\ngit-svn-id: ",
"$SVN_URL\@$log_msg->{revision}",
- " $uuid\n" or croak $!;
+ " $SVN_UUID\n" or croak $!;
$msg_fh->flush == 0 or croak $!;
seek $msg_fh, 0, 0 or croak $!;
- $ENV{GIT_AUTHOR_NAME} = $ENV{GIT_COMMITTER_NAME} =
- $log_msg->{author};
- $ENV{GIT_AUTHOR_EMAIL} = $ENV{GIT_COMMITTER_EMAIL} =
- $log_msg->{author}."\@$uuid";
- $ENV{GIT_AUTHOR_DATE} = $ENV{GIT_COMMITTER_DATE} =
- $log_msg->{date};
+ set_commit_env($log_msg);
+
my @exec = ('git-commit-tree',$tree);
push @exec, '-p', $_ foreach @exec_parents;
open STDIN, '<&', $msg_fh or croak $!;
return $commit;
}
+sub set_commit_env {
+ my ($log_msg) = @_;
+ my $author = $log_msg->{author};
+ my ($name,$email) = defined $users{$author} ? @{$users{$author}}
+ : ($author,"$author\@$SVN_UUID");
+ $ENV{GIT_AUTHOR_NAME} = $ENV{GIT_COMMITTER_NAME} = $name;
+ $ENV{GIT_AUTHOR_EMAIL} = $ENV{GIT_COMMITTER_EMAIL} = $email;
+ $ENV{GIT_AUTHOR_DATE} = $ENV{GIT_COMMITTER_DATE} = $log_msg->{date};
+}
+
sub apply_mod_line_blob {
my $m = shift;
if ($m->{mode_b} =~ /^120/) {
}
}
+# '<svn username> = real-name <email address>' mapping based on git-svnimport:
+sub load_authors {
+ open my $authors, '<', $_authors or die "Can't open $_authors $!\n";
+ while (<$authors>) {
+ chomp;
+ next unless /^(\S+?)\s*=\s*(.+?)\s*<(.+)>\s*$/;
+ my ($user, $name, $email) = ($1, $2, $3);
+ $users{$user} = [$name, $email];
+ }
+ close $authors or croak $!;
+}
+
__END__
Data structures: