Browse Source

Merge branch 'ra/anno' into next

* ra/anno:
  Add git-annotate, a tool for assigning blame.
  git-svn: 0.9.1: add --version and copyright/license (GPL v2+) information
  contrib/git-svn: add Makefile, test, and associated ignores
  git-svn: fix several corner-case and rare bugs with 'commit'
  contrib/git-svn.txt: add a note about renamed/copied directory support
  git-svn: change ; to && in addremove()
  git-svn: remove any need for the XML::Simple dependency
  git-svn: Allow for more argument types for commit (from..to)
  git-svn: allow --find-copies-harder and -l<num> to be passed on commit
  git-svn: fix a typo in defining the --no-stop-on-copy option
maint
Junio C Hamano 19 years ago
parent
commit
0c82a398ec
  1. 1
      Makefile
  2. 4
      contrib/git-svn/.gitignore
  3. 32
      contrib/git-svn/Makefile
  4. 382
      contrib/git-svn/git-svn.perl
  5. 16
      contrib/git-svn/git-svn.txt
  6. 216
      contrib/git-svn/t/t0000-contrib-git-svn.sh
  7. 321
      git-annotate.perl

1
Makefile

@ -124,6 +124,7 @@ SCRIPT_SH = \ @@ -124,6 +124,7 @@ SCRIPT_SH = \
SCRIPT_PERL = \
git-archimport.perl git-cvsimport.perl git-relink.perl \
git-shortlog.perl git-fmt-merge-msg.perl git-rerere.perl \
git-annotate.perl \
git-svnimport.perl git-mv.perl git-cvsexportcommit.perl

SCRIPT_PYTHON = \

4
contrib/git-svn/.gitignore vendored

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
git-svn
git-svn.xml
git-svn.html
git-svn.1

32
contrib/git-svn/Makefile

@ -0,0 +1,32 @@ @@ -0,0 +1,32 @@
all: git-svn

prefix?=$(HOME)
bindir=$(prefix)/bin
mandir=$(prefix)/man
man1=$(mandir)/man1
INSTALL?=install
doc_conf=../../Documentation/asciidoc.conf
-include ../../config.mak

git-svn: git-svn.perl
cp $< $@
chmod +x $@

install: all
$(INSTALL) -d -m755 $(DESTDIR)$(bindir)
$(INSTALL) git-svn $(DESTDIR)$(bindir)

install-doc: doc
$(INSTALL) git-svn.1 $(DESTDIR)$(man1)

doc: git-svn.1
git-svn.1 : git-svn.xml
xmlto man git-svn.xml
git-svn.xml : git-svn.txt
asciidoc -b docbook -d manpage \
-f ../../Documentation/asciidoc.conf $<
test:
cd t && $(SHELL) ./t0000-contrib-git-svn.sh

clean:
rm -f git-svn *.xml *.html *.1

382
contrib/git-svn/git-svn → contrib/git-svn/git-svn.perl

@ -1,4 +1,6 @@ @@ -1,4 +1,6 @@
#!/usr/bin/env perl
# Copyright (C) 2006, Eric Wong <normalperson@yhbt.net>
# License: GPL v2 or later
use warnings;
use strict;
use vars qw/ $AUTHOR $VERSION
@ -6,7 +8,7 @@ use vars qw/ $AUTHOR $VERSION @@ -6,7 +8,7 @@ use vars qw/ $AUTHOR $VERSION
$GIT_SVN_INDEX $GIT_SVN
$GIT_DIR $REV_DIR/;
$AUTHOR = 'Eric Wong <normalperson@yhbt.net>';
$VERSION = '0.9.0';
$VERSION = '0.9.1';
$GIT_DIR = $ENV{GIT_DIR} || "$ENV{PWD}/.git";
$GIT_SVN = $ENV{GIT_SVN_ID} || 'git-svn';
$GIT_SVN_INDEX = "$GIT_DIR/$GIT_SVN/index";
@ -21,7 +23,7 @@ $ENV{LC_ALL} = 'C'; @@ -21,7 +23,7 @@ $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.
# See what I do with XML::Simple to make the dependency optional.
# use eval { require SVN::... } to make it lazy load
use Carp qw/croak/;
use IO::File qw//;
use File::Basename qw/dirname basename/;
@ -30,7 +32,8 @@ use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev/; @@ -30,7 +32,8 @@ use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev/;
use File::Spec qw//;
my $sha1 = qr/[a-f\d]{40}/;
my $sha1_short = qr/[a-f\d]{6,40}/;
my ($_revision,$_stdin,$_no_ignore_ext,$_no_stop_copy,$_help,$_rmdir,$_edit);
my ($_revision,$_stdin,$_no_ignore_ext,$_no_stop_copy,$_help,$_rmdir,$_edit,
$_find_copies_harder, $_l, $_version);

GetOptions( 'revision|r=s' => \$_revision,
'no-ignore-externals' => \$_no_ignore_ext,
@ -38,7 +41,10 @@ GetOptions( 'revision|r=s' => \$_revision, @@ -38,7 +41,10 @@ GetOptions( 'revision|r=s' => \$_revision,
'edit|e' => \$_edit,
'rmdir' => \$_rmdir,
'help|H|h' => \$_help,
'no-stop-copy' => \$_no_stop_copy );
'find-copies-harder' => \$_find_copies_harder,
'l=i' => \$_l,
'version|V' => \$_version,
'no-stop-on-copy' => \$_no_stop_copy );
my %cmd = (
fetch => [ \&fetch, "Download new revisions from SVN" ],
init => [ \&init, "Initialize and fetch (import)"],
@ -63,6 +69,7 @@ foreach (keys %cmd) { @@ -63,6 +69,7 @@ foreach (keys %cmd) {
}
}
usage(0) if $_help;
version() if $_version;
usage(1) unless (defined $cmd);
svn_check_ignore_externals();
$cmd{$cmd}->[0]->(@ARGV);
@ -88,6 +95,11 @@ and want to keep them separate. @@ -88,6 +95,11 @@ and want to keep them separate.
exit $exit;
}

sub version {
print "git-svn version $VERSION\n";
exit 0;
}

sub rebuild {
$SVN_URL = shift or undef;
my $repo_uuid;
@ -174,8 +186,7 @@ sub fetch { @@ -174,8 +186,7 @@ sub fetch {
push @log_args, "-r$_revision";
push @log_args, '--stop-on-copy' unless $_no_stop_copy;

eval { require XML::Simple or croak $! };
my $svn_log = $@ ? svn_log_raw(@log_args) : svn_log_xml(@log_args);
my $svn_log = svn_log_raw(@log_args);
@$svn_log = sort { $a->{revision} <=> $b->{revision} } @$svn_log;

my $base = shift @$svn_log or croak "No base revision!\n";
@ -213,14 +224,21 @@ sub commit { @@ -213,14 +224,21 @@ sub commit {
print "Reading from stdin...\n";
@commits = ();
while (<STDIN>) {
if (/^([a-f\d]{6,40})\b/) {
if (/\b([a-f\d]{6,40})\b/) {
unshift @commits, $1;
}
}
}
my @revs;
foreach (@commits) {
push @revs, (safe_qx('git-rev-parse',$_));
foreach my $c (@commits) {
chomp(my @tmp = safe_qx('git-rev-parse',$c));
if (scalar @tmp == 1) {
push @revs, $tmp[0];
} elsif (scalar @tmp > 1) {
push @revs, reverse (safe_qx('git-rev-list',@tmp));
} else {
die "Failed to rev-parse $c\n";
}
}
chomp @revs;

@ -229,7 +247,11 @@ sub commit { @@ -229,7 +247,11 @@ sub commit {
my $svn_current_rev = svn_info('.')->{'Last Changed Rev'};
foreach my $c (@revs) {
print "Committing $c\n";
svn_checkout_tree($svn_current_rev, $c);
my $mods = svn_checkout_tree($svn_current_rev, $c);
if (scalar @$mods == 0) {
print "Skipping, no changes detected\n";
next;
}
$svn_current_rev = svn_commit_tree($svn_current_rev, $c);
}
print "Done committing ",scalar @revs," revisions to SVN\n";
@ -258,9 +280,9 @@ sub setup_git_svn { @@ -258,9 +280,9 @@ sub setup_git_svn {
}

sub assert_svn_wc_clean {
my ($svn_rev, $commit) = @_;
my ($svn_rev, $treeish) = @_;
croak "$svn_rev is not an integer!\n" unless ($svn_rev =~ /^\d+$/);
croak "$commit is not a sha1!\n" unless ($commit =~ /^$sha1$/o);
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",
@ -273,12 +295,42 @@ sub assert_svn_wc_clean { @@ -273,12 +295,42 @@ sub assert_svn_wc_clean {
print STDERR $_ foreach @status;
croak;
}
my ($tree_a) = grep(/^tree $sha1$/o,`git-cat-file commit $commit`);
$tree_a =~ s/^tree //;
chomp $tree_a;
chomp(my $tree_b = `GIT_INDEX_FILE=$GIT_SVN_INDEX git-write-tree`);
if ($tree_a ne $tree_b) {
croak "$svn_rev != $commit, $tree_a != $tree_b\n";
assert_tree($treeish);
}

sub assert_tree {
my ($treeish) = @_;
croak "Not a sha1: $treeish\n" unless $treeish =~ /^$sha1$/o;
chomp(my $type = `git-cat-file -t $treeish`);
my $expected;
while ($type eq 'tag') {
chomp(($treeish, $type) = `git-cat-file tag $treeish`);
}
if ($type eq 'commit') {
$expected = (grep /^tree /,`git-cat-file commit $treeish`)[0];
($expected) = ($expected =~ /^tree ($sha1)$/);
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";
}

my $old_index = $ENV{GIT_INDEX_FILE};
my $tmpindex = $GIT_SVN_INDEX.'.assert-tmp';
if (-e $tmpindex) {
unlink $tmpindex or croak $!;
}
$ENV{GIT_INDEX_FILE} = $tmpindex;
git_addremove();
chomp(my $tree = `git-write-tree`);
if ($old_index) {
$ENV{GIT_INDEX_FILE} = $old_index;
} else {
delete $ENV{GIT_INDEX_FILE};
}
if ($tree ne $expected) {
croak "Tree mismatch, Got: $tree, Expected: $expected\n";
}
}

@ -289,7 +341,6 @@ sub parse_diff_tree { @@ -289,7 +341,6 @@ sub parse_diff_tree {
my @mods;
while (<$diff_fh>) {
chomp $_; # this gets rid of the trailing "\0"
print $_,"\n";
if ($state eq 'meta' && /^:(\d{6})\s(\d{6})\s
$sha1\s($sha1)\s([MTCRAD])\d*$/xo) {
push @mods, { mode_a => $1, mode_b => $2,
@ -300,36 +351,44 @@ sub parse_diff_tree { @@ -300,36 +351,44 @@ sub parse_diff_tree {
$state = 'file_b';
}
} elsif ($state eq 'file_a') {
my $x = $mods[$#mods] or croak __LINE__,": Empty array\n";
my $x = $mods[$#mods] or croak "Empty array\n";
if ($x->{chg} !~ /^(?:C|R)$/) {
croak __LINE__,": Error parsing $_, $x->{chg}\n";
croak "Error parsing $_, $x->{chg}\n";
}
$x->{file_a} = $_;
$state = 'file_b';
} elsif ($state eq 'file_b') {
my $x = $mods[$#mods] or croak __LINE__,": Empty array\n";
my $x = $mods[$#mods] or croak "Empty array\n";
if (exists $x->{file_a} && $x->{chg} !~ /^(?:C|R)$/) {
croak __LINE__,": Error parsing $_, $x->{chg}\n";
croak "Error parsing $_, $x->{chg}\n";
}
if (!exists $x->{file_a} && $x->{chg} =~ /^(?:C|R)$/) {
croak __LINE__,": Error parsing $_, $x->{chg}\n";
croak "Error parsing $_, $x->{chg}\n";
}
$x->{file_b} = $_;
$state = 'meta';
} else {
croak __LINE__,": Error parsing $_\n";
croak "Error parsing $_\n";
}
}
close $diff_fh or croak $!;

return \@mods;
}

sub svn_check_prop_executable {
my $m = shift;
if ($m->{mode_b} =~ /755$/ && $m->{mode_a} !~ /755$/) {
sys(qw(svn propset svn:executable 1), $m->{file_b});
return if -l $m->{file_b};
if ($m->{mode_b} =~ /755$/) {
chmod((0755 &~ umask),$m->{file_b}) or croak $!;
if ($m->{mode_a} !~ /755$/) {
sys(qw(svn propset svn:executable 1), $m->{file_b});
}
-x $m->{file_b} or croak "$m->{file_b} is not executable!\n";
} elsif ($m->{mode_b} !~ /755$/ && $m->{mode_a} =~ /755$/) {
sys(qw(svn propdel svn:executable), $m->{file_b});
chmod((0644 &~ umask),$m->{file_b}) or croak $!;
-x $m->{file_b} and croak "$m->{file_b} is executable!\n";
}
}

@ -340,81 +399,166 @@ sub svn_ensure_parent_path { @@ -340,81 +399,166 @@ sub svn_ensure_parent_path {
sys(qw(svn add -N), $dir_b) unless (-d "$dir_b/.svn");
}

sub precommit_check {
my $mods = shift;
my (%rm_file, %rmdir_check, %added_check);

my %o = ( D => 0, R => 1, C => 2, A => 3, M => 3, T => 3 );
foreach my $m (sort { $o{$a->{chg}} <=> $o{$b->{chg}} } @$mods) {
if ($m->{chg} eq 'R') {
if (-d $m->{file_b}) {
err_dir_to_file("$m->{file_a} => $m->{file_b}");
}
# dir/$file => dir/file/$file
my $dirname = dirname($m->{file_b});
while ($dirname ne File::Spec->curdir) {
if ($dirname ne $m->{file_a}) {
$dirname = dirname($dirname);
next;
}
err_file_to_dir("$m->{file_a} => $m->{file_b}");
}
# baz/zzz => baz (baz is a file)
$dirname = dirname($m->{file_a});
while ($dirname ne File::Spec->curdir) {
if ($dirname ne $m->{file_b}) {
$dirname = dirname($dirname);
next;
}
err_dir_to_file("$m->{file_a} => $m->{file_b}");
}
}
if ($m->{chg} =~ /^(D|R)$/) {
my $t = $1 eq 'D' ? 'file_b' : 'file_a';
$rm_file{ $m->{$t} } = 1;
my $dirname = dirname( $m->{$t} );
my $basename = basename( $m->{$t} );
$rmdir_check{$dirname}->{$basename} = 1;
} elsif ($m->{chg} =~ /^(?:A|C)$/) {
if (-d $m->{file_b}) {
err_dir_to_file($m->{file_b});
}
my $dirname = dirname( $m->{file_b} );
my $basename = basename( $m->{file_b} );
$added_check{$dirname}->{$basename} = 1;
while ($dirname ne File::Spec->curdir) {
if ($rm_file{$dirname}) {
err_file_to_dir($m->{file_b});
}
$dirname = dirname $dirname;
}
}
}
return (\%rmdir_check, \%added_check);

sub err_dir_to_file {
my $file = shift;
print STDERR "Node change from directory to file ",
"is not supported by Subversion: ",$file,"\n";
exit 1;
}
sub err_file_to_dir {
my $file = shift;
print STDERR "Node change from file to directory ",
"is not supported by Subversion: ",$file,"\n";
exit 1;
}
}

sub svn_checkout_tree {
my ($svn_rev, $commit) = @_;
my ($svn_rev, $treeish) = @_;
my $from = file_to_s("$REV_DIR/$svn_rev");
assert_svn_wc_clean($svn_rev,$from);
print "diff-tree '$from' '$commit'\n";
print "diff-tree '$from' '$treeish'\n";
my $pid = open my $diff_fh, '-|';
defined $pid or croak $!;
if ($pid == 0) {
exec(qw(git-diff-tree -z -r -C), $from, $commit) or croak $!;
my @diff_tree = qw(git-diff-tree -z -r -C);
push @diff_tree, '--find-copies-harder' if $_find_copies_harder;
push @diff_tree, "-l$_l" if defined $_l;
exec(@diff_tree, $from, $treeish) or croak $!;
}
my $mods = parse_diff_tree($diff_fh);
unless (@$mods) {
# git can do empty commits, SVN doesn't allow it...
return $svn_rev;
return $mods;
}
my %rm;
foreach my $m (@$mods) {
my ($rm, $add) = precommit_check($mods);

my %o = ( D => 1, R => 0, C => -1, A => 3, M => 3, T => 3 );
foreach my $m (sort { $o{$a->{chg}} <=> $o{$b->{chg}} } @$mods) {
if ($m->{chg} eq 'C') {
svn_ensure_parent_path( $m->{file_b} );
sys(qw(svn cp), $m->{file_a}, $m->{file_b});
blob_to_file( $m->{sha1_b}, $m->{file_b});
apply_mod_line_blob($m);
svn_check_prop_executable($m);
} elsif ($m->{chg} eq 'D') {
$rm{dirname $m->{file_b}}->{basename $m->{file_b}} = 1;
sys(qw(svn rm --force), $m->{file_b});
} elsif ($m->{chg} eq 'R') {
svn_ensure_parent_path( $m->{file_b} );
sys(qw(svn mv --force), $m->{file_a}, $m->{file_b});
blob_to_file( $m->{sha1_b}, $m->{file_b});
apply_mod_line_blob($m);
svn_check_prop_executable($m);
$rm{dirname $m->{file_a}}->{basename $m->{file_a}} = 1;
} elsif ($m->{chg} eq 'M') {
if ($m->{mode_b} =~ /^120/ && $m->{mode_a} =~ /^120/) {
unlink $m->{file_b} or croak $!;
blob_to_symlink($m->{sha1_b}, $m->{file_b});
} else {
blob_to_file($m->{sha1_b}, $m->{file_b});
}
apply_mod_line_blob($m);
svn_check_prop_executable($m);
} elsif ($m->{chg} eq 'T') {
sys(qw(svn rm --force),$m->{file_b});
if ($m->{mode_b} =~ /^120/ && $m->{mode_a} =~ /^100/) {
blob_to_symlink($m->{sha1_b}, $m->{file_b});
} else {
blob_to_file($m->{sha1_b}, $m->{file_b});
}
svn_check_prop_executable($m);
apply_mod_line_blob($m);
sys(qw(svn add --force), $m->{file_b});
svn_check_prop_executable($m);
} elsif ($m->{chg} eq 'A') {
svn_ensure_parent_path( $m->{file_b} );
blob_to_file( $m->{sha1_b}, $m->{file_b});
if ($m->{mode_b} =~ /755$/) {
chmod 0755, $m->{file_b};
}
apply_mod_line_blob($m);
sys(qw(svn add --force), $m->{file_b});
svn_check_prop_executable($m);
} else {
croak "Invalid chg: $m->{chg}\n";
}
}
if ($_rmdir) {
my $old_index = $ENV{GIT_INDEX_FILE};
$ENV{GIT_INDEX_FILE} = $GIT_SVN_INDEX;
foreach my $dir (keys %rm) {
my $files = $rm{$dir};
my @files;
foreach (safe_qx('svn','ls',$dir)) {
chomp;
push @files, $_ unless $files->{$_};
}
sys(qw(svn rm),$dir) unless @files;
}
if ($old_index) {
$ENV{GIT_INDEX_FILE} = $old_index;
} else {
delete $ENV{GIT_INDEX_FILE};

assert_tree($treeish);
if ($_rmdir) { # remove empty directories
handle_rmdir($rm, $add);
}
assert_tree($treeish);
return $mods;
}

# svn ls doesn't work with respect to the current working tree, but what's
# in the repository. There's not even an option for it... *sigh*
# (added files don't show up and removed files remain in the ls listing)
sub svn_ls_current {
my ($dir, $rm, $add) = @_;
chomp(my @ls = safe_qx('svn','ls',$dir));
my @ret = ();
foreach (@ls) {
s#/$##; # trailing slashes are evil
push @ret, $_ unless $rm->{$dir}->{$_};
}
if (exists $add->{$dir}) {
push @ret, keys %{$add->{$dir}};
}
return \@ret;
}

sub handle_rmdir {
my ($rm, $add) = @_;

foreach my $dir (sort {length $b <=> length $a} keys %$rm) {
my $ls = svn_ls_current($dir, $rm, $add);
next if (scalar @$ls);
sys(qw(svn rm --force),$dir);

my $dn = dirname $dir;
$rm->{ $dn }->{ basename $dir } = 1;
$ls = svn_ls_current($dn, $rm, $add);
while (scalar @$ls == 0 && $dn ne File::Spec->curdir) {
sys(qw(svn rm --force),$dn);
$dir = basename $dn;
$dn = dirname $dn;
$rm->{ $dn }->{ $dir } = 1;
$ls = svn_ls_current($dn, $rm, $add);
}
}
}
@ -463,49 +607,6 @@ sub svn_commit_tree { @@ -463,49 +607,6 @@ sub svn_commit_tree {
return fetch("$rev_committed=$commit")->{revision};
}

sub svn_log_xml {
my (@log_args) = @_;
my $log_fh = IO::File->new_tmpfile or croak $!;

my $pid = fork;
defined $pid or croak $!;

if ($pid == 0) {
open STDOUT, '>&', $log_fh or croak $!;
exec (qw(svn log --xml), @log_args) or croak $!
}

waitpid $pid, 0;
croak $? if $?;

seek $log_fh, 0, 0;
my @svn_log;
my $log = XML::Simple::XMLin( $log_fh,
ForceArray => ['path','revision','logentry'],
KeepRoot => 0,
KeyAttr => { logentry => '+revision',
paths => '+path' },
)->{logentry};
foreach my $r (sort {$a <=> $b} keys %$log) {
my $log_msg = $log->{$r};
my ($Y,$m,$d,$H,$M,$S) = ($log_msg->{date} =~
/(\d{4})\-(\d\d)\-(\d\d)T
(\d\d)\:(\d\d)\:(\d\d)\.\d+Z$/x)
or croak "Failed to parse date: ",
$log->{$r}->{date};
$log_msg->{date} = "+0000 $Y-$m-$d $H:$M:$S";

# XML::Simple can't handle <msg></msg> as a string:
if (ref $log_msg->{msg} eq 'HASH') {
$log_msg->{msg} = "\n";
} else {
$log_msg->{msg} .= "\n";
}
push @svn_log, $log->{$r};
}
return \@svn_log;
}

sub svn_log_raw {
my (@log_args) = @_;
my $pid = open my $log_fh,'-|';
@ -516,21 +617,42 @@ sub svn_log_raw { @@ -516,21 +617,42 @@ sub svn_log_raw {
}

my @svn_log;
my $state;
my $state = 'sep';
while (<$log_fh>) {
chomp;
if (/^\-{72}$/) {
if ($state eq 'msg') {
if ($svn_log[$#svn_log]->{lines}) {
$svn_log[$#svn_log]->{msg} .= $_."\n";
unless(--$svn_log[$#svn_log]->{lines}) {
$state = 'sep';
}
} else {
croak "Log parse error at: $_\n",
$svn_log[$#svn_log]->{revision},
"\n";
}
next;
}
if ($state ne 'sep') {
croak "Log parse error at: $_\n",
"state: $state\n",
$svn_log[$#svn_log]->{revision},
"\n";
}
$state = 'rev';

# if we have an empty log message, put something there:
if (@svn_log) {
$svn_log[$#svn_log]->{msg} ||= "\n";
delete $svn_log[$#svn_log]->{lines};
}
next;
}
if ($state eq 'rev' && s/^r(\d+)\s*\|\s*//) {
my $rev = $1;
my ($author, $date) = split(/\s*\|\s*/, $_, 2);
my ($author, $date, $lines) = split(/\s*\|\s*/, $_, 3);
($lines) = ($lines =~ /(\d+)/);
my ($Y,$m,$d,$H,$M,$S,$tz) = ($date =~
/(\d{4})\-(\d\d)\-(\d\d)\s
(\d\d)\:(\d\d)\:(\d\d)\s([\-\+]\d+)/x)
@ -538,6 +660,7 @@ sub svn_log_raw { @@ -538,6 +660,7 @@ sub svn_log_raw {
my %log_msg = ( revision => $rev,
date => "$tz $Y-$m-$d $H:$M:$S",
author => $author,
lines => $lines,
msg => '' );
push @svn_log, \%log_msg;
$state = 'msg_start';
@ -547,7 +670,15 @@ sub svn_log_raw { @@ -547,7 +670,15 @@ sub svn_log_raw {
if ($state eq 'msg_start' && /^$/) {
$state = 'msg';
} elsif ($state eq 'msg') {
$svn_log[$#svn_log]->{msg} .= $_."\n";
if ($svn_log[$#svn_log]->{lines}) {
$svn_log[$#svn_log]->{msg} .= $_."\n";
unless (--$svn_log[$#svn_log]->{lines}) {
$state = 'sep';
}
} else {
croak "Log parse error at: $_\n",
$svn_log[$#svn_log]->{revision},"\n";
}
}
}
close $log_fh or croak $?;
@ -581,10 +712,10 @@ sub sys { system(@_) == 0 or croak $? } @@ -581,10 +712,10 @@ sub sys { system(@_) == 0 or croak $? }

sub git_addremove {
system( "git-diff-files --name-only -z ".
" | git-update-index --remove -z --stdin; ".
" | git-update-index --remove -z --stdin && ".
"git-ls-files -z --others ".
"'--exclude-from=$GIT_DIR/$GIT_SVN/info/exclude'".
" | git-update-index --add -z --stdin; "
" | git-update-index --add -z --stdin"
) == 0 or croak $?
}

@ -693,10 +824,23 @@ sub git_commit { @@ -693,10 +824,23 @@ sub git_commit {
return $commit;
}

sub apply_mod_line_blob {
my $m = shift;
if ($m->{mode_b} =~ /^120/) {
blob_to_symlink($m->{sha1_b}, $m->{file_b});
} else {
blob_to_file($m->{sha1_b}, $m->{file_b});
}
}

sub blob_to_symlink {
my ($blob, $link) = @_;
defined $link or croak "\$link not defined!\n";
croak "Not a sha1: $blob\n" unless $blob =~ /^$sha1$/o;
if (-l $link || -f _) {
unlink $link or croak $!;
}

my $dest = `git-cat-file blob $blob`; # no newline, so no chomp
symlink $dest, $link or croak $!;
}
@ -705,6 +849,10 @@ sub blob_to_file { @@ -705,6 +849,10 @@ sub blob_to_file {
my ($blob, $file) = @_;
defined $file or croak "\$file not defined!\n";
croak "Not a sha1: $blob\n" unless $blob =~ /^$sha1$/o;
if (-l $file || -f _) {
unlink $file or croak $!;
}

open my $blob_fh, '>', $file or croak "$!: $file\n";
my $pid = fork;
defined $pid or croak $!;

16
contrib/git-svn/git-svn.txt

@ -99,6 +99,13 @@ OPTIONS @@ -99,6 +99,13 @@ OPTIONS
default for objects that are commits, and forced on when committing
tree objects.

-l<num>::
--find-copies-harder::
Both of these are only used with the 'commit' command.

They are both passed directly to git-diff-tree see
git-diff-tree(1) for more information.

COMPATIBILITY OPTIONS
---------------------
--no-ignore-externals::
@ -142,7 +149,7 @@ Tracking and contributing to an Subversion managed-project: @@ -142,7 +149,7 @@ Tracking and contributing to an Subversion managed-project:
# Commit only the git commits you want to SVN::
git-svn commit <tree-ish> [<tree-ish_2> ...]
# Commit all the git commits from my-branch that don't exist in SVN::
git rev-list --pretty=oneline git-svn-HEAD..my-branch | git-svn commit
git commit git-svn-HEAD..my-branch
# Something is committed to SVN, pull the latest into your branch::
git-svn fetch && git pull . git-svn-HEAD

@ -199,6 +206,13 @@ working trees with metadata files. @@ -199,6 +206,13 @@ working trees with metadata files.
svn:keywords can't be ignored in Subversion (at least I don't know of
a way to ignore them).

Renamed and copied directories are not detected by git and hence not
tracked when committing to SVN. I do not plan on adding support for
this as it's quite difficult and time-consuming to get working for all
the possible corner cases (git doesn't do it, either). Renamed and
copied files are fully supported if they're similar enough for git to
detect them.

Author
------
Written by Eric Wong <normalperson@yhbt.net>.

216
contrib/git-svn/t/t0000-contrib-git-svn.sh

@ -0,0 +1,216 @@ @@ -0,0 +1,216 @@
#!/bin/sh
#
# Copyright (c) 2006 Eric Wong
#


PATH=$PWD/../:$PATH
test_description='git-svn tests'
if test -d ../../../t
then
cd ../../../t
else
echo "Must be run in contrib/git-svn/t" >&2
exit 1
fi

. ./test-lib.sh

GIT_DIR=$PWD/.git
GIT_SVN_DIR=$GIT_DIR/git-svn
SVN_TREE=$GIT_SVN_DIR/tree

svnadmin >/dev/null 2>&1
if test $? != 1
then
test_expect_success 'skipping contrib/git-svn test' :
test_done
exit
fi

svn >/dev/null 2>&1
if test $? != 1
then
test_expect_success 'skipping contrib/git-svn test' :
test_done
exit
fi

svnrepo=$PWD/svnrepo

set -e

svnadmin create $svnrepo
svnrepo="file://$svnrepo/test-git-svn"

mkdir import

cd import

echo foo > foo
ln -s foo foo.link
mkdir -p dir/a/b/c/d/e
echo 'deep dir' > dir/a/b/c/d/e/file
mkdir -p bar
echo 'zzz' > bar/zzz
echo '#!/bin/sh' > exec.sh
chmod +x exec.sh
svn import -m 'import for git-svn' . $svnrepo >/dev/null

cd ..

rm -rf import

test_expect_success \
'initialize git-svn' \
"git-svn init $svnrepo"

test_expect_success \
'import an SVN revision into git' \
'git-svn fetch'


name='try a deep --rmdir with a commit'
git checkout -b mybranch git-svn-HEAD
mv dir/a/b/c/d/e/file dir/file
cp dir/file file
git update-index --add --remove dir/a/b/c/d/e/file dir/file file
git commit -m "$name"

test_expect_success "$name" \
"git-svn commit --find-copies-harder --rmdir git-svn-HEAD..mybranch &&
test -d $SVN_TREE/dir && test ! -d $SVN_TREE/dir/a"


name='detect node change from file to directory #1'
mkdir dir/new_file
mv dir/file dir/new_file/file
mv dir/new_file dir/file
git update-index --remove dir/file
git update-index --add dir/file/file
git commit -m "$name"

test_expect_code 1 "$name" \
'git-svn commit --find-copies-harder --rmdir git-svn-HEAD..mybranch' \
|| true


name='detect node change from directory to file #1'
rm -rf dir $GIT_DIR/index
git checkout -b mybranch2 git-svn-HEAD
mv bar/zzz zzz
rm -rf bar
mv zzz bar
git update-index --remove -- bar/zzz
git update-index --add -- bar
git commit -m "$name"

test_expect_code 1 "$name" \
'git-svn commit --find-copies-harder --rmdir git-svn-HEAD..mybranch2' \
|| true


name='detect node change from file to directory #2'
rm -f $GIT_DIR/index
git checkout -b mybranch3 git-svn-HEAD
rm bar/zzz
git-update-index --remove bar/zzz
mkdir bar/zzz
echo yyy > bar/zzz/yyy
git-update-index --add bar/zzz/yyy
git commit -m "$name"

test_expect_code 1 "$name" \
'git-svn commit --find-copies-harder --rmdir git-svn-HEAD..mybranch3' \
|| true


name='detect node change from directory to file #2'
rm -f $GIT_DIR/index
git checkout -b mybranch4 git-svn-HEAD
rm -rf dir
git update-index --remove -- dir/file
touch dir
echo asdf > dir
git update-index --add -- dir
git commit -m "$name"

test_expect_code 1 "$name" \
'git-svn commit --find-copies-harder --rmdir git-svn-HEAD..mybranch4' \
|| true


name='remove executable bit from a file'
rm -f $GIT_DIR/index
git checkout -b mybranch5 git-svn-HEAD
chmod -x exec.sh
git update-index exec.sh
git commit -m "$name"

test_expect_success "$name" \
"git-svn commit --find-copies-harder --rmdir git-svn-HEAD..mybranch5 &&
test ! -x $SVN_TREE/exec.sh"


name='add executable bit back file'
chmod +x exec.sh
git update-index exec.sh
git commit -m "$name"

test_expect_success "$name" \
"git-svn commit --find-copies-harder --rmdir git-svn-HEAD..mybranch5 &&
test -x $SVN_TREE/exec.sh"



name='executable file becomes a symlink to bar/zzz (file)'
rm exec.sh
ln -s bar/zzz exec.sh
git update-index exec.sh
git commit -m "$name"

test_expect_success "$name" \
"git-svn commit --find-copies-harder --rmdir git-svn-HEAD..mybranch5 &&
test -L $SVN_TREE/exec.sh"



name='new symlink is added to a file that was also just made executable'
chmod +x bar/zzz
ln -s bar/zzz exec-2.sh
git update-index --add bar/zzz exec-2.sh
git commit -m "$name"

test_expect_success "$name" \
"git-svn commit --find-copies-harder --rmdir git-svn-HEAD..mybranch5 &&
test -x $SVN_TREE/bar/zzz &&
test -L $SVN_TREE/exec-2.sh"



name='modify a symlink to become a file'
git help > help || true
rm exec-2.sh
cp help exec-2.sh
git update-index exec-2.sh
git commit -m "$name"

test_expect_success "$name" \
"git-svn commit --find-copies-harder --rmdir git-svn-HEAD..mybranch5 &&
test -f $SVN_TREE/exec-2.sh &&
test ! -L $SVN_TREE/exec-2.sh &&
diff -u help $SVN_TREE/exec-2.sh"



name='test fetch functionality (svn => git) with alternate GIT_SVN_ID'
GIT_SVN_ID=alt
export GIT_SVN_ID
test_expect_success "$name" \
"git-svn init $svnrepo && git-svn fetch -v &&
git-rev-list --pretty=raw git-svn-HEAD | grep ^tree | uniq > a &&
git-rev-list --pretty=raw alt-HEAD | grep ^tree | uniq > b &&
diff -u a b"

test_done

321
git-annotate.perl

@ -0,0 +1,321 @@ @@ -0,0 +1,321 @@
#!/usr/bin/perl
# Copyright 2006, Ryan Anderson <ryan@michonline.com>
#
# GPL v2 (See COPYING)
#
# This file is licensed under the GPL v2, or a later version
# at the discretion of Linus Torvalds.

use warnings;
use strict;

my $filename = shift @ARGV;


my @stack = (
{
'rev' => "HEAD",
'filename' => $filename,
},
);

our (@lineoffsets, @pendinglineoffsets);
our @filelines = ();
open(F,"<",$filename)
or die "Failed to open filename: $!";

while(<F>) {
chomp;
push @filelines, $_;
}
close(F);
our $leftover_lines = @filelines;
our %revs;
our @revqueue;
our $head;

my $revsprocessed = 0;
while (my $bound = pop @stack) {
my @revisions = git_rev_list($bound->{'rev'}, $bound->{'filename'});
foreach my $revinst (@revisions) {
my ($rev, @parents) = @$revinst;
$head ||= $rev;

$revs{$rev}{'filename'} = $bound->{'filename'};
if (scalar @parents > 0) {
$revs{$rev}{'parents'} = \@parents;
next;
}

my $newbound = find_parent_renames($rev, $bound->{'filename'});
if ( exists $newbound->{'filename'} && $newbound->{'filename'} ne $bound->{'filename'}) {
push @stack, $newbound;
$revs{$rev}{'parents'} = [$newbound->{'rev'}];
}
}
}
push @revqueue, $head;
init_claim($head);
$revs{$head}{'lineoffsets'} = {};
handle_rev();


my $i = 0;
foreach my $l (@filelines) {
my ($output, $rev, $committer, $date);
if (ref $l eq 'ARRAY') {
($output, $rev, $committer, $date) = @$l;
if (length($rev) > 8) {
$rev = substr($rev,0,8);
}
} else {
$output = $l;
($rev, $committer, $date) = ('unknown', 'unknown', 'unknown');
}

printf("(%8s %10s %10s %d)%s\n", $rev, $committer, $date, $i++, $output);
}

sub init_claim {
my ($rev) = @_;
my %revinfo = git_commit_info($rev);
for (my $i = 0; $i < @filelines; $i++) {
$filelines[$i] = [ $filelines[$i], '', '', '', 1];
# line,
# rev,
# author,
# date,
# 1 <-- belongs to the original file.
}
$revs{$rev}{'lines'} = \@filelines;
}


sub handle_rev {
my $i = 0;
while (my $rev = shift @revqueue) {

my %revinfo = git_commit_info($rev);

foreach my $p (@{$revs{$rev}{'parents'}}) {

git_diff_parse($p, $rev, %revinfo);
push @revqueue, $p;
}


if (scalar @{$revs{$rev}{parents}} == 0) {
# We must be at the initial rev here, so claim everything that is left.
for (my $i = 0; $i < @{$revs{$rev}{lines}}; $i++) {
if (ref ${$revs{$rev}{lines}}[$i] eq '' || ${$revs{$rev}{lines}}[$i][1] eq '') {
claim_line($i, $rev, $revs{$rev}{lines}, %revinfo);
}
}
}
}
}


sub git_rev_list {
my ($rev, $file) = @_;

open(P,"-|","git-rev-list","--parents","--remove-empty",$rev,"--",$file)
or die "Failed to exec git-rev-list: $!";

my @revs;
while(my $line = <P>) {
chomp $line;
my ($rev, @parents) = split /\s+/, $line;
push @revs, [ $rev, @parents ];
}
close(P);

printf("0 revs found for rev %s (%s)\n", $rev, $file) if (@revs == 0);
return @revs;
}

sub find_parent_renames {
my ($rev, $file) = @_;

open(P,"-|","git-diff-tree", "-M50", "-r","--name-status", "-z","$rev")
or die "Failed to exec git-diff: $!";

local $/ = "\0";
my %bound;
my $junk = <P>;
while (my $change = <P>) {
chomp $change;
my $filename = <P>;
chomp $filename;

if ($change =~ m/^[AMD]$/ ) {
next;
} elsif ($change =~ m/^R/ ) {
my $oldfilename = $filename;
$filename = <P>;
chomp $filename;
if ( $file eq $filename ) {
my $parent = git_find_parent($rev, $oldfilename);
@bound{'rev','filename'} = ($parent, $oldfilename);
last;
}
}
}
close(P);

return \%bound;
}


sub git_find_parent {
my ($rev, $filename) = @_;

open(REVPARENT,"-|","git-rev-list","--remove-empty", "--parents","--max-count=1","$rev","--",$filename)
or die "Failed to open git-rev-list to find a single parent: $!";

my $parentline = <REVPARENT>;
chomp $parentline;
my ($revfound,$parent) = split m/\s+/, $parentline;

close(REVPARENT);

return $parent;
}


# Get a diff between the current revision and a parent.
# Record the commit information that results.
sub git_diff_parse {
my ($parent, $rev, %revinfo) = @_;

my ($ri, $pi) = (0,0);
open(DIFF,"-|","git-diff-tree","-M","-p",$rev,$parent,"--",
$revs{$rev}{'filename'}, $revs{$parent}{'filename'})
or die "Failed to call git-diff for annotation: $!";

my $slines = $revs{$rev}{'lines'};
my @plines;

my $gotheader = 0;
my ($remstart, $remlength, $addstart, $addlength);
my ($hunk_start, $hunk_index, $hunk_adds);
while(<DIFF>) {
chomp;
if (m/^@@ -(\d+),(\d+) \+(\d+),(\d+)/) {
($remstart, $remlength, $addstart, $addlength) = ($1, $2, $3, $4);
# Adjust for 0-based arrays
$remstart--;
$addstart--;
# Reinit hunk tracking.
$hunk_start = $remstart;
$hunk_index = 0;
$gotheader = 1;

for (my $i = $ri; $i < $remstart; $i++) {
$plines[$pi++] = $slines->[$i];
$ri++;
}
next;
} elsif (!$gotheader) {
next;
}

if (m/^\+(.*)$/) {
my $line = $1;
$plines[$pi++] = [ $line, '', '', '', 0 ];
next;

} elsif (m/^-(.*)$/) {
my $line = $1;
if (get_line($slines, $ri) eq $line) {
# Found a match, claim
claim_line($ri, $rev, $slines, %revinfo);
} else {
die sprintf("Sync error: %d/%d\n|%s\n|%s\n%s => %s\n",
$ri, $hunk_start + $hunk_index,
$line,
get_line($slines, $ri),
$rev, $parent);
}
$ri++;

} else {
if (substr($_,1) ne get_line($slines,$ri) ) {
die sprintf("Line %d (%d) does not match:\n|%s\n|%s\n%s => %s\n",
$hunk_start + $hunk_index, $ri,
substr($_,1),
get_line($slines,$ri),
$rev, $parent);
}
$plines[$pi++] = $slines->[$ri++];
}
$hunk_index++;
}
close(DIFF);
for (my $i = $ri; $i < @{$slines} ; $i++) {
push @plines, $slines->[$ri++];
}

$revs{$parent}{lines} = \@plines;
return;
}

sub get_line {
my ($lines, $index) = @_;

return ref $lines->[$index] ne '' ? $lines->[$index][0] : $lines->[$index];
}

sub git_cat_file {
my ($parent, $filename) = @_;
return () unless defined $parent && defined $filename;
my $blobline = `git-ls-tree $parent $filename`;
my ($mode, $type, $blob, $tfilename) = split(/\s+/, $blobline, 4);

open(C,"-|","git-cat-file", "blob", $blob)
or die "Failed to git-cat-file blob $blob (rev $parent, file $filename): " . $!;

my @lines;
while(<C>) {
chomp;
push @lines, $_;
}
close(C);

return @lines;
}


sub claim_line {
my ($floffset, $rev, $lines, %revinfo) = @_;
my $oline = get_line($lines, $floffset);
@{$lines->[$floffset]} = ( $oline, $rev,
$revinfo{'author'}, $revinfo{'author_date'} );
#printf("Claiming line %d with rev %s: '%s'\n",
# $floffset, $rev, $oline) if 1;
}

sub git_commit_info {
my ($rev) = @_;
open(COMMIT, "-|","git-cat-file", "commit", $rev)
or die "Failed to call git-cat-file: $!";

my %info;
while(<COMMIT>) {
chomp;
last if (length $_ == 0);

if (m/^author (.*) <(.*)> (.*)$/) {
$info{'author'} = $1;
$info{'author_email'} = $2;
$info{'author_date'} = $3;
} elsif (m/^committer (.*) <(.*)> (.*)$/) {
$info{'committer'} = $1;
$info{'committer_email'} = $2;
$info{'committer_date'} = $3;
}
}
close(COMMIT);

return %info;
}
Loading…
Cancel
Save