#!/usr/bin/perl
use lib
(split(/:/,
$ENV{GITPERLLIB
} ||
"/usr/share/perl/5.8.8" ));
# Copyright (C) 2006, Eric Wong <normalperson@yhbt.net>
# License: GPL v2 or later
use warnings;
use strict;
use vars
qw/
$AUTHOR $VERSION $sha1 $sha1_short $_revision
$_q $_authors %users/;
$AUTHOR = 'Eric Wong <normalperson@yhbt.net>';
$VERSION = '1.5.3.4';
my $git_dir_user_set =
1 if defined $ENV{GIT_DIR
};
$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';
$| = 1; # unbuffer STDOUT
require SVN::
Core;
# use()-ing this causes segfaults for me... *shrug* if ($SVN::Core::VERSION lt '1.1.0') {
fatal "Need SVN::Core 1.1.0 or better (got $SVN::Core::VERSION)\n";
}
push @Git::
SVN::
Ra::
ISA,
'SVN::Ra';
push @SVN::
Git::
Editor::
ISA,
'SVN::Delta::Editor';
push @SVN::
Git::
Fetcher::
ISA,
'SVN::Delta::Editor';
use File::
Basename qw/dirname basename/;
use File::
Path qw/mkpath/;
use Getopt::
Long qw/:config gnu_getopt no_ignore_case auto_abbrev/;
use IPC::Open3;
use Git;
BEGIN {
# import functions from Git into our packages, en masse
foreach (qw/command command_oneline command_noisy command_output_pipe
command_input_pipe command_close_pipe/) {
for my $package ( qw(SVN::
Git::
Editor SVN::
Git::
Fetcher Git::SVN::Migration Git::SVN::Log Git::SVN),
__PACKAGE__) {
*{"${package}::$_"} = \&{"Git::$_"};
}
}
}
my ($SVN);
$sha1_short =
qr/
[a-f\d
]{4,
40}/;
my ($_stdin, $_help, $_edit,
$_message, $_file,
$_template, $_shared,
$_version, $_fetch_all, $_no_rebase,
$_merge, $_strategy, $_dry_run, $_local,
$_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,
'no-auth-cache' => \$Git::SVN::Prompt::_no_auth_cache );
my %fc_opts = ( 'follow-parent|follow!' => \$Git::SVN::_follow_parent,
'authors-file|A=s' => \$_authors,
'repack:i' => \$Git::SVN::_repack,
'noMetadata' => \$Git::SVN::_no_metadata,
'useSvmProps' => \$Git::SVN::_use_svm_props,
'useSvnsyncProps' => \$Git::SVN::_use_svnsync_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, $_stdlayout);
my %icv;
my %init_opts = ( 'template=s' => \$_template, 'shared:s' => \$_shared,
'trunk|T=s' => \$_trunk, 'tags|t=s' => \$_tags,
'branches|b=s' => \$_branches, 'prefix=s' => \$_prefix,
'stdlayout|s' => \$_stdlayout,
'minimize-url|m' => \$Git::SVN::_minimize_url,
'no-metadata' => sub { $icv{noMetadata} = 1 },
'use-svm-props' => sub { $icv{useSvmProps} = 1 },
'use-svnsync-props' => sub { $icv{useSvnsyncProps} = 1 },
'rewrite-root=s' => sub { $icv{rewriteRoot} = $_[1] },
%remote_opts );
my %cmt_opts = ( 'edit|e' => \$_edit,
'rmdir' => \$SVN::Git::Editor::_rmdir,
'find-copies-harder' => \$SVN::Git::Editor::_find_copies_harder,
'l=i' => \$SVN::Git::Editor::_rename_limit,
'copy-similarity|C=i'=> \$SVN::Git::Editor::_cp_similarity
);
my %cmd = (
fetch => [ \&cmd_fetch, "Download new revisions from SVN",
{ 'revision|r=s' => \$_revision,
'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,
'no-rebase' => \$_no_rebase,
%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-fetch' => [ \&cmd_multi_fetch,
"Deprecated alias for $0 fetch --all",
{ 'revision|r=s' => \$_revision, %fc_opts } ],
'migrate' => [ sub { },
# no-op, we automatically run this anyways,
'Migrate configuration/metadata/layout from
previous versions of git-svn',
{ '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,
'verbose|v' => \$Git::SVN::Log::verbose,
'incremental' => \$Git::SVN::Log::incremental,
'oneline' => \$Git::SVN::Log::oneline,
'show-commit' => \$Git::SVN::Log::show_commit,
'non-recursive' => \$Git::SVN::Log::non_recursive,
'authors-file|A=s' => \$_authors,
'color' => \$Git::SVN::Log::color,
'pager=s' => \$Git::SVN::Log::pager,
} ],
'find-rev' => [ \&cmd_find_rev, "Translate between SVN revision numbers and tree-ish",
{ } ],
'rebase' => [ \&cmd_rebase, "Fetch and rebase your working directory",
{ 'merge|m|M' => \$_merge,
'verbose|v' => \$_verbose,
'strategy|s=s' => \$_strategy,
'local|l' => \$_local,
'fetch-all|all' => \$_fetch_all,
%fc_opts } ],
'commit-diff' => [ \&cmd_commit_diff,
'Commit a diff between two trees',
{ 'message|m=s' => \$_message,
'file|F=s' => \$_file,
'revision|r=s' => \$_revision,
%cmt_opts } ],
);
my $cmd;
for (my $i = 0; $i < @ARGV; $i++) {
$cmd = $ARGV[$i];
last;
}
};
my %opts = %
{$cmd{$cmd}->
[2]} if (defined $cmd);
read_repo_config(\%opts);
Getopt::Long::Configure('pass_through') if ($cmd && $cmd eq 'log');
my $rv = GetOptions(%opts, 'help|H|h' => \$_help, 'version|V' => \$_version,
'minimize-connections' => \$Git::SVN::Migration::_minimize,
'id|i=s' => \$Git::SVN::default_ref_id,
'svn-remote|remote|R=s' => sub {
$Git::SVN::no_reuse_existing = 1;
$Git::SVN::default_repo_id = $_[1] });
exit 1 if (!
$rv &&
$cmd &&
$cmd ne 'log');
usage(0) if $_help;
version() if $_version;
load_authors() if $_authors;
# make sure we're always running
unless ($cmd =~ /(?:clone|init|multi-init)$/) {
unless (-d $ENV{GIT_DIR}) {
if ($git_dir_user_set) {
die "GIT_DIR=$ENV{GIT_DIR} explicitly set, ",
"but it is not a directory\n";
}
my $git_dir =
delete $ENV{GIT_DIR
};
chomp(my $cdup = command_oneline
(qw/rev-parse --show-cdup/
));
die "Already at toplevel, but $git_dir ",
"not found '$cdup'\n";
}
chdir $cdup or die "Unable to chdir up to '$cdup'\n";
unless (-d $git_dir) {
die "$git_dir still not found after going to ",
"'$cdup'\n";
}
$ENV{GIT_DIR} = $git_dir;
}
}
unless ($cmd =~ /^(?:clone|init|multi-init|commit-diff)$/) {
Git::SVN::Migration::migration_check();
}
Git::SVN::init_vars();
Git::SVN::verify_remotes_sanity();
$cmd{$cmd}->[0]->(@ARGV);
};
fatal $@ if $@;
post_fetch_checkout();
####################### primary functions ######################
sub usage {
my $fd = $exit ? \*STDERR : \*STDOUT;
git-svn - bidirectional operations between a single Subversion tree and git
Usage: $0 <command> [options] [arguments]\n
print $fd "Available commands:\n" unless $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]}) { # mixed-case options are for .git/config only
next if /[A-Z]/ && /^[a-z]+$/i;
# prints out arguments as they should be passed:
my $x =
s#[:=]s$## ? '<arg>' : s#[:=]i$## ? '<num>' : ''; "--$_" : "-$_" }
}
}
\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 version {
print "git-svn version $VERSION (svn $SVN::Core::VERSION)\n";
exit 0;
}
sub do_git_init_db {
unless (-d $ENV{GIT_DIR}) {
my @init_db = ('init');
push @init_db, "--template=$_template" if defined $_template;
if (defined $_shared) {
if ($_shared =~ /[a-z]/) {
push @init_db, "--shared=$_shared";
} else {
push @init_db, "--shared";
}
}
command_noisy(@init_db);
}
my $set;
my $pfx = "svn-remote.$Git::SVN::default_repo_id";
foreach my $i (keys %icv) {
die "'$set' and '$i' cannot both be set\n" if $set;
next unless defined $icv{$i};
command_noisy('config', "$pfx.$i", $icv{$i});
$set = $i;
}
}
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} = '.git';
}
sub cmd_clone {
my ($url, $path) = @_;
if (!defined $path &&
(defined $_trunk || defined $_branches || defined $_tags ||
defined $_stdlayout) &&
$url !~ m#^[a-z\+]+://#) {
$path = $url;
}
$path = basename($url) if !defined $path || !length $path;
cmd_init($url, $path);
Git::SVN::fetch_all($Git::SVN::default_repo_id);
}
sub cmd_init {
if (defined $_stdlayout) {
$_trunk = 'trunk' if (!defined $_trunk);
$_tags = 'tags' if (!defined $_tags);
$_branches = 'branches' if (!defined $_branches);
}
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);
}
sub cmd_fetch {
if (grep /^\d+=./, @_) {
die "'<rev>=<commit>' fetch arguments are ",
"no longer supported.\n
"; }
my ($remote) = @_;
if (@_ > 1) {
die "Usage: $0 fetch [--all] [svn-remote]\n";
}
$remote ||= $Git::SVN::default_repo_id;
if ($_fetch_all) {
cmd_multi_fetch();
} else {
Git::SVN::fetch_all($remote, Git::SVN::read_all_remotes());
}
}
sub cmd_set_tree {
my (@commits) = @_;
if ($_stdin || !@commits) {
print "Reading from stdin...\n";
@commits = ();
while (<STDIN> ) {
if (/\b($sha1_short)\b/o) {
unshift @commits, $1;
}
}
}
my @revs;
foreach my $c (@commits) {
my @tmp = command('rev-parse',$c);
if (scalar @tmp == 1) {
push @revs, $tmp[0];
} elsif (scalar @tmp > 1) {
push @revs, reverse(command('rev-list',@tmp));
} else {
fatal "Failed to rev-parse $c\n";
}
}
my $gs = Git::SVN->new;
my ($r_last, $cmt_last) = $gs->last_rev_commit;
$gs->fetch;
if (defined $gs->{last_rev} && $r_last != $gs->{last_rev}) {
fatal "There are new revisions that were fetched ",
"and need to be merged (or acknowledged) ",
"before committing.\nlast rev: $r_last\n",
" current: $gs->{last_rev}\n";
}
$gs->set_tree($_) foreach @revs;
print "Done committing ",scalar @revs," revisions to SVN\n";
}
sub cmd_dcommit {
my $head = shift;
$head ||= 'HEAD';
my @refs;
my ($url, $rev, $uuid, $gs) = working_head_info($head, \@refs);
print "Committing to $url ...\n";
unless ($gs) {
die "Unable to determine upstream SVN information from ",
"$head history\n";
}
my $last_rev;
my ($linear_refs, $parents) = linearize_history($gs, \@refs);
if ($_no_rebase && scalar(@$linear_refs) > 1) {
warn "Attempting to commit more than one change while ",
"--no-rebase is enabled.\n",
"If these changes depend on
each other, re-running
", "without --no-rebase will be required."
}
foreach my $d (@$linear_refs) {
unless (defined $last_rev) {
(undef, $last_rev, undef) = cmt_metadata("$d~1" );
unless (defined $last_rev) {
fatal "Unable to extract revision information ",
"from commit $d~1\n";
}
}
if ($_dry_run) {
print "diff-tree $d~1 $d\n";
} else {
my $cmt_rev;
my %ed_opts = ( r => $last_rev,
log => get_commit_entry($d)->{log},
ra => Git::SVN::Ra->new($gs->full_url),
tree_a => "$d~1",
tree_b => $d,
editor_cb => sub {
print "Committed r$_[0]\n";
$cmt_rev = $_[0];
},
svn_path => '');
if (!SVN::Git::Editor->new(\%ed_opts)->apply_diff) {
print "No changes\n$d~1 == $d\n";
} elsif ($parents->{$d} && @{$parents->{$d}}) {
$gs->{inject_parents_dcommit}->{$cmt_rev} =
$parents->{$d};
}
$_fetch_all ? $gs->fetch_all : $gs->fetch;
next if $_no_rebase;
# 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 = rebase_cmd();
print STDERR "W: HEAD and ", $gs->refname,
" differ, using @finish:\n",
"@diff";
} else {
print "No changes between current HEAD and ",
$gs->refname,
"\nResetting to the latest ",
$gs->refname, "\n";
@finish = qw/reset --mixed/;
}
command_noisy(@finish, $gs->refname);
$last_rev = $cmt_rev;
}
}
}
sub cmd_find_rev {
my $revision_or_hash = shift;
my $result;
if ($revision_or_hash =~ /^r\d+$/) {
my $head = shift;
$head ||= 'HEAD';
my @refs;
my (undef, undef, undef, $gs) = working_head_info($head, \@refs);
unless ($gs) {
die "Unable to determine upstream SVN information from ",
"$head history\n";
}
my $desired_revision = substr($revision_or_hash, 1);
$result = $gs->rev_db_get($desired_revision);
} else {
my (undef, $rev, undef) = cmt_metadata($revision_or_hash);
$result = $rev;
}
print "$result\n" if $result;
}
sub cmd_rebase {
command_noisy(qw/update-index --refresh/);
my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
unless ($gs) {
die "Unable to determine upstream SVN information from ",
"working tree history\n";
}
if (command(qw/diff-index HEAD --/)) {
print STDERR "Cannot rebase with uncommited changes:\n";
command_noisy('status');
exit 1;
}
unless ($_local) {
$_fetch_all ? $gs->fetch_all : $gs->fetch;
}
command_noisy(rebase_cmd(), $gs->refname);
}
sub cmd_show_ignore {
my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
$gs ||= Git::SVN->new;
my $r = (defined $_revision ? $_revision : $gs->ra->get_latest_revnum);
$gs->traverse_ignore(\*STDOUT, $gs->{path}, $r);
}
sub cmd_multi_init {
my $url = shift;
unless (defined $_trunk || defined $_branches || defined $_tags) {
usage(1);
}
# there are currently some bugs that prevent multi-init/multi-fetch
# setups from working well without this.
$Git::SVN::_minimize_url = 1;
$_prefix = '' unless defined $_prefix;
if (defined $url) {
$url =~ s#/+$##;
init_subdir(@_);
}
do_git_init_db();
if (defined $_trunk) {
my $trunk_ref = $_prefix . 'trunk';
# try both old-style and new-style lookups:
my $gs_trunk = eval { Git::SVN->new($trunk_ref) };
unless ($gs_trunk) {
my ($trunk_url, $trunk_path) =
complete_svn_url($url, $_trunk);
$gs_trunk = Git::SVN->init($trunk_url, $trunk_path,
undef, $trunk_ref);
}
}
return unless defined $_branches || defined $_tags;
my $ra = $url ? Git::SVN::Ra->new($url) : undef;
complete_url_ls_init($ra, $_branches, '--branches/-b', $_prefix);
complete_url_ls_init($ra, $_tags, '--tags/-t', $_prefix . 'tags/');
}
sub cmd_multi_fetch {
my $remotes = Git::SVN::read_all_remotes();
foreach my $repo_id (sort keys %$remotes) {
if ($remotes->{$repo_id}->{url}) {
Git::SVN::fetch_all($repo_id, $remotes);
}
}
}
# this command is special because it requires no metadata
sub cmd_commit_diff {
my ($ta, $tb, $url) = @_;
my $usage = "Usage: $0 commit-diff -r<revision> ".
"<tree-ish> <tree-ish> [<URL>]\n";
fatal($usage) if (!defined $ta || !defined $tb);
my $svn_path;
if (!defined $url) {
my $gs = eval { Git::SVN->new };
if (!$gs) {
fatal("Needed URL or usable git-svn --id in ",
"the command-line\n", $usage);
}
$url = $gs->{url};
$svn_path = $gs->{path};
}
unless (defined $_revision) {
fatal("-r|--revision is a required argument\n", $usage);
}
if (defined $_message && defined $_file) {
fatal("Both --message/-
m and --file/-F specified
", "for the commit message.\n",
"I have
no idea what you mean\n
" ); }
if (defined $_file) {
$_message = file_to_s($_file);
} else {
$_message ||= get_commit_entry($tb)->{log};
}
my $ra ||= Git::SVN::Ra->new($url);
$svn_path ||= $ra->{svn_path};
my $r = $_revision;
if ($r eq 'HEAD') {
$r = $ra->get_latest_revnum;
} elsif ($r !~ /^\d+$/) {
die "revision argument: $r not understood by git-svn\n";
}
my %ed_opts = ( r => $r,
log => $_message,
ra => $ra,
tree_a => $ta,
tree_b => $tb,
editor_cb => sub { print "Committed r$_[0]\n" },
svn_path => $svn_path );
if (!SVN::Git::Editor->new(\%ed_opts)->apply_diff) {
print "No changes\n$ta == $tb\n";
}
}
########################### 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;
return if command_oneline(qw/rev-parse --is-inside-work-tree/) eq 'false';
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#/+$##;
if ($path !~ m#^[a-z\+]+://#) {
if (!defined $url || $url !~ m#^[a-z\+]+://#) {
fatal("E: '$path' is not a complete URL ",
"and a separate URL is not specified\n" );
}
return ($url, $path);
}
return ($path, '');
}
sub complete_url_ls_init {
my ($ra, $repo_path, $switch, $pfx) = @_;
unless ($repo_path) {
print STDERR "W: $switch not specified\n";
return;
}
$repo_path =~ s#/+$##;
if ($repo_path =~ m#^[a-z\+]+://#) {
$ra = Git::SVN::Ra->new($repo_path);
$repo_path = '';
} else {
$repo_path =~ s#^/+##;
unless ($ra) {
fatal("E: '$repo_path' is not a complete URL ",
"and a separate URL is not specified\n" );
}
}
my $url = $ra->{url};
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";
}
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 {
my ($ref) = @_;
eval { command_oneline([ 'rev-parse', '--verify', $ref ],
{ STDERR => 0 }); };
}
sub get_tree_from_treeish {
my ($treeish) = @_;
# $treeish can be a symbolic ref, too:
my $type = command_oneline(qw/cat-file -t/, $treeish);
my $expected;
while ($type eq 'tag') {
($treeish, $type) = command(qw/cat-file tag/, $treeish);
}
if ($type eq 'commit') {
$expected = (grep /^tree /, command(qw/cat-file commit/,
$treeish))[0];
($expected) = ($expected =~ /^tree ($sha1)$/o);
die "Unable to get tree from $treeish\n" unless $expected;
} elsif ($type eq 'tree') {
$expected = $treeish;
} else {
die "$treeish is a $type, expected tree, tag or commit\n";
}
return $expected;
}
sub get_commit_entry {
my ($treeish) = shift;
my %log_entry = ( log => '', tree => get_tree_from_treeish($treeish) );
my $commit_editmsg = "$ENV{GIT_DIR}/COMMIT_EDITMSG";
my $commit_msg = "$ENV{GIT_DIR}/COMMIT_MSG";
open my $log_fh, '>', $commit_editmsg or croak $!;
my $type = command_oneline(qw/cat-file -t/, $treeish);
if ($type eq 'commit' || $type eq 'tag') {
my ($msg_fh, $ctx) = command_output_pipe('cat-file',
$type, $treeish);
my $in_msg = 0;
while (<$msg_fh> ) {
if (!$in_msg) {
$in_msg = 1 if (/^\s*$/);
} elsif (/^git-svn-id: /) {
# skip this for now, we regenerate the
# correct one on re-fetch anyways
# TODO: set *:merge properties or like...
} else {
print $log_fh $_ or croak $!;
}
}
command_close_pipe($msg_fh, $ctx);
}
close $log_fh or croak $!;
if ($_edit || ($type eq 'tree')) {
my $editor = $ENV{VISUAL} || $ENV{EDITOR} || 'vi';
# TODO: strip out spaces, comments, like git-commit.sh
system($editor, $commit_editmsg);
}
rename $commit_editmsg, $commit_msg or croak $!;
open $log_fh, '<', $commit_msg or croak $!;
{ local $/; chomp($log_entry{log} = <$log_fh> ); }
close $log_fh or croak $!;
unlink $commit_msg;
\%log_entry;
}
sub s_to_file {
my ($str, $file, $mode) = @_;
open my $fd,'>',$file or croak $!;
print $fd $str,"\n" or croak $!;
close $fd or croak $!;
chmod ($mode &~ umask, $file) if (defined $mode);
}
sub file_to_s {
my $file = shift;
open my $fd,'<',$file or croak "$!: file: $file\n";
local $/;
my $ret = <$fd>;
close $fd or croak $!;
$ret =~ s/\s*$//s;
return $ret;
}
# '<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 /^(.+?|\(no author\))\s*=\s*(.+?)\s*<(.+)>\s*$/;
my ($user, $name, $email) = ($1, $2, $3);
if ($log) {
$Git::SVN::Log::rusers{"$name <$email>"} = $user;
} else {
$users{$user} = [$name, $email];
}
}
close $authors or croak $!;
}
# convert GetOpt::Long specs for use by git-config
sub read_repo_config {
return unless -d $ENV{GIT_DIR};
my $opts = shift;
my @config_only;
foreach my $o (keys %$opts) {
# if we have mixedCase and a long option-only, then
# it's a config-only variable that we don
't need for # the command-line.
push @config_only, $o if ($o =~ /[A-Z]/ && $o =~ /^[a-z]+$/i);
my $v = $opts->{$o};
my ($key) = ($o =~ /^([a-zA-Z\-]+)/);
$key =~ s/-//g;
my $arg = 'git-config';
$arg .= ' --
int' if ($o =~ /[:=]i$/); $arg .= ' --bool' if ($o !~ /[:=][sfi]$/);
if (ref $v eq 'ARRAY') {
chomp(my @tmp = `$arg --get-all svn.$key`);
@$v = @tmp if @tmp;
} else {
chomp(my $tmp = `$arg --get svn.$key`);
if ($tmp && !($arg =~ / --bool/ && $tmp eq 'false')) {
$$v = $tmp;
}
}
}
delete @$opts{@config_only} if @config_only;
}
sub extract_metadata {
my $id = shift or return (undef, undef, undef);
my ($url, $rev, $uuid) = ($id =~ /^\s*git-svn-id:\s+(.*)\@(\d+)
\s([a-f\d\-]+)$/x);
if (!defined $rev || !$uuid || !$url) {
# some of the original repositories I made had
# identifiers like this:
($rev, $uuid) = ($id =~/^\s*git-svn-id:\s(\d+)\@([a-f\d\-]+)/);
}
return ($url, $rev, $uuid);
}
sub cmt_metadata {
return extract_metadata((grep(/^git-svn-id: /,
command(qw/cat-file commit/, shift)))[-1]);
}
sub working_head_info {
my ($head, $refs) = @_;
my ($fh, $ctx) = command_output_pipe('log', '--no-color
', $head); my $hash;
my %max;
while (<$fh> ) {
if ( m{^commit ($::sha1)$} ) {
unshift @$refs, $hash if $hash and $refs;
$hash = $1;
next;
}
next unless s{^\s*(git-svn-id:)}{$1};
my ($url, $rev, $uuid) = extract_metadata($_);
if (defined $url && defined $rev) {
next if $max{$url} and $max{$url} < $rev;
if (my $gs = Git::SVN->find_by_url($url)) {
my $c = $gs->rev_db_get($rev);
if ($c && $c eq $hash) {
close $fh; # break the pipe
return ($url, $rev, $uuid, $gs);
} else {
$max{$url} ||= $gs->rev_db_max;
}
}
}
}
command_close_pipe($fh, $ctx);
(undef, undef, undef, undef);
}
sub read_commit_parents {
my ($parents, $c) = @_;
chomp(my $p = command_oneline(qw/rev-list --parents -1/, $c));
$p =~ s/^($c)\s*// or die "rev-list --parents -1 $c failed!\n";
@{$parents->{$c}} = split(/ /, $p);
}