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
This commit is contained in:
1
Makefile
1
Makefile
@ -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
Normal file
4
contrib/git-svn/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
git-svn
|
||||
git-svn.xml
|
||||
git-svn.html
|
||||
git-svn.1
|
32
contrib/git-svn/Makefile
Normal file
32
contrib/git-svn/Makefile
Normal file
@ -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
|
@ -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
|
||||
$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';
|
||||
|
||||
# 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/;
|
||||
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,
|
||||
'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) {
|
||||
}
|
||||
}
|
||||
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.
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
$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 {
|
||||
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 {
|
||||
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 {
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
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 $? }
|
||||
|
||||
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 {
|
||||
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 {
|
||||
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 $!;
|
@ -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:
|
||||
# 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.
|
||||
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
Normal file
216
contrib/git-svn/t/t0000-contrib-git-svn.sh
Normal file
@ -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
Executable file
321
git-annotate.perl
Executable file
@ -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;
|
||||
}
|
Reference in New Issue
Block a user