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:
Junio C Hamano
2006-02-20 14:25:46 -08:00
7 changed files with 854 additions and 118 deletions

View File

@ -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
View File

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

32
contrib/git-svn/Makefile Normal file
View 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

View File

@ -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 $!;

View File

@ -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>.

View 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
View 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;
}