use warnings;
use strict;
use vars qw/ $AUTHOR $VERSION
- $sha1 $sha1_short $_revision
+ $sha1 $sha1_short $_revision $_repository
$_q $_authors %users/;
$AUTHOR = 'Eric Wong <normalperson@yhbt.net>';
$VERSION = '@@GIT_VERSION@@';
$_template, $_shared,
$_version, $_fetch_all, $_no_rebase,
$_merge, $_strategy, $_dry_run, $_local,
- $_prefix, $_no_checkout, $_url, $_verbose);
+ $_prefix, $_no_checkout, $_url, $_verbose,
+ $_git_format);
$Git::SVN::_follow_parent = 1;
my %remote_opts = ( 'username=s' => \$Git::SVN::Prompt::_username,
'config-dir=s' => \$Git::SVN::Ra::config_dir,
'repack-flags|repack-args|repack-opts=s' =>
\$Git::SVN::_repack_flags,
'use-log-author' => \$Git::SVN::_use_log_author,
+ 'add-author-from' => \$Git::SVN::_add_author_from,
%remote_opts );
my ($_trunk, $_tags, $_branches, $_stdlayout);
'strategy|s=s' => \$_strategy,
'local|l' => \$_local,
'fetch-all|all' => \$_fetch_all,
+ 'dry-run|n' => \$_dry_run,
%fc_opts } ],
'commit-diff' => [ \&cmd_commit_diff,
'Commit a diff between two trees',
{ 'url' => \$_url, } ],
'blame' => [ \&Git::SVN::Log::cmd_blame,
"Show what revision and author last modified each line of a file",
- {} ],
+ { 'git-format' => \$_git_format } ],
);
my $cmd;
}
$ENV{GIT_DIR} = $git_dir;
}
+ $_repository = Git->repository(Repository => $ENV{GIT_DIR});
}
my %opts = %{$cmd{$cmd}->[2]} if (defined $cmd);
read_repo_config(\%opts);
-Getopt::Long::Configure('pass_through') if ($cmd && $cmd eq 'log');
+Getopt::Long::Configure('pass_through') if ($cmd && ($cmd eq 'log' || $cmd eq 'blame'));
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,
my $fd = $exit ? \*STDERR : \*STDOUT;
print $fd <<"";
git-svn - bidirectional operations between a single Subversion tree and git
-Usage: $0 <command> [options] [arguments]\n
+Usage: git svn <command> [options] [arguments]\n
print $fd "Available commands:\n" unless $cmd;
}
}
command_noisy(@init_db);
+ $_repository = Git->repository(Repository => ".git");
}
my $set;
my $pfx = "svn-remote.$Git::SVN::default_repo_id";
mkpath([$repo_path]) unless -d $repo_path;
chdir $repo_path or die "Couldn't chdir to $repo_path: $!\n";
$ENV{GIT_DIR} = '.git';
+ $_repository = Git->repository(Repository => $ENV{GIT_DIR});
}
sub cmd_clone {
$head ||= 'HEAD';
my @refs;
my ($url, $rev, $uuid, $gs) = working_head_info($head, \@refs);
- print "Committing to $url ...\n";
+ if ($url) {
+ print "Committing to $url ...\n";
+ }
unless ($gs) {
die "Unable to determine upstream SVN information from ",
- "$head history\n";
+ "$head history.\nPerhaps the repository is empty.";
}
my $last_rev;
my ($linear_refs, $parents) = linearize_history($gs, \@refs);
my $head = shift;
$head ||= 'HEAD';
my @refs;
- my (undef, undef, undef, $gs) = working_head_info($head, \@refs);
+ my (undef, undef, $uuid, $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_map_get($desired_revision);
+ $result = $gs->rev_map_get($desired_revision, $uuid);
} else {
my (undef, $rev, undef) = cmt_metadata($revision_or_hash);
$result = $rev;
die "Unable to determine upstream SVN information from ",
"working tree history\n";
}
+ if ($_dry_run) {
+ print "Remote Branch: " . $gs->refname . "\n";
+ print "SVN URL: " . $url . "\n";
+ return;
+ }
if (command(qw/diff-index HEAD --/)) {
print STDERR "Cannot rebase with uncommited changes:\n";
command_noisy('status');
print GITIGNORE "$s\n";
close(GITIGNORE)
or fatal("Failed to close `$ignore': $!");
- command_noisy('add', $ignore);
+ command_noisy('add', '-f', $ignore);
});
}
$path =~ s#/[^/]+/\.\.##g;
$path =~ s#/$##g;
$path =~ s#^\./## if $dot_slash_added;
+ $path =~ s#^/##;
+ $path =~ s#^\.$##;
return $path;
}
my $usage = "Usage: $0 commit-diff -r<revision> ".
"<tree-ish> <tree-ish> [<URL>]";
fatal($usage) if (!defined $ta || !defined $tb);
- my $svn_path;
+ my $svn_path = '';
if (!defined $url) {
my $gs = eval { Git::SVN->new };
if (!$gs) {
$_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;
my ($msg_fh, $ctx) = command_output_pipe('cat-file',
$type, $treeish);
my $in_msg = 0;
+ my $author;
+ my $saw_from = 0;
+ my $msgbuf = "";
while (<$msg_fh>) {
if (!$in_msg) {
$in_msg = 1 if (/^\s*$/);
+ $author = $1 if (/^author (.*>)/);
} 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 $!;
+ if (/^From:/ || /^Signed-off-by:/) {
+ $saw_from = 1;
+ }
+ $msgbuf .= $_;
}
}
+ $msgbuf =~ s/\s+$//s;
+ if ($Git::SVN::_add_author_from && defined($author)
+ && !$saw_from) {
+ $msgbuf .= "\n\nFrom: $author";
+ }
+ print $log_fh $msgbuf or croak $!;
command_close_pipe($msg_fh, $ctx);
}
close $log_fh or croak $!;
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_map_get($rev);
+ my $c = $gs->rev_map_get($rev, $uuid);
if ($c && $c eq $hash) {
close $fh; # break the pipe
return ($url, $rev, $uuid, $gs);
sub find_file_type_and_diff_status {
my ($path) = @_;
- return ('dir', '') if $path eq '.';
+ return ('dir', '') if $path eq '';
my $diff_output =
command_oneline(qw(diff --cached --name-status --), $path) || "";
use vars qw/$default_repo_id $default_ref_id $_no_metadata $_follow_parent
$_repack $_repack_flags $_use_svm_props $_head
$_use_svnsync_props $no_reuse_existing $_minimize_url
- $_use_log_author/;
+ $_use_log_author $_add_author_from/;
use Carp qw/croak/;
use File::Path qw/mkpath/;
use File::Copy qw/copy/;
sub read_all_remotes {
my $r = {};
+ my $use_svm_props = eval { command_oneline(qw/config --bool
+ svn.useSvmProps/) };
+ $use_svm_props = $use_svm_props eq 'true' if $use_svm_props;
foreach (grep { s/^svn-remote\.// } command(qw/config -l/)) {
if (m!^(.+)\.fetch=\s*(.*)\s*:\s*refs/remotes/(.+)\s*$!) {
my ($remote, $local_ref, $remote_ref) = ($1, $2, $3);
$local_ref =~ s{^/}{};
$r->{$remote}->{fetch}->{$local_ref} = $remote_ref;
+ $r->{$remote}->{svm} = {} if $use_svm_props;
+ } elsif (m!^(.+)\.usesvmprops=\s*(.*)\s*$!) {
+ $r->{$1}->{svm} = {};
} elsif (m!^(.+)\.url=\s*(.*)\s*$!) {
$r->{$1}->{url} = $2;
} elsif (m!^(.+)\.(branches|tags)=
}
}
}
+
+ map {
+ if (defined $r->{$_}->{svm}) {
+ my $svm;
+ eval {
+ my $section = "svn-remote.$_";
+ $svm = {
+ source => tmp_config('--get',
+ "$section.svm-source"),
+ replace => tmp_config('--get',
+ "$section.svm-replace"),
+ }
+ };
+ $r->{$_}->{svm} = $svm;
+ }
+ } keys %$r;
+
$r;
}
}
}
-# we allow more chars than remotes2config.sh...
-sub sanitize_remote_name {
- my ($name) = @_;
- $name =~ tr{A-Za-z0-9:,/+-}{.}c;
- $name;
-}
-
sub find_existing_remote {
my ($url, $remotes) = @_;
return undef if $no_reuse_existing;
}
my $p = $path;
my $rwr = rewrite_root({repo_id => $repo_id});
+ my $svm = $remotes->{$repo_id}->{svm}
+ if defined $remotes->{$repo_id}->{svm};
unless (defined $p) {
$p = $full_url;
my $z = $u;
+ my $prefix = '';
if ($rwr) {
$z = $rwr;
+ } elsif (defined $svm) {
+ $z = $svm->{source};
+ $prefix = $svm->{replace};
+ $prefix =~ s#^\Q$u\E(?:/|$)##;
+ $prefix =~ s#/$##;
}
- $p =~ s#^\Q$z\E(?:/|$)## or next;
+ $p =~ s#^\Q$z\E(?:/|$)#$prefix# or next;
}
foreach my $f (keys %$fetch) {
next if $f ne $p;
foreach (sort keys %$dirent) {
next if $dirent->{$_}->{kind} != $SVN::Node::dir;
- $self->prop_walk($p . $_, $rev, $sub);
+ $self->prop_walk($self->{path} . $p . $_, $rev, $sub);
}
}
$name_field = $1;
}
if (!defined $name_field) {
- #
+ if (!defined $email) {
+ $email = $name;
+ }
} elsif ($name_field =~ /(.*?)\s+<(.*)>/) {
($name, $email) = ($1, $2);
} elsif ($name_field =~ /(.*)@/) {
($name, $email) = ($1, $name_field);
} else {
- ($name, $email) = ($name_field, 'unknown');
+ ($name, $email) = ($name_field, $name_field);
}
}
if (defined $headrev && $self->use_svm_props) {
my ($log, $ctx) =
command_output_pipe(qw/rev-list --pretty=raw --no-color --reverse/,
$self->refname, '--');
- my $full_url = $self->full_url;
- remove_username($full_url);
+ my $metadata_url = $self->metadata_url;
+ remove_username($metadata_url);
my $svn_uuid = $self->ra_uuid;
my $c;
while (<$log>) {
# if we merged or otherwise started elsewhere, this is
# how we break out of it
if (($uuid ne $svn_uuid) ||
- ($full_url && $url && ($url ne $full_url))) {
+ ($metadata_url && $url && ($url ne $metadata_url))) {
next;
}
unless (defined $ref_id && length $ref_id) {
$_[2] = $ref_id = $Git::SVN::default_ref_id;
}
- $_[1] = $repo_id = sanitize_remote_name($repo_id);
+ $_[1] = $repo_id;
my $dir = "$ENV{GIT_DIR}/svn/$ref_id";
$_[3] = $path = '' unless (defined $path);
mkpath(["$ENV{GIT_DIR}/svn"]);
use strict;
use warnings;
use Carp qw/croak/;
+use File::Temp qw/tempfile/;
use IO::File qw//;
# file baton members: path, mode_a, mode_b, pool, fh, blob, base
my $base = IO::File->new_tmpfile;
$base->autoflush(1);
if ($fb->{blob}) {
- defined (my $pid = fork) or croak $!;
- if (!$pid) {
- open STDOUT, '>&', $base or croak $!;
- print STDOUT 'link ' if ($fb->{mode_a} == 120000);
- exec qw/git-cat-file blob/, $fb->{blob} or croak $!;
- }
- waitpid $pid, 0;
- croak $? if $?;
+ print $base 'link ' if ($fb->{mode_a} == 120000);
+ my $size = $::_repository->cat_blob($fb->{blob}, $base);
+ die "Failed to read object $fb->{blob}" if ($size < 0);
if (defined $exp) {
seek $base, 0, 0 or croak $!;
sysseek($fh, 0, 0) or croak $!;
}
}
- defined(my $pid = open my $out,'-|') or die "Can't fork: $!\n";
- if (!$pid) {
- open STDIN, '<&', $fh or croak $!;
- exec qw/git-hash-object -w --stdin/ or croak $!;
+
+ my ($tmp_fh, $tmp_filename) = File::Temp::tempfile(UNLINK => 1);
+ my $result;
+ while ($result = sysread($fh, my $string, 1024)) {
+ my $wrote = syswrite($tmp_fh, $string, $result);
+ defined($wrote) && $wrote == $result
+ or croak("write $tmp_filename: $!\n");
}
- chomp($hash = do { local $/; <$out> });
- close $out or croak $!;
+ defined $result or croak $!;
+ close $tmp_fh or croak $!;
+
close $fh or croak $!;
+
+ $hash = $::_repository->hash_and_insert_object($tmp_filename);
+ unlink($tmp_filename);
$hash =~ /^[a-f\d]{40}$/ or die "not a sha1: $hash\n";
close $fb->{base} or croak $!;
} else {
} elsif ($m->{mode_a} =~ /^120/ && $m->{mode_b} !~ /^120/) {
$self->change_file_prop($fbat,'svn:special',undef);
}
- defined(my $pid = fork) or croak $!;
- if (!$pid) {
- open STDOUT, '>&', $fh or croak $!;
- exec qw/git-cat-file blob/, $m->{sha1_b} or croak $!;
- }
- waitpid $pid, 0;
- croak $? if $?;
+ my $size = $::_repository->cat_blob($m->{sha1_b}, $fh);
+ croak "Failed to read object $m->{sha1_b}" if ($size < 0);
$fh->flush == 0 or croak $!;
seek $fh, 0, 0 or croak $!;
my ($uri) = @_;
my @tmp;
foreach (split m{/}, $uri) {
- s/([^\w.%-]|%(?![a-fA-F0-9]{2}))/sprintf("%%%02X",ord($1))/eg;
+ s/([^\w.%+-]|%(?![a-fA-F0-9]{2}))/sprintf("%%%02X",ord($1))/eg;
push @tmp, $_;
}
join('/', @tmp);
}
sub cmd_blame {
- my $path = shift;
+ my $path = pop;
config_pager();
run_pager();
- my ($fh, $ctx) = command_output_pipe('blame', @_, $path);
- while (my $line = <$fh>) {
- if ($line =~ /^\^?([[:xdigit:]]+)\s/) {
- my (undef, $rev, undef) = ::cmt_metadata($1);
- $rev = sprintf('%-10s', $rev);
- $line =~ s/^\^?[[:xdigit:]]+(\s)/$rev$1/;
+ my ($fh, $ctx, $rev);
+
+ if ($_git_format) {
+ ($fh, $ctx) = command_output_pipe('blame', @_, $path);
+ while (my $line = <$fh>) {
+ if ($line =~ /^\^?([[:xdigit:]]+)\s/) {
+ # Uncommitted edits show up as a rev ID of
+ # all zeros, which we can't look up with
+ # cmt_metadata
+ if ($1 !~ /^0+$/) {
+ (undef, $rev, undef) =
+ ::cmt_metadata($1);
+ $rev = '0' if (!$rev);
+ } else {
+ $rev = '0';
+ }
+ $rev = sprintf('%-10s', $rev);
+ $line =~ s/^\^?[[:xdigit:]]+(\s)/$rev$1/;
+ }
+ print $line;
+ }
+ } else {
+ ($fh, $ctx) = command_output_pipe('blame', '-p', @_, 'HEAD',
+ '--', $path);
+ my ($sha1);
+ my %authors;
+ while (my $line = <$fh>) {
+ if ($line =~ /^([[:xdigit:]]{40})\s\d+\s\d+/) {
+ $sha1 = $1;
+ (undef, $rev, undef) = ::cmt_metadata($1);
+ $rev = '0' if (!$rev);
+ }
+ elsif ($line =~ /^author (.*)/) {
+ $authors{$rev} = $1;
+ $authors{$rev} =~ s/\s/_/g;
+ }
+ elsif ($line =~ /^\t(.*)$/) {
+ printf("%6s %10s %s\n", $rev, $authors{$rev}, $1);
+ }
}
- print $line;
}
command_close_pipe($fh, $ctx);
}
mkpath([$svn_dir]);
print STDERR "Data from a previous version of git-svn exists, but\n\t",
"$svn_dir\n\t(required for this version ",
- "($::VERSION) of git-svn) does not. exist\n";
+ "($::VERSION) of git-svn) does not exist.\n";
my ($fh, $ctx) = command_output_pipe(qw/rev-parse --symbolic --all/);
while (<$fh>) {
my $x = $_;
# skip existing cases where we already connect to the root
if (($ra->{url} eq $ra->{repos_root}) ||
- (Git::SVN::sanitize_remote_name($ra->{repos_root}) eq
- $repo_id)) {
+ ($ra->{repos_root} eq $repo_id)) {
$root_repos->{$ra->{url}} = $repo_id;
next;
}
foreach my $url (keys %$new_urls) {
# see if we can re-use an existing [svn-remote "repo_id"]
# instead of creating a(n ugly) new section:
- my $repo_id = $root_repos->{$url} ||
- Git::SVN::sanitize_remote_name($url);
+ my $repo_id = $root_repos->{$url} || $url;
my $fetch = $new_urls->{$url};
foreach my $path (keys %$fetch) {