From 2e1ded44f709ddc95c5e8a87e0d98127979115fe Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Sun, 11 Jun 2006 09:50:47 -0700 Subject: [PATCH 01/43] [PATCH] gitk: rereadrefs needs listrefs The listrefs procedure was inadvertently removed during the course of development, but there is still a user of it, so resurrect it. Signed-off-by: Junio C Hamano Signed-off-by: Paul Mackerras --- gitk | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/gitk b/gitk index 9be10a43e60..ba4644f4502 100755 --- a/gitk +++ b/gitk @@ -5196,6 +5196,24 @@ proc rereadrefs {} { } } +proc listrefs {id} { + global idtags idheads idotherrefs + + set x {} + if {[info exists idtags($id)]} { + set x $idtags($id) + } + set y {} + if {[info exists idheads($id)]} { + set y $idheads($id) + } + set z {} + if {[info exists idotherrefs($id)]} { + set z $idotherrefs($id) + } + return [list $x $y $z] +} + proc showtag {tag isnew} { global ctext tagcontents tagids linknum From c7162c1db6fef6527eafb8829d60cf26b02b0108 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Tue, 23 May 2006 18:34:24 -0700 Subject: [PATCH 02/43] git-svn: t0000: add -f flag to checkout Some changes to the latest git.git made this test croak. So we'll always just force everything when using a new branch. Signed-off-by: Eric Wong --- contrib/git-svn/t/t0000-contrib-git-svn.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contrib/git-svn/t/t0000-contrib-git-svn.sh b/contrib/git-svn/t/t0000-contrib-git-svn.sh index 8b3a0d90296..a07fbad68bd 100644 --- a/contrib/git-svn/t/t0000-contrib-git-svn.sh +++ b/contrib/git-svn/t/t0000-contrib-git-svn.sh @@ -32,7 +32,7 @@ test_expect_success \ name='try a deep --rmdir with a commit' -git checkout -b mybranch remotes/git-svn +git checkout -f -b mybranch remotes/git-svn 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 @@ -58,7 +58,7 @@ test_expect_code 1 "$name" \ name='detect node change from directory to file #1' rm -rf dir $GIT_DIR/index -git checkout -b mybranch2 remotes/git-svn +git checkout -f -b mybranch2 remotes/git-svn mv bar/zzz zzz rm -rf bar mv zzz bar @@ -73,7 +73,7 @@ test_expect_code 1 "$name" \ name='detect node change from file to directory #2' rm -f $GIT_DIR/index -git checkout -b mybranch3 remotes/git-svn +git checkout -f -b mybranch3 remotes/git-svn rm bar/zzz git-update-index --remove bar/zzz mkdir bar/zzz @@ -88,7 +88,7 @@ test_expect_code 1 "$name" \ name='detect node change from directory to file #2' rm -f $GIT_DIR/index -git checkout -b mybranch4 remotes/git-svn +git checkout -f -b mybranch4 remotes/git-svn rm -rf dir git update-index --remove -- dir/file touch dir @@ -103,7 +103,7 @@ test_expect_code 1 "$name" \ name='remove executable bit from a file' rm -f $GIT_DIR/index -git checkout -b mybranch5 remotes/git-svn +git checkout -f -b mybranch5 remotes/git-svn chmod -x exec.sh git update-index exec.sh git commit -m "$name" From 8a97e368882afbc2bdc030214339bed54ed6545c Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sun, 28 May 2006 15:23:56 -0700 Subject: [PATCH 03/43] git-svn: fix handling of filenames with embedded '@' svn has trouble parsing files with embedded '@' characters. For example, svn propget svn:keywords foo@bar.c svn: Syntax error parsing revision 'bar.c' I asked about this on #svn and the workaround suggested was to append an explicit revision specifier: svn propget svn:keywords foo@bar.c@BASE This patch appends '@BASE' to the filename in all calls to 'svn propget'. Patch originally by Seth Falcon Seth: signoff? [ew: Made to work with older svn that don't support peg revisions] Signed-off-by: Eric Wong --- contrib/git-svn/git-svn.perl | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/contrib/git-svn/git-svn.perl b/contrib/git-svn/git-svn.perl index aac877974d3..7ed11ef0a55 100755 --- a/contrib/git-svn/git-svn.perl +++ b/contrib/git-svn/git-svn.perl @@ -34,7 +34,7 @@ my $sha1_short = qr/[a-f\d]{4,40}/; my ($_revision,$_stdin,$_no_ignore_ext,$_no_stop_copy,$_help,$_rmdir,$_edit, $_find_copies_harder, $_l, $_version, $_upgrade, $_authors); my (@_branch_from, %tree_map, %users); -my $_svn_co_url_revs; +my ($_svn_co_url_revs, $_svn_pg_peg_revs); my %fc_opts = ( 'no-ignore-externals' => \$_no_ignore_ext, 'branch|b=s' => \@_branch_from, @@ -336,7 +336,7 @@ sub show_ignore { my %ign; File::Find::find({wanted=>sub{if(lstat $_ && -d _ && -d "$_/.svn"){ s#^\./##; - @{$ign{$_}} = safe_qx(qw(svn propget svn:ignore),$_); + @{$ign{$_}} = svn_propget_base('svn:ignore', $_); }}, no_chdir=>1},'.'); print "\n# /\n"; @@ -859,7 +859,7 @@ sub sys { system(@_) == 0 or croak $? } sub eol_cp { my ($from, $to) = @_; - my $es = safe_qx(qw/svn propget svn:eol-style/, $to); + my $es = svn_propget_base('svn:eol-style', $to); open my $rfd, '<', $from or croak $!; binmode $rfd or croak $!; open my $wfd, '>', $to or croak $!; @@ -897,7 +897,7 @@ sub do_update_index { while (my $x = <$p>) { chomp $x; if (!$no_text_base && lstat $x && ! -l _ && - safe_qx(qw/svn propget svn:keywords/,$x)) { + svn_propget_base('svn:keywords', $x)) { my $mode = -x _ ? 0755 : 0644; my ($v,$d,$f) = File::Spec->splitpath($x); my $tb = File::Spec->catfile($d, '.svn', 'tmp', @@ -1135,6 +1135,9 @@ sub svn_compat_check { if (grep /usage: checkout URL\[\@REV\]/,@co_help) { $_svn_co_url_revs = 1; } + if (grep /\[TARGET\[\@REV\]\.\.\.\]/, `svn propget -h`) { + $_svn_pg_peg_revs = 1; + } # I really, really hope nobody hits this... unless (grep /stop-on-copy/, (safe_qx(qw(svn log -h)))) { @@ -1214,6 +1217,12 @@ sub load_authors { close $authors or croak $!; } +sub svn_propget_base { + my ($p, $f) = @_; + $f .= '@BASE' if $_svn_pg_peg_revs; + return safe_qx(qw/svn propget/, $p, $f); +} + __END__ Data structures: From 4a393f2b53f0997f79d47793d4c774fa0072887c Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Fri, 9 Jun 2006 23:27:01 -0700 Subject: [PATCH 04/43] git-svn: eol_cp corner-case fixes If we read the maximum size of our buffer into $buf, and the last character is '\015', there's a chance that the character is '\012', which means our regex won't work correctly. At the worst case, this could introduce an extra newline into the code. We'll now read an extra character if we see '\015' is the last character in $buf. We also forgot to recalculate the length of $buf after doing the newline substitution, causing some files to appeare truncated. We'll do that now and force byte semantics in length() for good measure. Signed-off-by: Eric Wong --- contrib/git-svn/git-svn.perl | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/contrib/git-svn/git-svn.perl b/contrib/git-svn/git-svn.perl index 7ed11ef0a55..8d2e7f74ea1 100755 --- a/contrib/git-svn/git-svn.perl +++ b/contrib/git-svn/git-svn.perl @@ -866,19 +866,26 @@ sub eol_cp { binmode $wfd or croak $!; my $eol = $EOL{$es} or undef; - if ($eol) { - print "$eol: $from => $to\n"; - } my $buf; + use bytes; while (1) { my ($r, $w, $t); defined($r = sysread($rfd, $buf, 4096)) or croak $!; return unless $r; - $buf =~ s/(?:\015|\012|\015\012)/$eol/gs if $eol; + if ($eol) { + if ($buf =~ /\015$/) { + my $c; + defined($r = sysread($rfd,$c,1)) or croak $!; + $buf .= $c if $r > 0; + } + $buf =~ s/(?:\015\012|\015|\012)/$eol/gs; + $r = length($buf); + } for ($w = 0; $w < $r; $w += $t) { $t = syswrite($wfd, $buf, $r - $w, $w) or croak $!; } } + no bytes; } sub do_update_index { From ce475dfcb5021339d8545a020e8697dd35a1bbea Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Fri, 2 Jun 2006 15:16:41 -0700 Subject: [PATCH 05/43] git-svn: restore original LC_ALL setting (or unset) for commit svn forces UTF-8 for commit messages, and with LC_ALL set to 'C' it is unable to determine encoding of the git commit message. Now we'll just assume the user has set LC_* correctly for the commit message they're using. Signed-off-by: Eric Wong --- contrib/git-svn/git-svn.perl | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/contrib/git-svn/git-svn.perl b/contrib/git-svn/git-svn.perl index 8d2e7f74ea1..8bc3d69fdb0 100755 --- a/contrib/git-svn/git-svn.perl +++ b/contrib/git-svn/git-svn.perl @@ -14,6 +14,7 @@ use Cwd qw/abs_path/; $GIT_DIR = abs_path($ENV{GIT_DIR} || '.git'); $ENV{GIT_DIR} = $GIT_DIR; +my $LC_ALL = $ENV{LC_ALL}; # make sure the svn binary gives consistent output between locales and TZs: $ENV{TZ} = 'UTC'; $ENV{LC_ALL} = 'C'; @@ -704,23 +705,34 @@ sub svn_commit_tree { my ($oneline) = ($log_msg{msg} =~ /([^\n\r]+)/); print "Committing $commit: $oneline\n"; + if (defined $LC_ALL) { + $ENV{LC_ALL} = $LC_ALL; + } else { + delete $ENV{LC_ALL}; + } my @ci_output = safe_qx(qw(svn commit -F),$commit_msg); - my ($committed) = grep(/^Committed revision \d+\./,@ci_output); + $ENV{LC_ALL} = 'C'; unlink $commit_msg; - defined $committed or croak + my ($committed) = ($ci_output[$#ci_output] =~ /(\d+)/); + if (!defined $committed) { + my $out = join("\n",@ci_output); + print STDERR "W: Trouble parsing \`svn commit' output:\n\n", + $out, "\n\nAssuming English locale..."; + ($committed) = ($out =~ /^Committed revision \d+\./sm); + defined $committed or die " FAILED!\n", "Commit output failed to parse committed revision!\n", - join("\n",@ci_output),"\n"; - my ($rev_committed) = ($committed =~ /^Committed revision (\d+)\./); + print STDERR " OK\n"; + } my @svn_up = qw(svn up); push @svn_up, '--ignore-externals' unless $_no_ignore_ext; - if ($rev_committed == ($svn_rev + 1)) { - push @svn_up, "-r$rev_committed"; + if ($committed == ($svn_rev + 1)) { + push @svn_up, "-r$committed"; sys(@svn_up); my $info = svn_info('.'); my $date = $info->{'Last Changed Date'} or die "Missing date\n"; - if ($info->{'Last Changed Rev'} != $rev_committed) { - croak "$info->{'Last Changed Rev'} != $rev_committed\n" + if ($info->{'Last Changed Rev'} != $committed) { + croak "$info->{'Last Changed Rev'} != $committed\n" } my ($Y,$m,$d,$H,$M,$S,$tz) = ($date =~ /(\d{4})\-(\d\d)\-(\d\d)\s @@ -728,16 +740,16 @@ sub svn_commit_tree { or croak "Failed to parse date: $date\n"; $log_msg{date} = "$tz $Y-$m-$d $H:$M:$S"; $log_msg{author} = $info->{'Last Changed Author'}; - $log_msg{revision} = $rev_committed; + $log_msg{revision} = $committed; $log_msg{msg} .= "\n"; my $parent = file_to_s("$REV_DIR/$svn_rev"); git_commit(\%log_msg, $parent, $commit); - return $rev_committed; + return $committed; } # resync immediately push @svn_up, "-r$svn_rev"; sys(@svn_up); - return fetch("$rev_committed=$commit")->{revision}; + return fetch("$committed=$commit")->{revision}; } # read the entire log into a temporary file (which is removed ASAP) From b63af9b340dd831840c70103c9f609940a910031 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sat, 3 Jun 2006 02:56:33 -0700 Subject: [PATCH 06/43] git-svn: don't allow commit if svn tree is not current If new revisions are fetched, that implies we haven't merged, acked, or nacked them yet, and attempting to write the tree we're committing means we'd silently clobber the newly fetched changes. Signed-off-by: Eric Wong --- contrib/git-svn/git-svn.perl | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/contrib/git-svn/git-svn.perl b/contrib/git-svn/git-svn.perl index 8bc3d69fdb0..72129de6a3a 100755 --- a/contrib/git-svn/git-svn.perl +++ b/contrib/git-svn/git-svn.perl @@ -309,9 +309,16 @@ sub commit { } chomp @revs; - fetch(); - chdir $SVN_WC or croak $!; + chdir $SVN_WC or croak "Unable to chdir $SVN_WC: $!\n"; my $info = svn_info('.'); + my $fetched = fetch(); + if ($info->{Revision} != $fetched->{revision}) { + print STDERR "There are new revisions that were fetched ", + "and need to be merged (or acknowledged) ", + "before committing.\n"; + exit 1; + } + $info = svn_info('.'); read_uuid($info); my $svn_current_rev = $info->{'Last Changed Rev'}; foreach my $c (@revs) { From 162f41292167a800432fc6bbacfcd9f93a90b0c8 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sun, 14 May 2006 20:00:00 -0700 Subject: [PATCH 07/43] git-svn: support -C passing to git-diff-tree The repo-config key is 'svn.copysimilarity' Signed-off-by: Eric Wong --- contrib/git-svn/git-svn.perl | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/contrib/git-svn/git-svn.perl b/contrib/git-svn/git-svn.perl index 72129de6a3a..089d597d25d 100755 --- a/contrib/git-svn/git-svn.perl +++ b/contrib/git-svn/git-svn.perl @@ -33,7 +33,8 @@ use POSIX qw/strftime/; my $sha1 = qr/[a-f\d]{40}/; my $sha1_short = qr/[a-f\d]{4,40}/; my ($_revision,$_stdin,$_no_ignore_ext,$_no_stop_copy,$_help,$_rmdir,$_edit, - $_find_copies_harder, $_l, $_version, $_upgrade, $_authors); + $_find_copies_harder, $_l, $_cp_similarity, + $_version, $_upgrade, $_authors); my (@_branch_from, %tree_map, %users); my ($_svn_co_url_revs, $_svn_pg_peg_revs); @@ -55,6 +56,7 @@ my %cmd = ( 'rmdir' => \$_rmdir, 'find-copies-harder' => \$_find_copies_harder, 'l=i' => \$_l, + 'copy-similarity|C=i'=> \$_cp_similarity, %fc_opts, } ], 'show-ignore' => [ \&show_ignore, "Show svn:ignore listings", { } ], @@ -580,7 +582,12 @@ sub svn_checkout_tree { my $pid = open my $diff_fh, '-|'; defined $pid or croak $!; if ($pid == 0) { - my @diff_tree = qw(git-diff-tree -z -r -C); + my @diff_tree = qw(git-diff-tree -z -r); + if ($_cp_similarity) { + push @diff_tree, "-C$_cp_similarity"; + } else { + push @diff_tree, '-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 $!; From bf78b1d89b29f8524ccfbd7042fa277277e316ff Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Fri, 28 Apr 2006 03:42:38 -0700 Subject: [PATCH 08/43] git-svn: --branch-all-refs / -B support This should make life easier for all those who type: `git-rev-parse --symbolic --all | xargs -n1 echo -b` every time they run git-svn fetch. Signed-off-by: Eric Wong --- contrib/git-svn/git-svn.perl | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/contrib/git-svn/git-svn.perl b/contrib/git-svn/git-svn.perl index 089d597d25d..c91160d37f0 100755 --- a/contrib/git-svn/git-svn.perl +++ b/contrib/git-svn/git-svn.perl @@ -34,12 +34,13 @@ my $sha1 = qr/[a-f\d]{40}/; my $sha1_short = qr/[a-f\d]{4,40}/; my ($_revision,$_stdin,$_no_ignore_ext,$_no_stop_copy,$_help,$_rmdir,$_edit, $_find_copies_harder, $_l, $_cp_similarity, - $_version, $_upgrade, $_authors); + $_version, $_upgrade, $_authors, $_branch_all_refs); my (@_branch_from, %tree_map, %users); my ($_svn_co_url_revs, $_svn_pg_peg_revs); my %fc_opts = ( 'no-ignore-externals' => \$_no_ignore_ext, 'branch|b=s' => \@_branch_from, + 'branch-all-refs|B' => \$_branch_all_refs, 'authors-file|A=s' => \$_authors ); # yes, 'native' sets "\n". Patches to fix this for non-*nix systems welcome: @@ -108,6 +109,7 @@ usage(0) if $_help; version() if $_version; usage(1) unless defined $cmd; load_authors() if $_authors; +load_all_refs() if $_branch_all_refs; svn_compat_check(); $cmd{$cmd}->[0]->(@ARGV); exit 0; @@ -1238,6 +1240,17 @@ sub map_tree_joins { } } +sub load_all_refs { + if (@_branch_from) { + print STDERR '--branch|-b parameters are ignored when ', + "--branch-all-refs|-B is passed\n"; + } + + # don't worry about rev-list on non-commit objects/tags, + # it shouldn't blow up if a ref is a blob or tree... + chomp(@_branch_from = `git-rev-parse --symbolic --all`); +} + # ' = real-name ' mapping based on git-svnimport: sub load_authors { open my $authors, '<', $_authors or die "Can't open $_authors $!\n"; From 098749d9bee6694abc8a0991996ff94b607abc7f Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Fri, 28 Apr 2006 03:51:16 -0700 Subject: [PATCH 09/43] git-svn: optimize --branch and --branch-all-ref By breaking the pipe read once we've seen a commit twice. This should make -B/--branch-all-ref faster and usable on a frequent basis. We use topological order now for calling git-rev-list, and any commit we've seen before should imply that all parents have been seen (at least I hope that's the case for --topo-order). Signed-off-by: Eric Wong --- contrib/git-svn/git-svn.perl | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/contrib/git-svn/git-svn.perl b/contrib/git-svn/git-svn.perl index c91160d37f0..d4b9323694e 100755 --- a/contrib/git-svn/git-svn.perl +++ b/contrib/git-svn/git-svn.perl @@ -1220,23 +1220,30 @@ sub check_upgrade_needed { # fills %tree_map with a reverse mapping of trees to commits. Useful # for finding parents to commit on. sub map_tree_joins { + my %seen; foreach my $br (@_branch_from) { my $pid = open my $pipe, '-|'; defined $pid or croak $!; if ($pid == 0) { - exec(qw(git-rev-list --pretty=raw), $br) or croak $?; + exec(qw(git-rev-list --topo-order --pretty=raw), $br) + or croak $?; } while (<$pipe>) { if (/^commit ($sha1)$/o) { my $commit = $1; + + # if we've seen a commit, + # we've seen its parents + last if $seen{$commit}; my ($tree) = (<$pipe> =~ /^tree ($sha1)$/o); unless (defined $tree) { die "Failed to parse commit $commit\n"; } push @{$tree_map{$tree}}, $commit; + $seen{$commit} = 1; } } - close $pipe or croak $?; + close $pipe; # we could be breaking the pipe early } } From 6dfbe5163e26e3e1126c9b08c3cb38195e92a82c Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Wed, 3 May 2006 22:54:00 -0700 Subject: [PATCH 10/43] git-svn: support manually placed initial trees from fetch Sometimes I don't feel like downloading an entire tree again when I actually decide a branch is worth tracking, so some users can get around it more easily with this. Signed-off-by: Eric Wong --- contrib/git-svn/git-svn.perl | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/contrib/git-svn/git-svn.perl b/contrib/git-svn/git-svn.perl index d4b9323694e..54f3d6312ea 100755 --- a/contrib/git-svn/git-svn.perl +++ b/contrib/git-svn/git-svn.perl @@ -262,7 +262,14 @@ sub fetch { } else { chdir $SVN_WC or croak $!; read_uuid(); - $last_commit = file_to_s("$REV_DIR/$base->{revision}"); + eval { $last_commit = file_to_s("$REV_DIR/$base->{revision}") }; + # looks like a user manually cp'd and svn switch'ed + unless ($last_commit) { + sys(qw/svn revert -R ./); + assert_svn_wc_clean($base->{revision}); + $last_commit = git_commit($base, @parents); + assert_tree($last_commit); + } } my @svn_up = qw(svn up); push @svn_up, '--ignore-externals' unless $_no_ignore_ext; From 883d0a78d2a28add079f809ffb8dd077f4a766d8 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Wed, 24 May 2006 01:22:07 -0700 Subject: [PATCH 11/43] git-svn: Move all git-svn-related paths into $GIT_DIR/svn Since GIT_SVN_ID usage is probably going to become more widespread , we won't run the chance of somebody having a GIT_SVN_ID name that conflicts with one of the default directories that already exist in $GIT_DIR (branches/tags). Signed-off-by: Eric Wong --- contrib/git-svn/git-svn.perl | 99 +++++++++++++++++++++++++++----- contrib/git-svn/t/lib-git-svn.sh | 2 +- 2 files changed, 85 insertions(+), 16 deletions(-) diff --git a/contrib/git-svn/git-svn.perl b/contrib/git-svn/git-svn.perl index 54f3d6312ea..2dce4e7b83e 100755 --- a/contrib/git-svn/git-svn.perl +++ b/contrib/git-svn/git-svn.perl @@ -6,7 +6,7 @@ use strict; use vars qw/ $AUTHOR $VERSION $SVN_URL $SVN_INFO $SVN_WC $SVN_UUID $GIT_SVN_INDEX $GIT_SVN - $GIT_DIR $REV_DIR/; + $GIT_DIR $REV_DIR $GIT_SVN_DIR/; $AUTHOR = 'Eric Wong '; $VERSION = '1.1.0-pre'; @@ -37,6 +37,7 @@ my ($_revision,$_stdin,$_no_ignore_ext,$_no_stop_copy,$_help,$_rmdir,$_edit, $_version, $_upgrade, $_authors, $_branch_all_refs); my (@_branch_from, %tree_map, %users); my ($_svn_co_url_revs, $_svn_pg_peg_revs); +my @repo_path_split_cache; my %fc_opts = ( 'no-ignore-externals' => \$_no_ignore_ext, 'branch|b=s' => \@_branch_from, @@ -100,10 +101,11 @@ GetOptions(%opts, 'help|H|h' => \$_help, 'id|i=s' => \$GIT_SVN) or exit 1; $GIT_SVN ||= $ENV{GIT_SVN_ID} || 'git-svn'; -$GIT_SVN_INDEX = "$GIT_DIR/$GIT_SVN/index"; +$GIT_SVN_DIR = "$GIT_DIR/svn/$GIT_SVN"; +$GIT_SVN_INDEX = "$GIT_SVN_DIR/index"; $SVN_URL = undef; -$REV_DIR = "$GIT_DIR/$GIT_SVN/revs"; -$SVN_WC = "$GIT_DIR/$GIT_SVN/tree"; +$REV_DIR = "$GIT_SVN_DIR/revs"; +$SVN_WC = "$GIT_SVN_DIR/tree"; usage(0) if $_help; version() if $_version; @@ -111,6 +113,7 @@ usage(1) unless defined $cmd; load_authors() if $_authors; load_all_refs() if $_branch_all_refs; svn_compat_check(); +migration_check() unless $cmd eq 'init'; $cmd{$cmd}->[0]->(@ARGV); exit 0; @@ -200,7 +203,7 @@ sub rebuild { $latest = $rev; } assert_revision_eq_or_unknown($rev, $c); - sys('git-update-ref',"$GIT_SVN/revs/$rev",$c); + sys('git-update-ref',"svn/$GIT_SVN/revs/$rev",$c); $newest_rev = $rev if ($rev > $newest_rev); } close $rev_list or croak $?; @@ -241,7 +244,7 @@ sub init { sub fetch { my (@parents) = @_; check_upgrade_needed(); - $SVN_URL ||= file_to_s("$GIT_DIR/$GIT_SVN/info/url"); + $SVN_URL ||= file_to_s("$GIT_SVN_DIR/info/url"); my @log_args = -d $SVN_WC ? ($SVN_WC) : ($SVN_URL); unless ($_revision) { $_revision = -d $SVN_WC ? 'BASE:HEAD' : '0:HEAD'; @@ -350,7 +353,7 @@ sub show_ignore { chomp(my @excludes = (<$fh>)); close $fh or croak $!; - $SVN_URL ||= file_to_s("$GIT_DIR/$GIT_SVN/info/url"); + $SVN_URL ||= file_to_s("$GIT_SVN_DIR/info/url"); chdir $SVN_WC or croak $!; my %ign; File::Find::find({wanted=>sub{if(lstat $_ && -d _ && -d "$_/.svn"){ @@ -374,7 +377,44 @@ sub read_uuid { my $info = shift || svn_info('.'); $SVN_UUID = $info->{'Repository UUID'} or croak "Repository UUID unreadable\n"; - s_to_file($SVN_UUID,"$GIT_DIR/$GIT_SVN/info/uuid"); + s_to_file($SVN_UUID,"$GIT_SVN_DIR/info/uuid"); +} + +sub quiet_run { + my $pid = fork; + defined $pid or croak $!; + if (!$pid) { + open my $null, '>', '/dev/null' or croak $!; + open STDERR, '>&', $null or croak $!; + open STDOUT, '>&', $null or croak $!; + exec @_ or croak $!; + } + waitpid $pid, 0; + return $?; +} + +sub repo_path_split { + my $full_url = shift; + $full_url =~ s#/+$##; + + foreach (@repo_path_split_cache) { + if ($full_url =~ s#$_##) { + my $u = $1; + $full_url =~ s#^/+##; + return ($u, $full_url); + } + } + + my ($url, $path) = ($full_url =~ m!^([a-z\+]+://[^/]*)(.*)$!i); + $path =~ s#^/+##; + my @paths = split(m#/+#, $path); + + while (quiet_run(qw/svn ls --non-interactive/, $url)) { + my $n = shift @paths || last; + $url .= "/$n"; + } + push @repo_path_split_cache, qr/^(\Q$url\E)/; + return ($url, $path); } sub setup_git_svn { @@ -382,14 +422,17 @@ sub setup_git_svn { unless (-d $GIT_DIR) { croak "GIT_DIR=$GIT_DIR does not exist!\n"; } - mkpath(["$GIT_DIR/$GIT_SVN"]); - mkpath(["$GIT_DIR/$GIT_SVN/info"]); + mkpath([$GIT_SVN_DIR]); + mkpath(["$GIT_SVN_DIR/info"]); mkpath([$REV_DIR]); - s_to_file($SVN_URL,"$GIT_DIR/$GIT_SVN/info/url"); + s_to_file($SVN_URL,"$GIT_SVN_DIR/info/url"); - open my $fd, '>>', "$GIT_DIR/$GIT_SVN/info/exclude" or croak $!; + open my $fd, '>>', "$GIT_SVN_DIR/info/exclude" or croak $!; print $fd '.svn',"\n"; close $fd or croak $!; + my ($url, $path) = repo_path_split($SVN_URL); + s_to_file($url, "$GIT_SVN_DIR/info/repo_url"); + s_to_file($path, "$GIT_SVN_DIR/info/repo_path"); } sub assert_svn_wc_clean { @@ -688,7 +731,7 @@ sub handle_rmdir { sub svn_commit_tree { my ($svn_rev, $commit) = @_; - my $commit_msg = "$GIT_DIR/$GIT_SVN/.svn-commit.tmp.$$"; + my $commit_msg = "$GIT_SVN_DIR/.svn-commit.tmp.$$"; my %log_msg = ( msg => '' ); open my $msg, '>', $commit_msg or croak $!; @@ -965,7 +1008,7 @@ sub index_changes { 'remove', $no_text_base); do_update_index([qw/git-ls-files -z --others/, - "--exclude-from=$GIT_DIR/$GIT_SVN/info/exclude"], + "--exclude-from=$GIT_SVN_DIR/info/exclude"], 'add', $no_text_base); } @@ -1097,7 +1140,7 @@ sub git_commit { push @update_ref, $primary_parent unless $?; } sys(@update_ref); - sys('git-update-ref',"$GIT_SVN/revs/$log_msg->{revision}",$commit); + sys('git-update-ref',"svn/$GIT_SVN/revs/$log_msg->{revision}",$commit); print "r$log_msg->{revision} = $commit\n"; return $commit; } @@ -1283,6 +1326,32 @@ sub svn_propget_base { return safe_qx(qw/svn propget/, $p, $f); } +sub migration_check { + return if (-d "$GIT_DIR/svn" || !-d $GIT_DIR); + print "Upgrading repository...\n"; + unless (-d "$GIT_DIR/svn") { + mkdir "$GIT_DIR/svn" or croak $!; + } + print "Data from a previous version of git-svn exists, but\n\t", + "$GIT_SVN_DIR\n\t(required for this version ", + "($VERSION) of git-svn) does not.\n"; + + foreach my $x (`git-rev-parse --symbolic --all`) { + next unless $x =~ s#^refs/remotes/##; + chomp $x; + next unless -f "$GIT_DIR/$x/info/url"; + my $u = eval { file_to_s("$GIT_DIR/$x/info/url") }; + next unless $u; + my $dn = dirname("$GIT_DIR/svn/$x"); + mkpath([$dn]) unless -d $dn; + rename "$GIT_DIR/$x", "$GIT_DIR/svn/$x" or croak "$!: $x"; + my ($url, $path) = repo_path_split($u); + s_to_file($url, "$GIT_DIR/svn/$x/info/repo_url"); + s_to_file($path, "$GIT_DIR/svn/$x/info/repo_path"); + } + print "Done upgrading.\n"; +} + __END__ Data structures: diff --git a/contrib/git-svn/t/lib-git-svn.sh b/contrib/git-svn/t/lib-git-svn.sh index a98e9d164dd..58408a6c07e 100644 --- a/contrib/git-svn/t/lib-git-svn.sh +++ b/contrib/git-svn/t/lib-git-svn.sh @@ -10,7 +10,7 @@ fi . ./test-lib.sh GIT_DIR=$PWD/.git -GIT_SVN_DIR=$GIT_DIR/git-svn +GIT_SVN_DIR=$GIT_DIR/svn/git-svn SVN_TREE=$GIT_SVN_DIR/tree svnadmin >/dev/null 2>&1 From b8c92caddac61e556254bf93c3a4bd744de94320 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Wed, 24 May 2006 01:40:37 -0700 Subject: [PATCH 12/43] git-svn: minor cleanups, extra error-checking While we're at it, read_repo_config has been added and expanded to handle case where command-line arguments are optional to Getopt::Long Signed-off-by: Eric Wong --- contrib/git-svn/git-svn.perl | 82 ++++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 36 deletions(-) diff --git a/contrib/git-svn/git-svn.perl b/contrib/git-svn/git-svn.perl index 2dce4e7b83e..a24306072e3 100755 --- a/contrib/git-svn/git-svn.perl +++ b/contrib/git-svn/git-svn.perl @@ -77,39 +77,15 @@ for (my $i = 0; $i < @ARGV; $i++) { my %opts = %{$cmd{$cmd}->[2]} if (defined $cmd); -# convert GetOpt::Long specs for use by git-repo-config -foreach my $o (keys %opts) { - my $v = $opts{$o}; - my ($key) = ($o =~ /^([a-z\-]+)/); - $key =~ s/-//g; - my $arg = 'git-repo-config'; - $arg .= ' --int' if ($o =~ /=i$/); - $arg .= ' --bool' if ($o !~ /=[sfi]$/); - if (ref $v eq 'ARRAY') { - chomp(my @tmp = `$arg --get-all svn.$key`); - @$v = @tmp if @tmp; - } else { - chomp(my $tmp = `$arg --get svn.$key`); - if ($tmp && !($arg =~ / --bool / && $tmp eq 'false')) { - $$v = $tmp; - } - } -} - +read_repo_config(\%opts); GetOptions(%opts, 'help|H|h' => \$_help, 'version|V' => \$_version, 'id|i=s' => \$GIT_SVN) or exit 1; -$GIT_SVN ||= $ENV{GIT_SVN_ID} || 'git-svn'; -$GIT_SVN_DIR = "$GIT_DIR/svn/$GIT_SVN"; -$GIT_SVN_INDEX = "$GIT_SVN_DIR/index"; -$SVN_URL = undef; -$REV_DIR = "$GIT_SVN_DIR/revs"; -$SVN_WC = "$GIT_SVN_DIR/tree"; - usage(0) if $_help; version() if $_version; usage(1) unless defined $cmd; +init_vars(); load_authors() if $_authors; load_all_refs() if $_branch_all_refs; svn_compat_check(); @@ -132,7 +108,7 @@ Usage: $0 [options] [arguments]\n print $fd ' ',pack('A13',$_),$cmd{$_}->[1],"\n"; foreach (keys %{$cmd{$_}->[2]}) { # prints out arguments as they should be passed: - my $x = s#=s$## ? '' : s#=i$## ? '' : ''; + my $x = s#[:=]s$## ? '' : s#[:=]i$## ? '' : ''; print $fd ' ' x 17, join(', ', map { length $_ > 1 ? "--$_" : "-$_" } split /\|/,$_)," $x\n"; @@ -220,9 +196,10 @@ sub rebuild { sys(@svn_up,"-r$newest_rev"); $ENV{GIT_INDEX_FILE} = $GIT_SVN_INDEX; index_changes(); - exec('git-write-tree'); + exec('git-write-tree') or croak $!; } waitpid $pid, 0; + croak $? if $?; if ($_upgrade) { print STDERR <<""; @@ -295,6 +272,7 @@ sub fetch { unless (-e "$GIT_DIR/refs/heads/master") { sys(qw(git-update-ref refs/heads/master),$last_commit); } + close $svn_log->{fh}; return $last; } @@ -830,7 +808,7 @@ sub svn_log_raw { exec (qw(svn log), @log_args) or croak $! } waitpid $pid, 0; - croak if $?; + croak $? if $?; seek $log_fh, 0, 0 or croak $!; return { state => 'sep', fh => $log_fh }; } @@ -1090,7 +1068,7 @@ sub git_commit { $ENV{GIT_INDEX_FILE} = $GIT_SVN_INDEX; index_changes(); chomp(my $tree = `git-write-tree`); - croak if $?; + croak $? if $?; if (exists $tree_map{$tree}) { my %seen_parent = map { $_ => 1 } @exec_parents; foreach (@{$tree_map{$tree}}) { @@ -1118,7 +1096,7 @@ sub git_commit { exec @exec or croak $!; } waitpid($pid,0); - croak if $?; + croak $? if $?; $out_fh->flush == 0 or croak $!; seek $out_fh, 0, 0 or croak $!; @@ -1134,7 +1112,7 @@ sub git_commit { close STDERR; close STDOUT; exec 'git-rev-parse','--verify', - "refs/remotes/$GIT_SVN^0"; + "refs/remotes/$GIT_SVN^0" or croak $!; } waitpid $pid, 0; push @update_ref, $primary_parent unless $?; @@ -1190,7 +1168,7 @@ sub blob_to_file { if ($pid == 0) { open STDOUT, '>&', $blob_fh or croak $!; - exec('git-cat-file','blob',$blob); + exec('git-cat-file','blob',$blob) or croak $!; } waitpid $pid, 0; croak $? if $?; @@ -1202,7 +1180,7 @@ sub safe_qx { my $pid = open my $child, '-|'; defined $pid or croak $!; if ($pid == 0) { - exec(@_) or croak $?; + exec(@_) or croak $!; } my @ret = (<$child>); close $child or croak $?; @@ -1252,7 +1230,7 @@ sub check_upgrade_needed { defined $pid or croak $!; if ($pid == 0) { close STDERR; - exec('git-rev-parse',"$GIT_SVN-HEAD") or croak $?; + exec('git-rev-parse',"$GIT_SVN-HEAD") or croak $!; } my @ret = (<$child>); close $child or croak $?; @@ -1276,7 +1254,7 @@ sub map_tree_joins { defined $pid or croak $!; if ($pid == 0) { exec(qw(git-rev-list --topo-order --pretty=raw), $br) - or croak $?; + or croak $!; } while (<$pipe>) { if (/^commit ($sha1)$/o) { @@ -1352,6 +1330,38 @@ sub migration_check { print "Done upgrading.\n"; } +sub init_vars { + $GIT_SVN ||= $ENV{GIT_SVN_ID} || 'git-svn'; + $GIT_SVN_DIR = "$GIT_DIR/svn/$GIT_SVN"; + $GIT_SVN_INDEX = "$GIT_SVN_DIR/index"; + $SVN_URL = undef; + $REV_DIR = "$GIT_SVN_DIR/revs"; + $SVN_WC = "$GIT_SVN_DIR/tree"; +} + +# convert GetOpt::Long specs for use by git-repo-config +sub read_repo_config { + return unless -d $GIT_DIR; + my $opts = shift; + foreach my $o (keys %$opts) { + my $v = $opts->{$o}; + my ($key) = ($o =~ /^([a-z\-]+)/); + $key =~ s/-//g; + my $arg = 'git-repo-config'; + $arg .= ' --int' if ($o =~ /[:=]i$/); + $arg .= ' --bool' if ($o !~ /[:=][sfi]$/); + if (ref $v eq 'ARRAY') { + chomp(my @tmp = `$arg --get-all svn.$key`); + @$v = @tmp if @tmp; + } else { + chomp(my $tmp = `$arg --get svn.$key`); + if ($tmp && !($arg =~ / --bool / && $tmp eq 'false')) { + $$v = $tmp; + } + } + } +} + __END__ Data structures: From dc5869c00d9aafbddcc11b93b5a0a7fcdeb755ea Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Wed, 24 May 2006 02:07:32 -0700 Subject: [PATCH 13/43] git-svn: add --repack and --repack-flags= options This should help keep disk usage sane for large imports. --repack takes an optional argument for the interval, it defaults to 1000 if no argument is specified. Arguments to --repack-flags are passed directly to git-repack. No arguments are passed by default. Idea stolen from git-cvsimport :) Signed-off-by: Eric Wong --- contrib/git-svn/git-svn.perl | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/contrib/git-svn/git-svn.perl b/contrib/git-svn/git-svn.perl index a24306072e3..a04cf1d354f 100755 --- a/contrib/git-svn/git-svn.perl +++ b/contrib/git-svn/git-svn.perl @@ -34,6 +34,7 @@ my $sha1 = qr/[a-f\d]{40}/; my $sha1_short = qr/[a-f\d]{4,40}/; my ($_revision,$_stdin,$_no_ignore_ext,$_no_stop_copy,$_help,$_rmdir,$_edit, $_find_copies_harder, $_l, $_cp_similarity, + $_repack, $_repack_nr, $_repack_flags, $_version, $_upgrade, $_authors, $_branch_all_refs); my (@_branch_from, %tree_map, %users); my ($_svn_co_url_revs, $_svn_pg_peg_revs); @@ -42,7 +43,9 @@ my @repo_path_split_cache; my %fc_opts = ( 'no-ignore-externals' => \$_no_ignore_ext, 'branch|b=s' => \@_branch_from, 'branch-all-refs|B' => \$_branch_all_refs, - 'authors-file|A=s' => \$_authors ); + 'authors-file|A=s' => \$_authors, + 'repack:i' => \$_repack, + 'repack-flags|repack-args|repack-opts=s' => \$_repack_flags); # yes, 'native' sets "\n". Patches to fix this for non-*nix systems welcome: my %EOL = ( CR => "\015", LF => "\012", CRLF => "\015\012", native => "\012" ); @@ -82,6 +85,7 @@ GetOptions(%opts, 'help|H|h' => \$_help, 'version|V' => \$_version, 'id|i=s' => \$GIT_SVN) or exit 1; +set_default_vals(); usage(0) if $_help; version() if $_version; usage(1) unless defined $cmd; @@ -1120,6 +1124,10 @@ sub git_commit { sys(@update_ref); sys('git-update-ref',"svn/$GIT_SVN/revs/$log_msg->{revision}",$commit); print "r$log_msg->{revision} = $commit\n"; + if ($_repack && (--$_repack_nr == 0)) { + $_repack_nr = $_repack; + sys("git repack $_repack_flags"); + } return $commit; } @@ -1362,6 +1370,14 @@ sub read_repo_config { } } +sub set_default_vals { + if (defined $_repack) { + $_repack = 1000 if ($_repack <= 0); + $_repack_nr = $_repack; + $_repack_flags ||= ''; + } +} + __END__ Data structures: From f8ab6b732f0a2cead5089aea2ce0c3c3aa97cafe Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Wed, 31 May 2006 15:49:56 -0700 Subject: [PATCH 14/43] git-svn: add --shared and --template= options to pass to init-db Signed-off-by: Eric Wong --- contrib/git-svn/git-svn.perl | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/contrib/git-svn/git-svn.perl b/contrib/git-svn/git-svn.perl index a04cf1d354f..d8f103ed9ad 100755 --- a/contrib/git-svn/git-svn.perl +++ b/contrib/git-svn/git-svn.perl @@ -35,6 +35,7 @@ my $sha1_short = qr/[a-f\d]{4,40}/; my ($_revision,$_stdin,$_no_ignore_ext,$_no_stop_copy,$_help,$_rmdir,$_edit, $_find_copies_harder, $_l, $_cp_similarity, $_repack, $_repack_nr, $_repack_flags, + $_template, $_shared, $_version, $_upgrade, $_authors, $_branch_all_refs); my (@_branch_from, %tree_map, %users); my ($_svn_co_url_revs, $_svn_pg_peg_revs); @@ -54,7 +55,9 @@ my %cmd = ( fetch => [ \&fetch, "Download new revisions from SVN", { 'revision|r=s' => \$_revision, %fc_opts } ], init => [ \&init, "Initialize a repo for tracking" . - " (requires URL argument)", { } ], + " (requires URL argument)", + { 'template=s' => \$_template, + 'shared' => \$_shared } ], commit => [ \&commit, "Commit git revisions to SVN", { 'stdin|' => \$_stdin, 'edit|e' => \$_edit, @@ -217,7 +220,10 @@ sub init { $SVN_URL = shift or die "SVN repository location required " . "as a command-line argument\n"; unless (-d $GIT_DIR) { - sys('git-init-db'); + my @init_db = ('git-init-db'); + push @init_db, "--template=$_template" if defined $_template; + push @init_db, "--shared" if defined $_shared; + sys(@init_db); } setup_git_svn(); } From 9d55b41aadd65b1ebfbbe1336db00168c2dd01c5 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Mon, 12 Jun 2006 15:53:13 -0700 Subject: [PATCH 15/43] git-svn: add some functionality to better support branches in svn New commands: graft-branches - The most interesting command of the bunch. It detects branches in SVN via various techniques (currently regexes and file copies). It can be later extended to handle svk and other properties people may use to track merges in svk. Basically, merge tracking is not standardized at all in the SVN world, and git grafts are perfect for dealing with this situation. Existing branch support (via tree matches) is only handled at fetch time. The following tow were originally implemented as shell scripts several months ago, but I just decided to streamline things a bit and added them to the main script. multi-init - supports git-svnimport-like command-line syntax for importing repositories that are layed out as recommended by the SVN folks. This is a bit more tolerant than the git-svnimport command-line syntax and doesn't require the user to figure out where the repository URL ends and where the repository path begins. multi-fetch - runs fetch on all known SVN branches we're tracking. This will NOT discover new branches (unlike git-svnimport), so multi-init will need to be re-run (it's idempotent). Consider these three to be auxilliary commands (like show-ignore, and rebuild) so their behavior won't receive as much testing or scrutiny as the core commands (fetch and commit). Signed-off-by: Eric Wong --- contrib/git-svn/git-svn.perl | 429 ++++++++++++++++++++++++++++++++++- 1 file changed, 424 insertions(+), 5 deletions(-) diff --git a/contrib/git-svn/git-svn.perl b/contrib/git-svn/git-svn.perl index d8f103ed9ad..d5c7e479671 100755 --- a/contrib/git-svn/git-svn.perl +++ b/contrib/git-svn/git-svn.perl @@ -35,8 +35,8 @@ my $sha1_short = qr/[a-f\d]{4,40}/; my ($_revision,$_stdin,$_no_ignore_ext,$_no_stop_copy,$_help,$_rmdir,$_edit, $_find_copies_harder, $_l, $_cp_similarity, $_repack, $_repack_nr, $_repack_flags, - $_template, $_shared, - $_version, $_upgrade, $_authors, $_branch_all_refs); + $_template, $_shared, $_no_default_regex, $_no_graft_copy, + $_version, $_upgrade, $_authors, $_branch_all_refs, @_opt_m); my (@_branch_from, %tree_map, %users); my ($_svn_co_url_revs, $_svn_pg_peg_revs); my @repo_path_split_cache; @@ -48,6 +48,12 @@ my %fc_opts = ( 'no-ignore-externals' => \$_no_ignore_ext, 'repack:i' => \$_repack, 'repack-flags|repack-args|repack-opts=s' => \$_repack_flags); +my ($_trunk, $_tags, $_branches); +my %multi_opts = ( 'trunk|T=s' => \$_trunk, + 'tags|t=s' => \$_tags, + 'branches|b=s' => \$_branches ); +my %init_opts = ( 'template=s' => \$_template, 'shared' => \$_shared ); + # yes, 'native' sets "\n". Patches to fix this for non-*nix systems welcome: my %EOL = ( CR => "\015", LF => "\012", CRLF => "\015\012", native => "\012" ); @@ -56,8 +62,7 @@ my %cmd = ( { 'revision|r=s' => \$_revision, %fc_opts } ], init => [ \&init, "Initialize a repo for tracking" . " (requires URL argument)", - { 'template=s' => \$_template, - 'shared' => \$_shared } ], + \%init_opts ], commit => [ \&commit, "Commit git revisions to SVN", { 'stdin|' => \$_stdin, 'edit|e' => \$_edit, @@ -71,7 +76,19 @@ my %cmd = ( rebuild => [ \&rebuild, "Rebuild git-svn metadata (after git clone)", { 'no-ignore-externals' => \$_no_ignore_ext, 'upgrade' => \$_upgrade } ], + 'graft-branches' => [ \&graft_branches, + 'Detect merges/branches from already imported history', + { 'merge-rx|m' => \@_opt_m, + 'no-default-regex' => \$_no_default_regex, + 'no-graft-copy' => \$_no_graft_copy } ], + 'multi-init' => [ \&multi_init, + 'Initialize multiple trees (like git-svnimport)', + { %multi_opts, %fc_opts } ], + 'multi-fetch' => [ \&multi_fetch, + 'Fetch multiple trees (like git-svnimport)', + \%fc_opts ], ); + my $cmd; for (my $i = 0; $i < @ARGV; $i++) { if (defined $cmd{$ARGV[$i]}) { @@ -96,7 +113,7 @@ init_vars(); load_authors() if $_authors; load_all_refs() if $_branch_all_refs; svn_compat_check(); -migration_check() unless $cmd eq 'init'; +migration_check() unless $cmd =~ /^(?:init|multi-init)$/; $cmd{$cmd}->[0]->(@ARGV); exit 0; @@ -219,6 +236,7 @@ when you have upgraded your tools and habits to use refs/remotes/$GIT_SVN sub init { $SVN_URL = shift or die "SVN repository location required " . "as a command-line argument\n"; + $SVN_URL =~ s!/+$!!; # strip trailing slash unless (-d $GIT_DIR) { my @init_db = ('git-init-db'); push @init_db, "--template=$_template" if defined $_template; @@ -358,8 +376,283 @@ sub show_ignore { } } +sub graft_branches { + my $gr_file = "$GIT_DIR/info/grafts"; + my ($grafts, $comments) = read_grafts($gr_file); + my $gr_sha1; + + if (%$grafts) { + # temporarily disable our grafts file to make this idempotent + chomp($gr_sha1 = safe_qx(qw/git-hash-object -w/,$gr_file)); + rename $gr_file, "$gr_file~$gr_sha1" or croak $!; + } + + my $l_map = read_url_paths(); + my @re = map { qr/$_/is } @_opt_m if @_opt_m; + unless ($_no_default_regex) { + push @re, ( qr/\b(?:merge|merging|merged)\s+(\S.+)/is, + qr/\b(?:from|of)\s+(\S.+)/is ); + } + foreach my $u (keys %$l_map) { + if (@re) { + foreach my $p (keys %{$l_map->{$u}}) { + graft_merge_msg($grafts,$l_map,$u,$p); + } + } + graft_file_copy($grafts,$l_map,$u) unless $_no_graft_copy; + } + + write_grafts($grafts, $comments, $gr_file); + unlink "$gr_file~$gr_sha1" if $gr_sha1; +} + +sub multi_init { + my $url = shift; + $_trunk ||= 'trunk'; + $_trunk =~ s#/+$##; + $url =~ s#/+$## if $url; + if ($_trunk !~ m#^[a-z\+]+://#) { + $_trunk = '/' . $_trunk if ($_trunk !~ m#^/#); + unless ($url) { + print STDERR "E: '$_trunk' is not a complete URL ", + "and a separate URL is not specified\n"; + exit 1; + } + $_trunk = $url . $_trunk; + } + if ($GIT_SVN eq 'git-svn') { + print "GIT_SVN_ID set to 'trunk' for $_trunk\n"; + $GIT_SVN = $ENV{GIT_SVN_ID} = 'trunk'; + } + init_vars(); + init($_trunk); + complete_url_ls_init($url, $_branches, '--branches/-b', ''); + complete_url_ls_init($url, $_tags, '--tags/-t', 'tags/'); +} + +sub multi_fetch { + # try to do trunk first, since branches/tags + # may be descended from it. + if (-d "$GIT_DIR/svn/trunk") { + print "Fetching trunk\n"; + defined(my $pid = fork) or croak $!; + if (!$pid) { + $GIT_SVN = $ENV{GIT_SVN_ID} = 'trunk'; + init_vars(); + fetch(@_); + exit 0; + } + waitpid $pid, 0; + croak $? if $?; + } + rec_fetch('', "$GIT_DIR/svn", @_); +} + ########################### utility functions ######################### +sub rec_fetch { + my ($pfx, $p, @args) = @_; + my @dir; + foreach (sort <$p/*>) { + if (-r "$_/info/url") { + $pfx .= '/' if $pfx && $pfx !~ m!/$!; + my $id = $pfx . basename $_; + next if $id eq 'trunk'; + print "Fetching $id\n"; + defined(my $pid = fork) or croak $!; + if (!$pid) { + $GIT_SVN = $ENV{GIT_SVN_ID} = $id; + init_vars(); + fetch(@args); + exit 0; + } + waitpid $pid, 0; + croak $? if $?; + } elsif (-d $_) { + push @dir, $_; + } + } + foreach (@dir) { + my $x = $_; + $x =~ s!^\Q$GIT_DIR\E/svn/!!; + rec_fetch($x, $_); + } +} + +sub complete_url_ls_init { + my ($url, $var, $switch, $pfx) = @_; + unless ($var) { + print STDERR "W: $switch not specified\n"; + return; + } + $var =~ s#/+$##; + if ($var !~ m#^[a-z\+]+://#) { + $var = '/' . $var if ($var !~ m#^/#); + unless ($url) { + print STDERR "E: '$var' is not a complete URL ", + "and a separate URL is not specified\n"; + exit 1; + } + $var = $url . $var; + } + chomp(my @ls = safe_qx(qw/svn ls --non-interactive/, $var)); + my $old = $GIT_SVN; + defined(my $pid = fork) or croak $!; + if (!$pid) { + foreach my $u (map { "$var/$_" } (grep m!/$!, @ls)) { + $u =~ s#/+$##; + if ($u !~ m!\Q$var\E/(.+)$!) { + print STDERR "W: Unrecognized URL: $u\n"; + die "This should never happen\n"; + } + my $id = $pfx.$1; + print "init $u => $id\n"; + $GIT_SVN = $ENV{GIT_SVN_ID} = $id; + init_vars(); + init($u); + } + exit 0; + } + waitpid $pid, 0; + croak $? if $?; +} + +sub common_prefix { + my $paths = shift; + my %common; + foreach (@$paths) { + my @tmp = split m#/#, $_; + my $p = ''; + while (my $x = shift @tmp) { + $p .= "/$x"; + $common{$p} ||= 0; + $common{$p}++; + } + } + foreach (sort {length $b <=> length $a} keys %common) { + if ($common{$_} == @$paths) { + return $_; + } + } + return ''; +} + +# this isn't funky-filename safe, but good enough for now... +sub graft_file_copy { + my ($grafts, $l_map, $u) = @_; + my $paths = $l_map->{$u}; + my $pfx = common_prefix([keys %$paths]); + + my $pid = open my $fh, '-|'; + defined $pid or croak $!; + unless ($pid) { + exec(qw/svn log -v/, $u.$pfx) or croak $!; + } + my ($r, $mp) = (undef, undef); + while (<$fh>) { + chomp; + if (/^\-{72}$/) { + $mp = $r = undef; + } elsif (/^r(\d+) \| /) { + $r = $1 unless defined $r; + } elsif (/^Changed paths:/) { + $mp = 1; + } elsif ($mp && m#^ [AR] /(\S.*?) \(from /(\S+?):(\d+)\)$#) { + my $dbg = "r$r | $_"; + my ($p1, $p0, $r0) = ($1, $2, $3); + my $c; + foreach my $x (keys %$paths) { + next unless ($p1 =~ /^\Q$x\E/); + my $i = $paths->{$x}; + my $f = "$GIT_DIR/svn/$i/revs/$r"; + unless (-r $f) { + print STDERR "r$r of $i not imported,", + " $dbg\n"; + next; + } + $c = file_to_s($f); + } + next unless $c; + foreach my $x (keys %$paths) { + next unless ($p0 =~ /^\Q$x\E/); + my $i = $paths->{$x}; + my $f = "$GIT_DIR/svn/$i/revs/$r0"; + while ($r0 && !-r $f) { + # could be an older revision, too... + $r0--; + $f = "$GIT_DIR/svn/$i/revs/$r0"; + } + unless (-r $f) { + print STDERR "r$r0 of $i not imported,", + " $dbg\n"; + next; + } + my $r1 = file_to_s($f); + $grafts->{$c}->{$r1} = 1; + } + } + } +} + +sub process_merge_msg_matches { + my ($grafts, $l_map, $u, $p, $c, @matches) = @_; + my (@strong, @weak); + foreach (@matches) { + # merging with ourselves is not interesting + next if $_ eq $p; + if ($l_map->{$u}->{$_}) { + push @strong, $_; + } else { + push @weak, $_; + } + } + foreach my $w (@weak) { + last if @strong; + # no exact match, use branch name as regexp. + my $re = qr/\Q$w\E/i; + foreach (keys %{$l_map->{$u}}) { + if (/$re/) { + push @strong, $_; + last; + } + } + last if @strong; + $w = basename($w); + $re = qr/\Q$w\E/i; + foreach (keys %{$l_map->{$u}}) { + if (/$re/) { + push @strong, $_; + last; + } + } + } + my ($rev) = ($c->{m} =~ /^git-svn-id:\s(?:\S+?)\@(\d+) + \s(?:[a-f\d\-]+)$/xsm); + unless (defined $rev) { + ($rev) = ($c->{m} =~/^git-svn-id:\s(\d+) + \@(?:[a-f\d\-]+)/xsm); + return unless defined $rev; + } + foreach my $m (@strong) { + my ($r0, $s0) = find_rev_before($rev, $m); + $grafts->{$c->{c}}->{$s0} = 1 if defined $s0; + } +} + +sub graft_merge_msg { + my ($grafts, $l_map, $u, $p, @re) = @_; + + my $x = $l_map->{$u}->{$p}; + my $rl = rev_list_raw($x); + while (my $c = next_rev_list_entry($rl)) { + foreach my $re (@re) { + my (@br) = ($c->{m} =~ /$re/g); + next unless @br; + process_merge_msg_matches($grafts,$l_map,$u,$p,$c,@br); + } + } +} + sub read_uuid { return if $SVN_UUID; my $info = shift || svn_info('.'); @@ -402,6 +695,7 @@ sub repo_path_split { $url .= "/$n"; } push @repo_path_split_cache, qr/^(\Q$url\E)/; + $path = join('/',@paths); return ($url, $path); } @@ -806,6 +1100,38 @@ sub svn_commit_tree { return fetch("$committed=$commit")->{revision}; } +sub rev_list_raw { + my (@args) = @_; + my $pid = open my $fh, '-|'; + defined $pid or croak $!; + if (!$pid) { + exec(qw/git-rev-list --pretty=raw/, @args) or croak $!; + } + return { fh => $fh, t => { } }; +} + +sub next_rev_list_entry { + my $rl = shift; + my $fh = $rl->{fh}; + my $x = $rl->{t}; + while (<$fh>) { + if (/^commit ($sha1)$/o) { + if ($x->{c}) { + $rl->{t} = { c => $1 }; + return $x; + } else { + $x->{c} = $1; + } + } elsif (/^parent ($sha1)$/o) { + $x->{p}->{$1} = 1; + } elsif (s/^ //) { + $x->{m} ||= ''; + $x->{m} .= $_; + } + } + return ($x != $rl->{t}) ? $x : undef; +} + # read the entire log into a temporary file (which is removed ASAP) # and store the file handle + parser state sub svn_log_raw { @@ -1318,6 +1644,16 @@ sub svn_propget_base { return safe_qx(qw/svn propget/, $p, $f); } +sub git_svn_each { + my $sub = shift; + foreach (`git-rev-parse --symbolic --all`) { + next unless s#^refs/remotes/##; + chomp $_; + next unless -f "$GIT_DIR/svn/$_/info/url"; + &$sub($_); + } +} + sub migration_check { return if (-d "$GIT_DIR/svn" || !-d $GIT_DIR); print "Upgrading repository...\n"; @@ -1344,6 +1680,16 @@ sub migration_check { print "Done upgrading.\n"; } +sub find_rev_before { + my ($r, $git_svn_id) = @_; + my @revs = map { basename $_ } <$GIT_DIR/svn/$git_svn_id/revs/*>; + foreach my $r0 (sort { $b <=> $a } @revs) { + next if $r0 >= $r; + return ($r0, file_to_s("$GIT_DIR/svn/$git_svn_id/revs/$r0")); + } + return (undef, undef); +} + sub init_vars { $GIT_SVN ||= $ENV{GIT_SVN_ID} || 'git-svn'; $GIT_SVN_DIR = "$GIT_DIR/svn/$GIT_SVN"; @@ -1384,6 +1730,79 @@ sub set_default_vals { } } +sub read_grafts { + my $gr_file = shift; + my ($grafts, $comments) = ({}, {}); + if (open my $fh, '<', $gr_file) { + my @tmp; + while (<$fh>) { + if (/^($sha1)\s+/) { + my $c = $1; + if (@tmp) { + @{$comments->{$c}} = @tmp; + @tmp = (); + } + foreach my $p (split /\s+/, $_) { + $grafts->{$c}->{$p} = 1; + } + } else { + push @tmp, $_; + } + } + close $fh or croak $!; + @{$comments->{'END'}} = @tmp if @tmp; + } + return ($grafts, $comments); +} + +sub write_grafts { + my ($grafts, $comments, $gr_file) = @_; + + open my $fh, '>', $gr_file or croak $!; + foreach my $c (sort keys %$grafts) { + if ($comments->{$c}) { + print $fh $_ foreach @{$comments->{$c}}; + } + my $p = $grafts->{$c}; + delete $p->{$c}; # commits are not self-reproducing... + my $pid = open my $ch, '-|'; + defined $pid or croak $!; + if (!$pid) { + exec(qw/git-cat-file commit/, $c) or croak $!; + } + while (<$ch>) { + if (/^parent ([a-f\d]{40})/) { + $p->{$1} = 1; + } else { + last unless /^\S/i; + } + } + close $ch; # breaking the pipe + print $fh $c, ' ', join(' ', sort keys %$p),"\n"; + } + if ($comments->{'END'}) { + print $fh $_ foreach @{$comments->{'END'}}; + } + close $fh or croak $!; +} + +sub read_url_paths { + my $l_map = {}; + git_svn_each(sub { my $x = shift; + my $u = file_to_s("$GIT_DIR/svn/$x/info/repo_url"); + my $p = file_to_s("$GIT_DIR/svn/$x/info/repo_path"); + # we hate trailing slashes + if ($u =~ s#(?:^\/+|\/+$)##g) { + s_to_file($u,"$GIT_DIR/svn/$x/info/repo_url"); + } + if ($p =~ s#(?:^\/+|\/+$)##g) { + s_to_file($p,"$GIT_DIR/svn/$x/info/repo_path"); + } + $l_map->{$u}->{$p} = $x; + }); + return $l_map; +} + __END__ Data structures: From 7a97de4e19757b5576f32ce67d90cb792dbb893b Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Mon, 12 Jun 2006 05:57:02 -0700 Subject: [PATCH 16/43] git-svn: add UTF-8 message test Signed-off-by: Eric Wong --- contrib/git-svn/t/t0000-contrib-git-svn.sh | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/contrib/git-svn/t/t0000-contrib-git-svn.sh b/contrib/git-svn/t/t0000-contrib-git-svn.sh index a07fbad68bd..0c6ff2066b7 100644 --- a/contrib/git-svn/t/t0000-contrib-git-svn.sh +++ b/contrib/git-svn/t/t0000-contrib-git-svn.sh @@ -4,6 +4,7 @@ # test_description='git-svn tests' +GIT_SVN_LC_ALL=$LC_ALL . ./lib-git-svn.sh mkdir import @@ -163,6 +164,18 @@ test_expect_success "$name" \ diff -u help $SVN_TREE/exec-2.sh" +if test -n "$GIT_SVN_LC_ALL" && echo $GIT_SVN_LC_ALL | grep -q '\.UTF-8$' +then + name="commit with UTF-8 message: locale: $GIT_SVN_LC_ALL" + echo '# hello' >> exec-2.sh + git update-index exec-2.sh + git commit -m 'éï∏' + export LC_ALL="$GIT_SVN_LC_ALL" + test_expect_success "$name" "git-svn commit HEAD" + unset LC_ALL +else + echo "UTF-8 locale not set, test skipped ($GIT_SVN_LC_ALL)" +fi name='test fetch functionality (svn => git) with alternate GIT_SVN_ID' GIT_SVN_ID=alt From 79bb8d88fc61b03a80fe99915f15a25172286c1f Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Thu, 1 Jun 2006 02:35:44 -0700 Subject: [PATCH 17/43] git-svn: add 'log' command, a facsimile of basic `svn log' This quick feature should make it easy to look up svn log messages when svn users refer to -r/--revision numbers. The following features from `svn log' are supported: --revision=[:] - is supported, non-numeric args are not: HEAD, NEXT, BASE, PREV, etc ... -v/--verbose - just maps to --raw (in git log), so it's completely incompatible with the --verbose output in svn log --limit= - is NOT the same as --max-count, doesn't count merged/excluded commits --incremental - supported (trivial :P) New features: --show-commit - shows the git commit sha1, as well --oneline - our version of --pretty=oneline Any other arguments are passed directly to `git log' Signed-off-by: Eric Wong --- contrib/git-svn/git-svn.perl | 260 ++++++++++++++++++++++++++++++++--- 1 file changed, 243 insertions(+), 17 deletions(-) diff --git a/contrib/git-svn/git-svn.perl b/contrib/git-svn/git-svn.perl index d5c7e479671..03416aeec15 100755 --- a/contrib/git-svn/git-svn.perl +++ b/contrib/git-svn/git-svn.perl @@ -15,6 +15,7 @@ $GIT_DIR = abs_path($ENV{GIT_DIR} || '.git'); $ENV{GIT_DIR} = $GIT_DIR; my $LC_ALL = $ENV{LC_ALL}; +my $TZ = $ENV{TZ}; # make sure the svn binary gives consistent output between locales and TZs: $ENV{TZ} = 'UTC'; $ENV{LC_ALL} = 'C'; @@ -27,7 +28,7 @@ use Carp qw/croak/; use IO::File qw//; use File::Basename qw/dirname basename/; use File::Path qw/mkpath/; -use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev/; +use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev pass_through/; use File::Spec qw//; use POSIX qw/strftime/; my $sha1 = qr/[a-f\d]{40}/; @@ -36,8 +37,9 @@ my ($_revision,$_stdin,$_no_ignore_ext,$_no_stop_copy,$_help,$_rmdir,$_edit, $_find_copies_harder, $_l, $_cp_similarity, $_repack, $_repack_nr, $_repack_flags, $_template, $_shared, $_no_default_regex, $_no_graft_copy, + $_limit, $_verbose, $_incremental, $_oneline, $_l_fmt, $_show_commit, $_version, $_upgrade, $_authors, $_branch_all_refs, @_opt_m); -my (@_branch_from, %tree_map, %users); +my (@_branch_from, %tree_map, %users, %rusers); my ($_svn_co_url_revs, $_svn_pg_peg_revs); my @repo_path_split_cache; @@ -87,6 +89,15 @@ my %cmd = ( 'multi-fetch' => [ \&multi_fetch, 'Fetch multiple trees (like git-svnimport)', \%fc_opts ], + 'log' => [ \&show_log, 'Show commit logs', + { 'limit=i' => \$_limit, + 'revision|r=s' => \$_revision, + 'verbose|v' => \$_verbose, + 'incremental' => \$_incremental, + 'oneline' => \$_oneline, + 'show-commit' => \$_show_commit, + 'authors-file|A=s' => \$_authors, + } ], ); my $cmd; @@ -101,9 +112,10 @@ for (my $i = 0; $i < @ARGV; $i++) { my %opts = %{$cmd{$cmd}->[2]} if (defined $cmd); read_repo_config(\%opts); -GetOptions(%opts, 'help|H|h' => \$_help, - 'version|V' => \$_version, - 'id|i=s' => \$GIT_SVN) or exit 1; +my $rv = GetOptions(%opts, 'help|H|h' => \$_help, + 'version|V' => \$_version, + 'id|i=s' => \$GIT_SVN); +exit 1 if (!$rv && $cmd ne 'log'); set_default_vals(); usage(0) if $_help; @@ -173,18 +185,10 @@ sub rebuild { croak "Non-SHA1: $c\n" unless $c =~ /^$sha1$/o; my @commit = grep(/^git-svn-id: /,`git-cat-file commit $c`); next if (!@commit); # skip merges - my $id = $commit[$#commit]; - my ($url, $rev, $uuid) = ($id =~ /^git-svn-id:\s(\S+?)\@(\d+) - \s([a-f\d\-]+)$/x); - if (!$rev || !$uuid || !$url) { - # some of the original repositories I made had - # indentifiers like this: - ($rev, $uuid) = ($id =~/^git-svn-id:\s(\d+) - \@([a-f\d\-]+)/x); - if (!$rev || !$uuid) { - croak "Unable to extract revision or UUID from ", - "$c, $id\n"; - } + my ($url, $rev, $uuid) = extract_metadata($commit[$#commit]); + if (!$rev || !$uuid) { + croak "Unable to extract revision or UUID from ", + "$c, $commit[$#commit]\n"; } # if we merged or otherwise started elsewhere, this is @@ -448,6 +452,81 @@ sub multi_fetch { rec_fetch('', "$GIT_DIR/svn", @_); } +sub show_log { + my (@args) = @_; + my ($r_min, $r_max); + my $r_last = -1; # prevent dupes + rload_authors() if $_authors; + if (defined $TZ) { + $ENV{TZ} = $TZ; + } else { + delete $ENV{TZ}; + } + if (defined $_revision) { + if ($_revision =~ /^(\d+):(\d+)$/) { + ($r_min, $r_max) = ($1, $2); + } elsif ($_revision =~ /^\d+$/) { + $r_min = $r_max = $_revision; + } else { + print STDERR "-r$_revision is not supported, use ", + "standard \'git log\' arguments instead\n"; + exit 1; + } + } + + my $pid = open(my $log,'-|'); + defined $pid or croak $!; + if (!$pid) { + my @rl = (qw/git-log --abbrev-commit --pretty=raw + --default/, "remotes/$GIT_SVN"); + push @rl, '--raw' if $_verbose; + exec(@rl, @args) or croak $!; + } + setup_pager(); + my (@k, $c, $d); + while (<$log>) { + if (/^commit ($sha1_short)/o) { + my $cmt = $1; + if ($c && defined $c->{r} && $c->{r} != $r_last) { + $r_last = $c->{r}; + process_commit($c, $r_min, $r_max, \@k) or + goto out; + } + $d = undef; + $c = { c => $cmt }; + } elsif (/^author (.+) (\d+) ([\-\+]?\d+)$/) { + get_author_info($c, $1, $2, $3); + } elsif (/^(?:tree|parent|committer) /) { + # ignore + } elsif (/^:\d{6} \d{6} $sha1_short/o) { + push @{$c->{raw}}, $_; + } elsif (/^diff /) { + $d = 1; + push @{$c->{diff}}, $_; + } elsif ($d) { + push @{$c->{diff}}, $_; + } elsif (/^ (git-svn-id:.+)$/) { + my ($url, $rev, $uuid) = extract_metadata($1); + $c->{r} = $rev; + } elsif (s/^ //) { + push @{$c->{l}}, $_; + } + } + if ($c && defined $c->{r} && $c->{r} != $r_last) { + $r_last = $c->{r}; + process_commit($c, $r_min, $r_max, \@k); + } + if (@k) { + my $swap = $r_max; + $r_max = $r_min; + $r_min = $swap; + process_commit($_, $r_min, $r_max) foreach reverse @k; + } +out: + close $log; + print '-' x72,"\n" unless $_incremental || $_oneline; +} + ########################### utility functions ######################### sub rec_fetch { @@ -1638,6 +1717,17 @@ sub load_authors { close $authors or croak $!; } +sub rload_authors { + open my $authors, '<', $_authors or die "Can't open $_authors $!\n"; + while (<$authors>) { + chomp; + next unless /^(\S+?)\s*=\s*(.+?)\s*<(.+)>\s*$/; + my ($user, $name, $email) = ($1, $2, $3); + $rusers{"$name <$email>"} = $user; + } + close $authors or croak $!; +} + sub svn_propget_base { my ($p, $f) = @_; $f .= '@BASE' if $_svn_pg_peg_revs; @@ -1803,6 +1893,142 @@ sub read_url_paths { return $l_map; } +sub extract_metadata { + my $id = shift; + my ($url, $rev, $uuid) = ($id =~ /^git-svn-id:\s(\S+?)\@(\d+) + \s([a-f\d\-]+)$/x); + if (!$rev || !$uuid || !$url) { + # some of the original repositories I made had + # indentifiers like this: + ($rev, $uuid) = ($id =~/^git-svn-id:\s(\d+)\@([a-f\d\-]+)/); + } + return ($url, $rev, $uuid); +} + +sub tz_to_s_offset { + my ($tz) = @_; + $tz =~ s/(\d\d)$//; + return ($1 * 60) + ($tz * 3600); +} + +sub setup_pager { # translated to Perl from pager.c + return unless (-t *STDOUT); + my $pager = $ENV{PAGER}; + if (!defined $pager) { + $pager = 'less'; + } elsif (length $pager == 0 || $pager eq 'cat') { + return; + } + pipe my $rfd, my $wfd or return; + defined(my $pid = fork) or croak $!; + if (!$pid) { + open STDOUT, '>&', $wfd or croak $!; + return; + } + open STDIN, '<&', $rfd or croak $!; + $ENV{LESS} ||= '-S'; + exec $pager or croak "Can't run pager: $!\n";; +} + +sub get_author_info { + my ($dest, $author, $t, $tz) = @_; + $author =~ s/(?:^\s*|\s*$)//g; + my $_a; + if ($_authors) { + $_a = $rusers{$author} || undef; + } + if (!$_a) { + ($_a) = ($author =~ /<([^>]+)\@[^>]+>$/); + } + $dest->{t} = $t; + $dest->{tz} = $tz; + $dest->{a} = $_a; + # Date::Parse isn't in the standard Perl distro :( + if ($tz =~ s/^\+//) { + $t += tz_to_s_offset($tz); + } elsif ($tz =~ s/^\-//) { + $t -= tz_to_s_offset($tz); + } + $dest->{t_utc} = $t; +} + +sub process_commit { + my ($c, $r_min, $r_max, $defer) = @_; + if (defined $r_min && defined $r_max) { + if ($r_min == $c->{r} && $r_min == $r_max) { + show_commit($c); + return 0; + } + return 1 if $r_min == $r_max; + if ($r_min < $r_max) { + # we need to reverse the print order + return 0 if (defined $_limit && --$_limit < 0); + push @$defer, $c; + return 1; + } + if ($r_min != $r_max) { + return 1 if ($r_min < $c->{r}); + return 1 if ($r_max > $c->{r}); + } + } + return 0 if (defined $_limit && --$_limit < 0); + show_commit($c); + return 1; +} + +sub show_commit { + my $c = shift; + if ($_oneline) { + my $x = "\n"; + if (my $l = $c->{l}) { + while ($l->[0] =~ /^\s*$/) { shift @$l } + $x = $l->[0]; + } + $_l_fmt ||= 'A' . length($c->{r}); + print 'r',pack($_l_fmt, $c->{r}),' | '; + print "$c->{c} | " if $_show_commit; + print $x; + } else { + show_commit_normal($c); + } +} + +sub show_commit_normal { + my ($c) = @_; + print '-' x72, "\nr$c->{r} | "; + print "$c->{c} | " if $_show_commit; + print "$c->{a} | ", strftime("%Y-%m-%d %H:%M:%S %z (%a, %d %b %Y)", + localtime($c->{t_utc})), ' | '; + my $nr_line = 0; + + if (my $l = $c->{l}) { + while ($l->[$#$l] eq "\n" && $l->[($#$l - 1)] eq "\n") { + pop @$l; + } + $nr_line = scalar @$l; + if (!$nr_line) { + print "1 line\n\n\n"; + } else { + if ($nr_line == 1) { + $nr_line = '1 line'; + } else { + $nr_line .= ' lines'; + } + print $nr_line, "\n\n"; + print $_ foreach @$l; + } + } else { + print "1 line\n\n"; + + } + foreach my $x (qw/raw diff/) { + if ($c->{$x}) { + print "\n"; + print $_ foreach @{$c->{$x}} + } + } +} + __END__ Data structures: From a5e0cedc0a4d0018f3e7e4ba8ca54c91742dd859 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Mon, 12 Jun 2006 15:23:48 -0700 Subject: [PATCH 18/43] git-svn: add support for Perl SVN::* libraries This means we no longer have to deal with having bloated SVN working copies around and we get a nice performance increase as well because we don't have to exec the SVN binary and start a new server connection each time. Of course we have to manually manage memory with SVN::Pool whenever we can, and hack around cases where SVN just eats memory despite pools (I blame Perl, too). I would like to keep memory usage as stable as possible during long fetch/commit processes since I still use computers with only 256-512M RAM. commit should always be faster with the SVN library code. The SVN::Delta interface is leaky (or I'm not using it with pools correctly), so I'm forking on every commit, but that doesn't seem to hurt performance too much (at least on normal Unix/Linux systems where fork() is pretty cheap). fetch should be faster in most common cases, but probably not all. fetches will be faster where client/server delta generation is the bottleneck and not bandwidth. Of course, full-files are generated server-side via deltas, too. Full files are always transferred when they're updated, just like git-svnimport and unlike command-line svn. I'm also hacking around memory leaks (see comments) here by using some more forks. I've tested fetch with http://, https://, file://, and svn:// repositories, so we should be reasonably covered in terms of error handling for fetching. Of course, we'll keep plain command-line svn compatibility as a fallback for people running SVN 1.1 (I'm looking into library support for 1.1.x SVN, too). If you want to force command-line SVN usage, set GIT_SVN_NO_LIB=1 in your environment. We also require two simultaneous connections (just like git-svnimport), but this shouldn't be a problem for most servers. Less important commands: show-ignore is slower because it requires repository access, but -r/--revision can be specified. graft-branches may use more memory, but it's a short-term process and is funky-filename-safe. Signed-off-by: Eric Wong --- contrib/git-svn/git-svn.perl | 1068 ++++++++++++++++++-- contrib/git-svn/t/lib-git-svn.sh | 2 +- contrib/git-svn/t/t0000-contrib-git-svn.sh | 15 +- 3 files changed, 974 insertions(+), 111 deletions(-) diff --git a/contrib/git-svn/git-svn.perl b/contrib/git-svn/git-svn.perl index 03416aeec15..9618c8bab52 100755 --- a/contrib/git-svn/git-svn.perl +++ b/contrib/git-svn/git-svn.perl @@ -31,6 +31,10 @@ use File::Path qw/mkpath/; use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev pass_through/; use File::Spec qw//; use POSIX qw/strftime/; + +my ($SVN_PATH, $SVN, $SVN_LOG, $_use_lib); +$_use_lib = 1 unless $ENV{GIT_SVN_NO_LIB}; +libsvn_load(); my $sha1 = qr/[a-f\d]{40}/; my $sha1_short = qr/[a-f\d]{4,40}/; my ($_revision,$_stdin,$_no_ignore_ext,$_no_stop_copy,$_help,$_rmdir,$_edit, @@ -74,7 +78,8 @@ my %cmd = ( 'copy-similarity|C=i'=> \$_cp_similarity, %fc_opts, } ], - 'show-ignore' => [ \&show_ignore, "Show svn:ignore listings", { } ], + 'show-ignore' => [ \&show_ignore, "Show svn:ignore listings", + { 'revision|r=i' => \$_revision } ], rebuild => [ \&rebuild, "Rebuild git-svn metadata (after git clone)", { 'no-ignore-externals' => \$_no_ignore_ext, 'upgrade' => \$_upgrade } ], @@ -211,6 +216,8 @@ sub rebuild { $newest_rev = $rev if ($rev > $newest_rev); } close $rev_list or croak $?; + + goto out if $_use_lib; if (!chdir $SVN_WC) { svn_cmd_checkout($SVN_URL, $latest, $SVN_WC); chdir $SVN_WC or croak $!; @@ -228,7 +235,7 @@ sub rebuild { } waitpid $pid, 0; croak $? if $?; - +out: if ($_upgrade) { print STDERR <<""; Keeping deprecated refs/head/$GIT_SVN-HEAD for now. Please remove it @@ -251,9 +258,18 @@ sub init { } sub fetch { - my (@parents) = @_; check_upgrade_needed(); $SVN_URL ||= file_to_s("$GIT_SVN_DIR/info/url"); + my $ret = $_use_lib ? fetch_lib(@_) : fetch_cmd(@_); + if ($ret->{commit} && quiet_run(qw(git-rev-parse --verify + refs/heads/master^0))) { + sys(qw(git-update-ref refs/heads/master),$ret->{commit}); + } + return $ret; +} + +sub fetch_cmd { + my (@parents) = @_; my @log_args = -d $SVN_WC ? ($SVN_WC) : ($SVN_URL); unless ($_revision) { $_revision = -d $SVN_WC ? 'BASE:HEAD' : '0:HEAD'; @@ -301,13 +317,91 @@ sub fetch { $last_commit = git_commit($log_msg, $last_commit, @parents); $last = $log_msg; } - unless (-e "$GIT_DIR/refs/heads/master") { - sys(qw(git-update-ref refs/heads/master),$last_commit); - } close $svn_log->{fh}; + $last->{commit} = $last_commit; return $last; } +sub fetch_lib { + my (@parents) = @_; + $SVN_URL ||= file_to_s("$GIT_SVN_DIR/info/url"); + my $repo; + ($repo, $SVN_PATH) = repo_path_split($SVN_URL); + $SVN_LOG ||= libsvn_connect($repo); + $SVN ||= libsvn_connect($repo); + my ($last_rev, $last_commit) = svn_grab_base_rev(); + my ($base, $head) = libsvn_parse_revision($last_rev); + if ($base > $head) { + return { revision => $last_rev, commit => $last_commit } + } + my $index = set_index($GIT_SVN_INDEX); + + # limit ourselves and also fork() since get_log won't release memory + # after processing a revision and SVN stuff seems to leak + my $inc = 1000; + my ($min, $max) = ($base, $head < $base+$inc ? $head : $base+$inc); + read_uuid(); + if (defined $last_commit) { + unless (-e $GIT_SVN_INDEX) { + sys(qw/git-read-tree/, $last_commit); + } + chomp (my $x = `git-write-tree`); + my ($y) = (`git-cat-file commit $last_commit` + =~ /^tree ($sha1)/m); + if ($y ne $x) { + unlink $GIT_SVN_INDEX or croak $!; + sys(qw/git-read-tree/, $last_commit); + } + chomp ($x = `git-write-tree`); + if ($y ne $x) { + print STDERR "trees ($last_commit) $y != $x\n", + "Something is seriously wrong...\n"; + } + } + while (1) { + # fork, because using SVN::Pool with get_log() still doesn't + # seem to help enough to keep memory usage down. + defined(my $pid = fork) or croak $!; + if (!$pid) { + $SVN::Error::handler = \&libsvn_skip_unknown_revs; + print "Fetching revisions $min .. $max\n"; + + # Yes I'm perfectly aware that the fourth argument + # below is the limit revisions number. Unfortunately + # performance sucks with it enabled, so it's much + # faster to fetch revision ranges instead of relying + # on the limiter. + $SVN_LOG->get_log( '/'.$SVN_PATH, $min, $max, 0, 1, 1, + sub { + my $log_msg; + if ($last_commit) { + $log_msg = libsvn_fetch( + $last_commit, @_); + $last_commit = git_commit( + $log_msg, + $last_commit, + @parents); + } else { + $log_msg = libsvn_new_tree(@_); + $last_commit = git_commit( + $log_msg, @parents); + } + }); + $SVN::Error::handler = sub { 'quiet warnings' }; + exit 0; + } + waitpid $pid, 0; + croak $? if $?; + ($last_rev, $last_commit) = svn_grab_base_rev(); + last if ($max >= $head); + $min = $max + 1; + $max += $inc; + $max = $head if ($max > $head); + } + restore_index($index); + return { revision => $last_rev, commit => $last_commit }; +} + sub commit { my (@commits) = @_; check_upgrade_needed(); @@ -332,6 +426,12 @@ sub commit { } } chomp @revs; + $_use_lib ? commit_lib(@revs) : commit_cmd(@revs); + print "Done committing ",scalar @revs," revisions to SVN\n"; +} + +sub commit_cmd { + my (@revs) = @_; chdir $SVN_WC or croak "Unable to chdir $SVN_WC: $!\n"; my $info = svn_info('.'); @@ -353,17 +453,95 @@ sub commit { } $svn_current_rev = svn_commit_tree($svn_current_rev, $c); } - print "Done committing ",scalar @revs," revisions to SVN\n"; +} + +sub commit_lib { + my (@revs) = @_; + my ($r_last, $cmt_last) = svn_grab_base_rev(); + defined $r_last or die "Must have an existing revision to commit\n"; + my $fetched = fetch_lib(); + if ($r_last != $fetched->{revision}) { + print STDERR "There are new revisions that were fetched ", + "and need to be merged (or acknowledged) ", + "before committing.\n", + "last rev: $r_last\n", + " current: $fetched->{revision}\n"; + exit 1; + } + read_uuid(); + my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef, 0) : (); + my $commit_msg = "$GIT_SVN_DIR/.svn-commit.tmp.$$"; + + foreach my $c (@revs) { + # fork for each commit because there's a memory leak I + # can't track down... (it's probably in the SVN code) + defined(my $pid = open my $fh, '-|') or croak $!; + if (!$pid) { + if (defined $LC_ALL) { + $ENV{LC_ALL} = $LC_ALL; + } else { + delete $ENV{LC_ALL}; + } + my $log_msg = get_commit_message($c, $commit_msg); + my $ed = SVN::Git::Editor->new( + { r => $r_last, + ra => $SVN, + c => $c, + svn_path => $SVN_PATH + }, + $SVN->get_commit_editor( + $log_msg->{msg}, + sub { + libsvn_commit_cb( + @_, $c, + $log_msg->{msg}, + $r_last, + $cmt_last) + }, + @lock) + ); + my $mods = libsvn_checkout_tree($r_last, $c, $ed); + if (@$mods == 0) { + print "No changes\nr$r_last = $cmt_last\n"; + $ed->abort_edit; + } else { + $ed->close_edit; + } + exit 0; + } + my ($r_new, $cmt_new, $no); + while (<$fh>) { + print $_; + chomp; + if (/^r(\d+) = ($sha1)$/o) { + ($r_new, $cmt_new) = ($1, $2); + } elsif ($_ eq 'No changes') { + $no = 1; + } + } + close $fh or croak $!; + if (! defined $r_new && ! defined $cmt_new) { + unless ($no) { + die "Failed to parse revision information\n"; + } + } else { + ($r_last, $cmt_last) = ($r_new, $cmt_new); + } + } + unlink $commit_msg; } sub show_ignore { - require File::Find or die $!; - my $exclude_file = "$GIT_DIR/info/exclude"; - open my $fh, '<', $exclude_file or croak $!; - chomp(my @excludes = (<$fh>)); - close $fh or croak $!; - $SVN_URL ||= file_to_s("$GIT_SVN_DIR/info/url"); + $_use_lib ? show_ignore_lib() : show_ignore_cmd(); +} + +sub show_ignore_cmd { + require File::Find or die $!; + if (defined $_revision) { + die "-r/--revision option doesn't work unless the Perl SVN ", + "libraries are used\n"; + } chdir $SVN_WC or croak $!; my %ign; File::Find::find({wanted=>sub{if(lstat $_ && -d _ && -d "$_/.svn"){ @@ -380,6 +558,14 @@ sub show_ignore { } } +sub show_ignore_lib { + my $repo; + ($repo, $SVN_PATH) = repo_path_split($SVN_URL); + $SVN ||= libsvn_connect($repo); + my $r = defined $_revision ? $_revision : $SVN->get_latest_revnum; + libsvn_traverse_ignore(\*STDOUT, $SVN_PATH, $r); +} + sub graft_branches { my $gr_file = "$GIT_DIR/info/grafts"; my ($grafts, $comments) = read_grafts($gr_file); @@ -403,7 +589,13 @@ sub graft_branches { graft_merge_msg($grafts,$l_map,$u,$p); } } - graft_file_copy($grafts,$l_map,$u) unless $_no_graft_copy; + unless ($_no_graft_copy) { + if ($_use_lib) { + graft_file_copy_lib($grafts,$l_map,$u); + } else { + graft_file_copy_cmd($grafts,$l_map,$u); + } + } } write_grafts($grafts, $comments, $gr_file); @@ -574,7 +766,8 @@ sub complete_url_ls_init { } $var = $url . $var; } - chomp(my @ls = safe_qx(qw/svn ls --non-interactive/, $var)); + chomp(my @ls = $_use_lib ? libsvn_ls_fullurl($var) + : safe_qx(qw/svn ls --non-interactive/, $var)); my $old = $GIT_SVN; defined(my $pid = fork) or croak $!; if (!$pid) { @@ -617,7 +810,7 @@ sub common_prefix { } # this isn't funky-filename safe, but good enough for now... -sub graft_file_copy { +sub graft_file_copy_cmd { my ($grafts, $l_map, $u) = @_; my $paths = $l_map->{$u}; my $pfx = common_prefix([keys %$paths]); @@ -625,7 +818,9 @@ sub graft_file_copy { my $pid = open my $fh, '-|'; defined $pid or croak $!; unless ($pid) { - exec(qw/svn log -v/, $u.$pfx) or croak $!; + my @exec = qw/svn log -v/; + push @exec, "-r$_revision" if defined $_revision; + exec @exec, $u.$pfx or croak $!; } my ($r, $mp) = (undef, undef); while (<$fh>) { @@ -637,42 +832,40 @@ sub graft_file_copy { } elsif (/^Changed paths:/) { $mp = 1; } elsif ($mp && m#^ [AR] /(\S.*?) \(from /(\S+?):(\d+)\)$#) { - my $dbg = "r$r | $_"; my ($p1, $p0, $r0) = ($1, $2, $3); - my $c; - foreach my $x (keys %$paths) { - next unless ($p1 =~ /^\Q$x\E/); - my $i = $paths->{$x}; - my $f = "$GIT_DIR/svn/$i/revs/$r"; - unless (-r $f) { - print STDERR "r$r of $i not imported,", - " $dbg\n"; - next; - } - $c = file_to_s($f); - } + my $c = find_graft_path_commit($paths, $p1, $r); next unless $c; - foreach my $x (keys %$paths) { - next unless ($p0 =~ /^\Q$x\E/); - my $i = $paths->{$x}; - my $f = "$GIT_DIR/svn/$i/revs/$r0"; - while ($r0 && !-r $f) { - # could be an older revision, too... - $r0--; - $f = "$GIT_DIR/svn/$i/revs/$r0"; - } - unless (-r $f) { - print STDERR "r$r0 of $i not imported,", - " $dbg\n"; - next; - } - my $r1 = file_to_s($f); - $grafts->{$c}->{$r1} = 1; - } + find_graft_path_parents($grafts, $paths, $c, $p0, $r0); } } } +sub graft_file_copy_lib { + my ($grafts, $l_map, $u) = @_; + my $tree_paths = $l_map->{$u}; + my $pfx = common_prefix([keys %$tree_paths]); + my ($repo, $path) = repo_path_split($u.$pfx); + $SVN_LOG ||= libsvn_connect($repo); + $SVN ||= libsvn_connect($repo); + + my ($base, $head) = libsvn_parse_revision(); + my $inc = 1000; + my ($min, $max) = ($base, $head < $base+$inc ? $head : $base+$inc); + while (1) { + my $pool = SVN::Pool->new; + $SVN_LOG->get_log( "/$path", $min, $max, 0, 1, 1, + sub { + libsvn_graft_file_copies($grafts, $tree_paths, + $path, @_); + }, $pool); + $pool->clear; + last if ($max >= $head); + $min = $max + 1; + $max += $inc; + $max = $head if ($max > $head); + } +} + sub process_merge_msg_matches { my ($grafts, $l_map, $u, $p, $c, @matches) = @_; my (@strong, @weak); @@ -734,9 +927,15 @@ sub graft_merge_msg { sub read_uuid { return if $SVN_UUID; - my $info = shift || svn_info('.'); - $SVN_UUID = $info->{'Repository UUID'} or + if ($_use_lib) { + my $pool = SVN::Pool->new; + $SVN_UUID = $SVN->get_uuid($pool); + $pool->clear; + } else { + my $info = shift || svn_info('.'); + $SVN_UUID = $info->{'Repository UUID'} or croak "Repository UUID unreadable\n"; + } s_to_file($SVN_UUID,"$GIT_SVN_DIR/info/uuid"); } @@ -769,9 +968,19 @@ sub repo_path_split { $path =~ s#^/+##; my @paths = split(m#/+#, $path); - while (quiet_run(qw/svn ls --non-interactive/, $url)) { - my $n = shift @paths || last; - $url .= "/$n"; + if ($_use_lib) { + while (1) { + $SVN = libsvn_connect($url); + last if (defined $SVN && + defined eval { $SVN->get_latest_revnum }); + my $n = shift @paths || last; + $url .= "/$n"; + } + } else { + while (quiet_run(qw/svn ls --non-interactive/, $url)) { + my $n = shift @paths || last; + $url .= "/$n"; + } } push @repo_path_split_cache, qr/^(\Q$url\E)/; $path = join('/',@paths); @@ -797,6 +1006,7 @@ sub setup_git_svn { } sub assert_svn_wc_clean { + return if $_use_lib; my ($svn_rev) = @_; croak "$svn_rev is not an integer!\n" unless ($svn_rev =~ /^\d+$/); my $lcr = svn_info('.')->{'Last Changed Rev'}; @@ -819,7 +1029,7 @@ sub assert_svn_wc_clean { } } -sub assert_tree { +sub get_tree_from_treeish { my ($treeish) = @_; croak "Not a sha1: $treeish\n" unless $treeish =~ /^$sha1$/o; chomp(my $type = `git-cat-file -t $treeish`); @@ -836,20 +1046,22 @@ sub assert_tree { } else { die "$treeish is a $type, expected tree, tag or commit\n"; } + return $expected; +} + +sub assert_tree { + return if $_use_lib; + my ($treeish) = @_; + my $expected = get_tree_from_treeish($treeish); - 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; + my $old_index = set_index($tmpindex); index_changes(1); chomp(my $tree = `git-write-tree`); - if ($old_index) { - $ENV{GIT_INDEX_FILE} = $old_index; - } else { - delete $ENV{GIT_INDEX_FILE}; - } + restore_index($old_index); if ($tree ne $expected) { croak "Tree mismatch, Got: $tree, Expected: $expected\n"; } @@ -987,7 +1199,8 @@ sub precommit_check { } } -sub svn_checkout_tree { + +sub get_diff { my ($svn_rev, $treeish) = @_; my $from = file_to_s("$REV_DIR/$svn_rev"); assert_tree($from); @@ -1005,11 +1218,13 @@ sub svn_checkout_tree { 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, but SVN doesn't allow it... - return $mods; - } + return parse_diff_tree($diff_fh); +} + +sub svn_checkout_tree { + my ($svn_rev, $treeish) = @_; + my $mods = get_diff($svn_rev, $treeish); + return $mods unless (scalar @$mods); my ($rm, $add) = precommit_check($mods); my %o = ( D => 1, R => 0, C => -1, A => 3, M => 3, T => 3 ); @@ -1052,6 +1267,23 @@ sub svn_checkout_tree { return $mods; } +sub libsvn_checkout_tree { + my ($svn_rev, $treeish, $ed) = @_; + my $mods = get_diff($svn_rev, $treeish); + return $mods unless (scalar @$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) { + my $f = $m->{chg}; + if (defined $o{$f}) { + $ed->$f($m); + } else { + croak "Invalid change type: $f\n"; + } + } + $ed->rmdirs if $_rmdir; + 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) @@ -1090,12 +1322,12 @@ sub handle_rmdir { } } -sub svn_commit_tree { - my ($svn_rev, $commit) = @_; - my $commit_msg = "$GIT_SVN_DIR/.svn-commit.tmp.$$"; +sub get_commit_message { + my ($commit, $commit_msg) = (@_); my %log_msg = ( msg => '' ); open my $msg, '>', $commit_msg or croak $!; + print "commit: $commit\n"; chomp(my $type = `git-cat-file -t $commit`); if ($type eq 'commit') { my $pid = open my $msg_fh, '-|'; @@ -1129,7 +1361,14 @@ sub svn_commit_tree { { local $/; chomp($log_msg{msg} = <$msg>); } close $msg or croak $!; - my ($oneline) = ($log_msg{msg} =~ /([^\n\r]+)/); + return \%log_msg; +} + +sub svn_commit_tree { + my ($svn_rev, $commit) = @_; + my $commit_msg = "$GIT_SVN_DIR/.svn-commit.tmp.$$"; + my $log_msg = get_commit_message($commit, $commit_msg); + my ($oneline) = ($log_msg->{msg} =~ /([^\n\r]+)/); print "Committing $commit: $oneline\n"; if (defined $LC_ALL) { @@ -1165,12 +1404,12 @@ sub svn_commit_tree { /(\d{4})\-(\d\d)\-(\d\d)\s (\d\d)\:(\d\d)\:(\d\d)\s([\-\+]\d+)/x) or croak "Failed to parse date: $date\n"; - $log_msg{date} = "$tz $Y-$m-$d $H:$M:$S"; - $log_msg{author} = $info->{'Last Changed Author'}; - $log_msg{revision} = $committed; - $log_msg{msg} .= "\n"; + $log_msg->{date} = "$tz $Y-$m-$d $H:$M:$S"; + $log_msg->{author} = $info->{'Last Changed Author'}; + $log_msg->{revision} = $committed; + $log_msg->{msg} .= "\n"; my $parent = file_to_s("$REV_DIR/$svn_rev"); - git_commit(\%log_msg, $parent, $commit); + git_commit($log_msg, $parent, $commit); return $committed; } # resync immediately @@ -1335,8 +1574,14 @@ sub eol_cp { binmode $rfd or croak $!; open my $wfd, '>', $to or croak $!; binmode $wfd or croak $!; + eol_cp_fd($rfd, $wfd, $es); + close $rfd or croak $!; + close $wfd or croak $!; +} - my $eol = $EOL{$es} or undef; +sub eol_cp_fd { + my ($rfd, $wfd, $es) = @_; + my $eol = defined $es ? $EOL{$es} : undef; my $buf; use bytes; while (1) { @@ -1396,6 +1641,7 @@ sub do_update_index { } sub index_changes { + return if $_use_lib; my $no_text_base = shift; do_update_index([qw/git-diff-files --name-only -z/], 'remove', @@ -1459,63 +1705,59 @@ sub assert_revision_eq_or_unknown { sub git_commit { my ($log_msg, @parents) = @_; assert_revision_unknown($log_msg->{revision}); - my $out_fh = IO::File->new_tmpfile or croak $!; - map_tree_joins() if (@_branch_from && !%tree_map); + my (@tmp_parents, @exec_parents, %seen_parent); + if (my $lparents = $log_msg->{parents}) { + @tmp_parents = @$lparents + } # commit parents can be conditionally bound to a particular # svn revision via: "svn_revno=commit_sha1", filter them out here: - my @exec_parents; foreach my $p (@parents) { next unless defined $p; if ($p =~ /^(\d+)=($sha1_short)$/o) { if ($1 == $log_msg->{revision}) { - push @exec_parents, $2; + push @tmp_parents, $2; } } else { - push @exec_parents, $p if $p =~ /$sha1_short/o; + push @tmp_parents, $p if $p =~ /$sha1_short/o; } } - - my $pid = fork; - defined $pid or croak $!; - if ($pid == 0) { - $ENV{GIT_INDEX_FILE} = $GIT_SVN_INDEX; + my $tree = $log_msg->{tree}; + if (!defined $tree) { + my $index = set_index($GIT_SVN_INDEX); index_changes(); - chomp(my $tree = `git-write-tree`); + chomp($tree = `git-write-tree`); croak $? if $?; - if (exists $tree_map{$tree}) { - my %seen_parent = map { $_ => 1 } @exec_parents; - foreach (@{$tree_map{$tree}}) { - # MAXPARENT is defined to 16 in commit-tree.c: - if ($seen_parent{$_} || @exec_parents > 16) { - next; - } - push @exec_parents, $_; - $seen_parent{$_} = 1; - } - } + restore_index($index); + } + if (exists $tree_map{$tree}) { + push @tmp_parents, @{$tree_map{$tree}}; + } + foreach (@tmp_parents) { + next if $seen_parent{$_}; + $seen_parent{$_} = 1; + push @exec_parents, $_; + # MAXPARENT is defined to 16 in commit-tree.c: + last if @exec_parents > 16; + } + + defined(my $pid = open my $out_fh, '-|') or croak $!; + if ($pid == 0) { my $msg_fh = IO::File->new_tmpfile or croak $!; print $msg_fh $log_msg->{msg}, "\ngit-svn-id: ", "$SVN_URL\@$log_msg->{revision}", " $SVN_UUID\n" or croak $!; $msg_fh->flush == 0 or croak $!; seek $msg_fh, 0, 0 or croak $!; - set_commit_env($log_msg); - my @exec = ('git-commit-tree',$tree); push @exec, '-p', $_ foreach @exec_parents; open STDIN, '<&', $msg_fh or croak $!; - open STDOUT, '>&', $out_fh or croak $!; exec @exec or croak $!; } - waitpid($pid,0); - croak $? if $?; - - $out_fh->flush == 0 or croak $!; - seek $out_fh, 0, 0 or croak $!; chomp(my $commit = do { local $/; <$out_fh> }); + close $out_fh or croak $?; if ($commit !~ /^$sha1$/o) { croak "Failed to commit, invalid sha1: $commit\n"; } @@ -1534,6 +1776,7 @@ sub git_commit { } sys(@update_ref); sys('git-update-ref',"svn/$GIT_SVN/revs/$log_msg->{revision}",$commit); + # this output is read via pipe, do not change: print "r$log_msg->{revision} = $commit\n"; if ($_repack && (--$_repack_nr == 0)) { $_repack_nr = $_repack; @@ -1545,6 +1788,9 @@ sub git_commit { sub set_commit_env { my ($log_msg) = @_; my $author = $log_msg->{author}; + if (!defined $author || length $author == 0) { + $author = '(no author)'; + } my ($name,$email) = defined $users{$author} ? @{$users{$author}} : ($author,"$author\@$SVN_UUID"); $ENV{GIT_AUTHOR_NAME} = $ENV{GIT_COMMITTER_NAME} = $name; @@ -2029,6 +2275,612 @@ sub show_commit_normal { } } +sub libsvn_load { + return unless $_use_lib; + $_use_lib = eval { + require SVN::Core; + if ($SVN::Core::VERSION lt '1.2.1') { + die "Need SVN::Core 1.2.1 or better ", + "(got $SVN::Core::VERSION) ", + "Falling back to command-line svn\n"; + } + require SVN::Ra; + require SVN::Delta; + push @SVN::Git::Editor::ISA, 'SVN::Delta::Editor'; + my $kill_stupid_warnings = $SVN::Node::none.$SVN::Node::file. + $SVN::Node::dir.$SVN::Node::unknown. + $SVN::Node::none.$SVN::Node::file. + $SVN::Node::dir.$SVN::Node::unknown; + 1; + }; +} + +sub libsvn_connect { + my ($url) = @_; + my $auth = SVN::Core::auth_open([SVN::Client::get_simple_provider(), + SVN::Client::get_ssl_server_trust_file_provider(), + SVN::Client::get_username_provider()]); + my $s = eval { SVN::Ra->new(url => $url, auth => $auth) }; + return $s; +} + +sub libsvn_get_file { + my ($gui, $f, $rev) = @_; + my $p = $f; + return unless ($p =~ s#^\Q$SVN_PATH\E/?##); + + my $fd = IO::File->new_tmpfile or croak $!; + my $pool = SVN::Pool->new; + my ($r, $props) = $SVN->get_file($f, $rev, $fd, $pool); + $pool->clear; + $fd->flush == 0 or croak $!; + seek $fd, 0, 0 or croak $!; + if (my $es = $props->{'svn:eol-style'}) { + my $new_fd = IO::File->new_tmpfile or croak $!; + eol_cp_fd($fd, $new_fd, $es); + close $fd or croak $!; + $fd = $new_fd; + seek $fd, 0, 0 or croak $!; + $fd->flush == 0 or croak $!; + } + my $mode = '100644'; + if (exists $props->{'svn:executable'}) { + $mode = '100755'; + } + if (exists $props->{'svn:special'}) { + $mode = '120000'; + local $/; + my $link = <$fd>; + $link =~ s/^link // or die "svn:special file with contents: <", + $link, "> is not understood\n"; + seek $fd, 0, 0 or croak $!; + truncate $fd, 0 or croak $!; + print $fd $link or croak $!; + seek $fd, 0, 0 or croak $!; + $fd->flush == 0 or croak $!; + } + my $pid = open my $ho, '-|'; + defined $pid or croak $!; + if (!$pid) { + open STDIN, '<&', $fd or croak $!; + exec qw/git-hash-object -w --stdin/ or croak $!; + } + chomp(my $hash = do { local $/; <$ho> }); + close $ho or croak $?; + $hash =~ /^$sha1$/o or die "not a sha1: $hash\n"; + print $gui $mode,' ',$hash,"\t",$p,"\0" or croak $!; + close $fd or croak $!; +} + +sub libsvn_log_entry { + my ($rev, $author, $date, $msg, $parents) = @_; + my ($Y,$m,$d,$H,$M,$S) = ($date =~ /^(\d{4})\-(\d\d)\-(\d\d)T + (\d\d)\:(\d\d)\:(\d\d).\d+Z$/x) + or die "Unable to parse date: $date\n"; + if (defined $_authors && ! defined $users{$author}) { + die "Author: $author not defined in $_authors file\n"; + } + return { revision => $rev, date => "+0000 $Y-$m-$d $H:$M:$S", + author => $author, msg => $msg."\n", parents => $parents || [] } +} + +sub process_rm { + my ($gui, $last_commit, $f) = @_; + $f =~ s#^\Q$SVN_PATH\E/?## or return; + # remove entire directories. + if (safe_qx('git-ls-tree',$last_commit,'--',$f) =~ /^040000 tree/) { + defined(my $pid = open my $ls, '-|') or croak $!; + if (!$pid) { + exec(qw/git-ls-tree -r --name-only -z/, + $last_commit,'--',$f) or croak $!; + } + local $/ = "\0"; + while (<$ls>) { + print $gui '0 ',0 x 40,"\t",$_ or croak $!; + } + close $ls or croak $!; + } else { + print $gui '0 ',0 x 40,"\t",$f,"\0" or croak $!; + } +} + +sub libsvn_fetch { + my ($last_commit, $paths, $rev, $author, $date, $msg) = @_; + open my $gui, '| git-update-index -z --index-info' or croak $!; + my @amr; + foreach my $f (keys %$paths) { + my $m = $paths->{$f}->action(); + $f =~ s#^/+##; + if ($m =~ /^[DR]$/) { + process_rm($gui, $last_commit, $f); + next if $m eq 'D'; + # 'R' can be file replacements, too, right? + } + my $pool = SVN::Pool->new; + my $t = $SVN->check_path($f, $rev, $pool); + if ($t == $SVN::Node::file) { + if ($m =~ /^[AMR]$/) { + push @amr, $f; + } else { + die "Unrecognized action: $m, ($f r$rev)\n"; + } + } + $pool->clear; + } + libsvn_get_file($gui, $_, $rev) foreach (@amr); + close $gui or croak $!; + return libsvn_log_entry($rev, $author, $date, $msg, [$last_commit]); +} + +sub svn_grab_base_rev { + defined(my $pid = open my $fh, '-|') or croak $!; + if (!$pid) { + open my $null, '>', '/dev/null' or croak $!; + open STDERR, '>&', $null or croak $!; + exec qw/git-rev-parse --verify/,"refs/remotes/$GIT_SVN^0" + or croak $!; + } + chomp(my $c = do { local $/; <$fh> }); + close $fh; + if (defined $c && length $c) { + my ($url, $rev, $uuid) = extract_metadata((grep(/^git-svn-id: /, + safe_qx(qw/git-cat-file commit/, $c)))[0]); + return ($rev, $c); + } + return (undef, undef); +} + +sub libsvn_parse_revision { + my $base = shift; + my $head = $SVN->get_latest_revnum(); + if (!defined $_revision || $_revision eq 'BASE:HEAD') { + return ($base + 1, $head) if (defined $base); + return (0, $head); + } + return ($1, $2) if ($_revision =~ /^(\d+):(\d+)$/); + return ($_revision, $_revision) if ($_revision =~ /^\d+$/); + if ($_revision =~ /^BASE:(\d+)$/) { + return ($base + 1, $1) if (defined $base); + return (0, $head); + } + return ($1, $head) if ($_revision =~ /^(\d+):HEAD$/); + die "revision argument: $_revision not understood by git-svn\n", + "Try using the command-line svn client instead\n"; +} + +sub libsvn_traverse { + my ($gui, $pfx, $path, $rev) = @_; + my $cwd = "$pfx/$path"; + my $pool = SVN::Pool->new; + $cwd =~ s#^/+##g; + my ($dirent, $r, $props) = $SVN->get_dir($cwd, $rev, $pool); + foreach my $d (keys %$dirent) { + my $t = $dirent->{$d}->kind; + if ($t == $SVN::Node::dir) { + libsvn_traverse($gui, $cwd, $d, $rev); + } elsif ($t == $SVN::Node::file) { + libsvn_get_file($gui, "$cwd/$d", $rev); + } + } + $pool->clear; +} + +sub libsvn_traverse_ignore { + my ($fh, $path, $r) = @_; + $path =~ s#^/+##g; + my $pool = SVN::Pool->new; + my ($dirent, undef, $props) = $SVN->get_dir($path, $r, $pool); + my $p = $path; + $p =~ s#^\Q$SVN_PATH\E/?##; + print $fh length $p ? "\n# $p\n" : "\n# /\n"; + if (my $s = $props->{'svn:ignore'}) { + $s =~ s/[\r\n]+/\n/g; + chomp $s; + if (length $p == 0) { + $s =~ s#\n#\n/$p#g; + print $fh "/$s\n"; + } else { + $s =~ s#\n#\n/$p/#g; + print $fh "/$p/$s\n"; + } + } + foreach (sort keys %$dirent) { + next if $dirent->{$_}->kind != $SVN::Node::dir; + libsvn_traverse_ignore($fh, "$path/$_", $r); + } + $pool->clear; +} + +sub libsvn_new_tree { + my ($paths, $rev, $author, $date, $msg) = @_; + my $svn_path = '/'.$SVN_PATH; + + # look for a parent from another branch: + foreach (keys %$paths) { + next if ($_ ne $svn_path); + my $i = $paths->{$_}; + my $branch_from = $i->copyfrom_path or next; + my $r = $i->copyfrom_rev; + print STDERR "Found possible branch point: ", + "$branch_from => $svn_path, $r\n"; + $branch_from =~ s#^/##; + my $l_map = read_url_paths(); + my $url = $SVN->{url}; + defined $l_map->{$url} or next; + my $id = $l_map->{$url}->{$branch_from} or next; + my $f = "$GIT_DIR/svn/$id/revs/$r"; + while ($r && !-r $f) { + $r--; + $f = "$GIT_DIR/svn/$id/revs/$r"; + } + if (-r $f) { + my $parent = file_to_s($f); + unlink $GIT_SVN_INDEX; + print STDERR "Found branch parent: $parent\n"; + sys(qw/git-read-tree/, $parent); + return libsvn_fetch($parent, $paths, $rev, + $author, $date, $msg); + } + print STDERR "Nope, branch point not imported or unknown\n"; + } + open my $gui, '| git-update-index -z --index-info' or croak $!; + my $pool = SVN::Pool->new; + libsvn_traverse($gui, '', $SVN_PATH, $rev, $pool); + $pool->clear; + close $gui or croak $!; + return libsvn_log_entry($rev, $author, $date, $msg); +} + +sub find_graft_path_commit { + my ($tree_paths, $p1, $r1) = @_; + foreach my $x (keys %$tree_paths) { + next unless ($p1 =~ /^\Q$x\E/); + my $i = $tree_paths->{$x}; + my $f = "$GIT_DIR/svn/$i/revs/$r1"; + + return file_to_s($f) if (-r $f); + + print STDERR "r$r1 of $i not imported\n"; + next; + } + return undef; +} + +sub find_graft_path_parents { + my ($grafts, $tree_paths, $c, $p0, $r0) = @_; + foreach my $x (keys %$tree_paths) { + next unless ($p0 =~ /^\Q$x\E/); + my $i = $tree_paths->{$x}; + my $f = "$GIT_DIR/svn/$i/revs/$r0"; + while ($r0 && !-r $f) { + # could be an older revision, too... + $r0--; + $f = "$GIT_DIR/svn/$i/revs/$r0"; + } + unless (-r $f) { + print STDERR "r$r0 of $i not imported\n"; + next; + } + my $parent = file_to_s($f); + $grafts->{$c}->{$parent} = 1; + } +} + +sub libsvn_graft_file_copies { + my ($grafts, $tree_paths, $path, $paths, $rev) = @_; + foreach (keys %$paths) { + my $i = $paths->{$_}; + my ($m, $p0, $r0) = ($i->action, $i->copyfrom_path, + $i->copyfrom_rev); + next unless (defined $p0 && defined $r0); + + my $p1 = $_; + $p1 =~ s#^/##; + $p0 =~ s#^/##; + my $c = find_graft_path_commit($tree_paths, $p1, $rev); + next unless $c; + find_graft_path_parents($grafts, $tree_paths, $c, $p0, $r0); + } +} + +sub set_index { + my $old = $ENV{GIT_INDEX_FILE}; + $ENV{GIT_INDEX_FILE} = shift; + return $old; +} + +sub restore_index { + my ($old) = @_; + if (defined $old) { + $ENV{GIT_INDEX_FILE} = $old; + } else { + delete $ENV{GIT_INDEX_FILE}; + } +} + +sub libsvn_commit_cb { + my ($rev, $date, $committer, $c, $msg, $r_last, $cmt_last) = @_; + if ($rev == ($r_last + 1)) { + # optimized (avoid fetch) + my $log = libsvn_log_entry($rev,$committer,$date,$msg); + $log->{tree} = get_tree_from_treeish($c); + my $cmt = git_commit($log, $cmt_last, $c); + my @diff = safe_qx('git-diff-tree', $cmt, $c); + if (@diff) { + print STDERR "Trees differ: $cmt $c\n", + join('',@diff),"\n"; + exit 1; + } + } else { + fetch_lib("$rev=$c"); + } +} + +sub libsvn_ls_fullurl { + my $fullurl = shift; + my ($repo, $path) = repo_path_split($fullurl); + $SVN ||= libsvn_connect($repo); + my @ret; + my $pool = SVN::Pool->new; + my ($dirent, undef, undef) = $SVN->get_dir($path, + $SVN->get_latest_revnum, $pool); + foreach my $d (keys %$dirent) { + if ($dirent->{$d}->kind == $SVN::Node::dir) { + push @ret, "$d/"; # add '/' for compat with cli svn + } + } + $pool->clear; + return @ret; +} + + +sub libsvn_skip_unknown_revs { + my $err = shift; + my $errno = $err->apr_err(); + # Maybe the branch we're tracking didn't + # exist when the repo started, so it's + # not an error if it doesn't, just continue + # + # Wonderfully consistent library, eh? + # 160013 - svn:// and file:// + # 175002 - http(s):// + # More codes may be discovered later... + if ($errno == 175002 || $errno == 160013) { + print STDERR "directory non-existent\n"; + return; + } + croak "Error from SVN, ($errno): ", $err->expanded_message,"\n"; +}; + +package SVN::Git::Editor; +use vars qw/@ISA/; +use strict; +use warnings; +use Carp qw/croak/; +use IO::File; + +sub new { + my $class = shift; + my $git_svn = shift; + my $self = SVN::Delta::Editor->new(@_); + bless $self, $class; + foreach (qw/svn_path c r ra /) { + die "$_ required!\n" unless (defined $git_svn->{$_}); + $self->{$_} = $git_svn->{$_}; + } + $self->{pool} = SVN::Pool->new; + $self->{bat} = { '' => $self->open_root($self->{r}, $self->{pool}) }; + $self->{rm} = { }; + require Digest::MD5; + return $self; +} + +sub split_path { + return ($_[0] =~ m#^(.*?)/?([^/]+)$#); +} + +sub repo_path { + (defined $_[1] && length $_[1]) ? "$_[0]->{svn_path}/$_[1]" + : $_[0]->{svn_path} +} + +sub url_path { + my ($self, $path) = @_; + $self->{ra}->{url} . '/' . $self->repo_path($path); +} + +sub rmdirs { + my ($self) = @_; + my $rm = $self->{rm}; + delete $rm->{''}; # we never delete the url we're tracking + return unless %$rm; + + foreach (keys %$rm) { + my @d = split m#/#, $_; + my $c = shift @d; + $rm->{$c} = 1; + while (@d) { + $c .= '/' . shift @d; + $rm->{$c} = 1; + } + } + delete $rm->{$self->{svn_path}}; + delete $rm->{''}; # we never delete the url we're tracking + return unless %$rm; + + defined(my $pid = open my $fh,'-|') or croak $!; + if (!$pid) { + exec qw/git-ls-tree --name-only -r -z/, $self->{c} or croak $!; + } + local $/ = "\0"; + while (<$fh>) { + chomp; + $_ = $self->{svn_path} . '/' . $_; + my ($dn) = ($_ =~ m#^(.*?)/?(?:[^/]+)$#); + delete $rm->{$dn}; + last unless %$rm; + } + my ($r, $p, $bat) = ($self->{r}, $self->{pool}, $self->{bat}); + foreach my $d (sort { $b =~ tr#/#/# <=> $a =~ tr#/#/# } keys %$rm) { + $self->close_directory($bat->{$d}, $p); + my ($dn) = ($d =~ m#^(.*?)/?(?:[^/]+)$#); + $self->SUPER::delete_entry($d, $r, $bat->{$dn}, $p); + delete $bat->{$d}; + } +} + +sub open_or_add_dir { + my ($self, $full_path, $baton) = @_; + my $p = SVN::Pool->new; + my $t = $self->{ra}->check_path($full_path, $self->{r}, $p); + $p->clear; + if ($t == $SVN::Node::none) { + return $self->add_directory($full_path, $baton, + undef, -1, $self->{pool}); + } elsif ($t == $SVN::Node::dir) { + return $self->open_directory($full_path, $baton, + $self->{r}, $self->{pool}); + } + print STDERR "$full_path already exists in repository at ", + "r$self->{r} and it is not a directory (", + ($t == $SVN::Node::file ? 'file' : 'unknown'),"/$t)\n"; + exit 1; +} + +sub ensure_path { + my ($self, $path) = @_; + my $bat = $self->{bat}; + $path = $self->repo_path($path); + return $bat->{''} unless (length $path); + my @p = split m#/+#, $path; + my $c = shift @p; + $bat->{$c} ||= $self->open_or_add_dir($c, $bat->{''}); + while (@p) { + my $c0 = $c; + $c .= '/' . shift @p; + $bat->{$c} ||= $self->open_or_add_dir($c, $bat->{$c0}); + } + return $bat->{$c}; +} + +sub A { + my ($self, $m) = @_; + my ($dir, $file) = split_path($m->{file_b}); + my $pbat = $self->ensure_path($dir); + my $fbat = $self->add_file($self->repo_path($m->{file_b}), $pbat, + undef, -1); + $self->chg_file($fbat, $m); + $self->close_file($fbat,undef,$self->{pool}); +} + +sub C { + my ($self, $m) = @_; + my ($dir, $file) = split_path($m->{file_b}); + my $pbat = $self->ensure_path($dir); + my $fbat = $self->add_file($self->repo_path($m->{file_b}), $pbat, + $self->url_path($m->{file_a}), $self->{r}); + $self->chg_file($fbat, $m); + $self->close_file($fbat,undef,$self->{pool}); +} + +sub delete_entry { + my ($self, $path, $pbat) = @_; + my $rpath = $self->repo_path($path); + my ($dir, $file) = split_path($rpath); + $self->{rm}->{$dir} = 1; + $self->SUPER::delete_entry($rpath, $self->{r}, $pbat, $self->{pool}); +} + +sub R { + my ($self, $m) = @_; + my ($dir, $file) = split_path($m->{file_b}); + my $pbat = $self->ensure_path($dir); + my $fbat = $self->add_file($self->repo_path($m->{file_b}), $pbat, + $self->url_path($m->{file_a}), $self->{r}); + $self->chg_file($fbat, $m); + $self->close_file($fbat,undef,$self->{pool}); + + ($dir, $file) = split_path($m->{file_a}); + $pbat = $self->ensure_path($dir); + $self->delete_entry($m->{file_a}, $pbat); +} + +sub M { + my ($self, $m) = @_; + my ($dir, $file) = split_path($m->{file_b}); + my $pbat = $self->ensure_path($dir); + my $fbat = $self->open_file($self->repo_path($m->{file_b}), + $pbat,$self->{r},$self->{pool}); + $self->chg_file($fbat, $m); + $self->close_file($fbat,undef,$self->{pool}); +} + +sub T { shift->M(@_) } + +sub change_file_prop { + my ($self, $fbat, $pname, $pval) = @_; + $self->SUPER::change_file_prop($fbat, $pname, $pval, $self->{pool}); +} + +sub chg_file { + my ($self, $fbat, $m) = @_; + if ($m->{mode_b} =~ /755$/ && $m->{mode_a} !~ /755$/) { + $self->change_file_prop($fbat,'svn:executable','*'); + } elsif ($m->{mode_b} !~ /755$/ && $m->{mode_a} =~ /755$/) { + $self->change_file_prop($fbat,'svn:executable',undef); + } + my $fh = IO::File->new_tmpfile or croak $!; + if ($m->{mode_b} =~ /^120/) { + print $fh 'link ' or croak $!; + $self->change_file_prop($fbat,'svn:special','*'); + } elsif ($m->{mode_a} =~ /^120/ && $m->{mode_b} !~ /^120/) { + $self->change_file_prop($fbat,'svn:special',undef); + } + defined(my $pid = fork) or croak $!; + if (!$pid) { + open STDOUT, '>&', $fh or croak $!; + exec qw/git-cat-file blob/, $m->{sha1_b} or croak $!; + } + waitpid $pid, 0; + croak $? if $?; + $fh->flush == 0 or croak $!; + seek $fh, 0, 0 or croak $!; + + my $md5 = Digest::MD5->new; + $md5->addfile($fh) or croak $!; + seek $fh, 0, 0 or croak $!; + + my $exp = $md5->hexdigest; + my $atd = $self->apply_textdelta($fbat, undef, $self->{pool}); + my $got = SVN::TxDelta::send_stream($fh, @$atd, $self->{pool}); + die "Checksum mismatch\nexpected: $exp\ngot: $got\n" if ($got ne $exp); + + close $fh or croak $!; +} + +sub D { + my ($self, $m) = @_; + my ($dir, $file) = split_path($m->{file_b}); + my $pbat = $self->ensure_path($dir); + $self->delete_entry($m->{file_b}, $pbat); +} + +sub close_edit { + my ($self) = @_; + my ($p,$bat) = ($self->{pool}, $self->{bat}); + foreach (sort { $b =~ tr#/#/# <=> $a =~ tr#/#/# } keys %$bat) { + $self->close_directory($bat->{$_}, $p); + } + $self->SUPER::close_edit($p); + $p->clear; +} + +sub abort_edit { + my ($self) = @_; + $self->SUPER::abort_edit($self->{pool}); + $self->{pool}->clear; +} + __END__ Data structures: @@ -2062,3 +2914,7 @@ diff-index line ($m hash) file_b => new/current file name of a file (any chg) } ; + +Notes: + I don't trust the each() function on unless I created %hash myself + because the internal iterator may not have started at base. diff --git a/contrib/git-svn/t/lib-git-svn.sh b/contrib/git-svn/t/lib-git-svn.sh index 58408a6c07e..2843258fc47 100644 --- a/contrib/git-svn/t/lib-git-svn.sh +++ b/contrib/git-svn/t/lib-git-svn.sh @@ -11,7 +11,7 @@ fi GIT_DIR=$PWD/.git GIT_SVN_DIR=$GIT_DIR/svn/git-svn -SVN_TREE=$GIT_SVN_DIR/tree +SVN_TREE=$GIT_SVN_DIR/svn-tree svnadmin >/dev/null 2>&1 if test $? != 1 diff --git a/contrib/git-svn/t/t0000-contrib-git-svn.sh b/contrib/git-svn/t/t0000-contrib-git-svn.sh index 0c6ff2066b7..c33b522d083 100644 --- a/contrib/git-svn/t/t0000-contrib-git-svn.sh +++ b/contrib/git-svn/t/t0000-contrib-git-svn.sh @@ -31,6 +31,7 @@ test_expect_success \ 'import an SVN revision into git' \ 'git-svn fetch' +test_expect_success "checkout from svn" "svn co $svnrepo $SVN_TREE" name='try a deep --rmdir with a commit' git checkout -f -b mybranch remotes/git-svn @@ -41,6 +42,7 @@ git commit -m "$name" test_expect_success "$name" \ "git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch && + svn up $SVN_TREE && test -d $SVN_TREE/dir && test ! -d $SVN_TREE/dir/a" @@ -52,7 +54,7 @@ git update-index --remove dir/file git update-index --add dir/file/file git commit -m "$name" -test_expect_code 1 "$name" \ +test_expect_failure "$name" \ 'git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch' \ || true @@ -67,7 +69,7 @@ git update-index --remove -- bar/zzz git update-index --add -- bar git commit -m "$name" -test_expect_code 1 "$name" \ +test_expect_failure "$name" \ 'git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch2' \ || true @@ -82,7 +84,7 @@ echo yyy > bar/zzz/yyy git-update-index --add bar/zzz/yyy git commit -m "$name" -test_expect_code 1 "$name" \ +test_expect_failure "$name" \ 'git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch3' \ || true @@ -97,7 +99,7 @@ echo asdf > dir git update-index --add -- dir git commit -m "$name" -test_expect_code 1 "$name" \ +test_expect_failure "$name" \ 'git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch4' \ || true @@ -111,6 +113,7 @@ git commit -m "$name" test_expect_success "$name" \ "git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch5 && + svn up $SVN_TREE && test ! -x $SVN_TREE/exec.sh" @@ -121,6 +124,7 @@ git commit -m "$name" test_expect_success "$name" \ "git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch5 && + svn up $SVN_TREE && test -x $SVN_TREE/exec.sh" @@ -133,6 +137,7 @@ git commit -m "$name" test_expect_success "$name" \ "git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch5 && + svn up $SVN_TREE && test -L $SVN_TREE/exec.sh" @@ -145,6 +150,7 @@ git commit -m "$name" test_expect_success "$name" \ "git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch5 && + svn up $SVN_TREE && test -x $SVN_TREE/bar/zzz && test -L $SVN_TREE/exec-2.sh" @@ -159,6 +165,7 @@ git commit -m "$name" test_expect_success "$name" \ "git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch5 && + svn up $SVN_TREE && test -f $SVN_TREE/exec-2.sh && test ! -L $SVN_TREE/exec-2.sh && diff -u help $SVN_TREE/exec-2.sh" From 42d328701dbdbc02c3361673629a44df478e69d7 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Tue, 13 Jun 2006 04:02:23 -0700 Subject: [PATCH 19/43] git-svn: make the $GIT_DIR/svn/*/revs directory obsolete This is a very intrusive change, so I've beefed up the tests significantly. Added 'full-test' a target to the Makefile, to test different possible configurations. This is intended for maintainers only. Users should only be concerned with 'test' succeeding. We now have a very simple custom database format for handling mapping of svn revisions => git commits. Of course, we're not really using it yet, either. Also disabled automatic branch-finding on new trees for now. It's too easily broken. revisions_eq() function should be helpful for branch detection. Also removed an extra assertion in fetch_cmd() that wasn't correctly done. This bug was found by full-test. Signed-off-by: Eric Wong --- contrib/git-svn/Makefile | 12 +- contrib/git-svn/git-svn.perl | 245 +++++++++++------- contrib/git-svn/t/t0000-contrib-git-svn.sh | 13 + .../git-svn/t/t0001-contrib-git-svn-props.sh | 84 +++--- 4 files changed, 223 insertions(+), 131 deletions(-) diff --git a/contrib/git-svn/Makefile b/contrib/git-svn/Makefile index 48f60b3a0da..d73aa5641c9 100644 --- a/contrib/git-svn/Makefile +++ b/contrib/git-svn/Makefile @@ -29,8 +29,16 @@ git-svn.html : git-svn.txt asciidoc -b xhtml11 -d manpage \ -f ../../Documentation/asciidoc.conf $< test: git-svn - cd t && $(SHELL) ./t0000-contrib-git-svn.sh - cd t && $(SHELL) ./t0001-contrib-git-svn-props.sh + cd t && $(SHELL) ./t0000-contrib-git-svn.sh $(TEST_FLAGS) + cd t && $(SHELL) ./t0001-contrib-git-svn-props.sh $(TEST_FLAGS) + +full-test: + $(MAKE) test GIT_SVN_NO_LIB=1 GIT_SVN_NO_OPTIMIZE_COMMITS=1 + $(MAKE) test GIT_SVN_NO_LIB=0 GIT_SVN_NO_OPTIMIZE_COMMITS=1 + $(MAKE) test GIT_SVN_NO_LIB=1 GIT_SVN_NO_OPTIMIZE_COMMITS=0 \ + LC_ALL=en_US.UTF-8 + $(MAKE) test GIT_SVN_NO_LIB=0 GIT_SVN_NO_OPTIMIZE_COMMITS=0 \ + LC_ALL=en_US.UTF-8 clean: rm -f git-svn *.xml *.html *.1 diff --git a/contrib/git-svn/git-svn.perl b/contrib/git-svn/git-svn.perl index 9618c8bab52..884969ebdd5 100755 --- a/contrib/git-svn/git-svn.perl +++ b/contrib/git-svn/git-svn.perl @@ -6,9 +6,9 @@ use strict; use vars qw/ $AUTHOR $VERSION $SVN_URL $SVN_INFO $SVN_WC $SVN_UUID $GIT_SVN_INDEX $GIT_SVN - $GIT_DIR $REV_DIR $GIT_SVN_DIR/; + $GIT_DIR $GIT_SVN_DIR $REVDB/; $AUTHOR = 'Eric Wong '; -$VERSION = '1.1.0-pre'; +$VERSION = '1.1.1-broken'; use Cwd qw/abs_path/; $GIT_DIR = abs_path($ENV{GIT_DIR} || '.git'); @@ -31,10 +31,13 @@ use File::Path qw/mkpath/; use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev pass_through/; use File::Spec qw//; use POSIX qw/strftime/; +use Memoize; +memoize('revisions_eq'); my ($SVN_PATH, $SVN, $SVN_LOG, $_use_lib); $_use_lib = 1 unless $ENV{GIT_SVN_NO_LIB}; libsvn_load(); +my $_optimize_commits = 1 unless $ENV{GIT_SVN_NO_OPTIMIZE_COMMITS}; my $sha1 = qr/[a-f\d]{40}/; my $sha1_short = qr/[a-f\d]{4,40}/; my ($_revision,$_stdin,$_no_ignore_ext,$_no_stop_copy,$_help,$_rmdir,$_edit, @@ -43,7 +46,7 @@ my ($_revision,$_stdin,$_no_ignore_ext,$_no_stop_copy,$_help,$_rmdir,$_edit, $_template, $_shared, $_no_default_regex, $_no_graft_copy, $_limit, $_verbose, $_incremental, $_oneline, $_l_fmt, $_show_commit, $_version, $_upgrade, $_authors, $_branch_all_refs, @_opt_m); -my (@_branch_from, %tree_map, %users, %rusers); +my (@_branch_from, %tree_map, %users, %rusers, %equiv); my ($_svn_co_url_revs, $_svn_pg_peg_revs); my @repo_path_split_cache; @@ -201,7 +204,6 @@ sub rebuild { next if (defined $SVN_UUID && ($uuid ne $SVN_UUID)); next if (defined $SVN_URL && defined $url && ($url ne $SVN_URL)); - print "r$rev = $c\n"; unless (defined $latest) { if (!$SVN_URL && !$url) { croak "SVN repository location required: $url\n"; @@ -211,8 +213,8 @@ sub rebuild { setup_git_svn(); $latest = $rev; } - assert_revision_eq_or_unknown($rev, $c); - sys('git-update-ref',"svn/$GIT_SVN/revs/$rev",$c); + revdb_set($REVDB, $rev, $c); + print "r$rev = $c\n"; $newest_rev = $rev if ($rev > $newest_rev); } close $rev_list or croak $?; @@ -280,7 +282,11 @@ sub fetch_cmd { my $svn_log = svn_log_raw(@log_args); my $base = next_log_entry($svn_log) or croak "No base revision!\n"; - my $last_commit = undef; + # don't need last_revision from grab_base_rev() because + # user could've specified a different revision to skip (they + # didn't want to import certain revisions into git for whatever + # reason, so trust $base->{revision} instead. + my (undef, $last_commit) = svn_grab_base_rev(); unless (-d $SVN_WC) { svn_cmd_checkout($SVN_URL,$base->{revision},$SVN_WC); chdir $SVN_WC or croak $!; @@ -290,7 +296,6 @@ sub fetch_cmd { } else { chdir $SVN_WC or croak $!; read_uuid(); - eval { $last_commit = file_to_s("$REV_DIR/$base->{revision}") }; # looks like a user manually cp'd and svn switch'ed unless ($last_commit) { sys(qw/svn revert -R ./); @@ -303,7 +308,6 @@ sub fetch_cmd { push @svn_up, '--ignore-externals' unless $_no_ignore_ext; my $last = $base; while (my $log_msg = next_log_entry($svn_log)) { - assert_tree($last_commit); if ($last->{revision} >= $log_msg->{revision}) { croak "Out of order: last >= current: ", "$last->{revision} >= $log_msg->{revision}\n"; @@ -444,14 +448,14 @@ sub commit_cmd { } $info = svn_info('.'); read_uuid($info); - my $svn_current_rev = $info->{'Last Changed Rev'}; + my $last = $fetched; foreach my $c (@revs) { - my $mods = svn_checkout_tree($svn_current_rev, $c); + my $mods = svn_checkout_tree($last, $c); if (scalar @$mods == 0) { print "Skipping, no changes detected\n"; next; } - $svn_current_rev = svn_commit_tree($svn_current_rev, $c); + $last = svn_commit_tree($last, $c); } } @@ -500,7 +504,7 @@ sub commit_lib { }, @lock) ); - my $mods = libsvn_checkout_tree($r_last, $c, $ed); + my $mods = libsvn_checkout_tree($cmt_last, $c, $ed); if (@$mods == 0) { print "No changes\nr$r_last = $cmt_last\n"; $ed->abort_edit; @@ -814,7 +818,7 @@ sub graft_file_copy_cmd { my ($grafts, $l_map, $u) = @_; my $paths = $l_map->{$u}; my $pfx = common_prefix([keys %$paths]); - + $SVN_URL ||= $u.$pfx; my $pid = open my $fh, '-|'; defined $pid or croak $!; unless ($pid) { @@ -851,6 +855,8 @@ sub graft_file_copy_lib { my ($base, $head) = libsvn_parse_revision(); my $inc = 1000; my ($min, $max) = ($base, $head < $base+$inc ? $head : $base+$inc); + my $eh = $SVN::Error::handler; + $SVN::Error::handler = \&libsvn_skip_unknown_revs; while (1) { my $pool = SVN::Pool->new; $SVN_LOG->get_log( "/$path", $min, $max, 0, 1, 1, @@ -864,6 +870,7 @@ sub graft_file_copy_lib { $max += $inc; $max = $head if ($max > $head); } + $SVN::Error::handler = $eh; } sub process_merge_msg_matches { @@ -994,7 +1001,8 @@ sub setup_git_svn { } mkpath([$GIT_SVN_DIR]); mkpath(["$GIT_SVN_DIR/info"]); - mkpath([$REV_DIR]); + open my $fh, '>>',$REVDB or croak $!; + close $fh; s_to_file($SVN_URL,"$GIT_SVN_DIR/info/url"); open my $fd, '>>', "$GIT_SVN_DIR/info/exclude" or croak $!; @@ -1201,8 +1209,7 @@ sub precommit_check { sub get_diff { - my ($svn_rev, $treeish) = @_; - my $from = file_to_s("$REV_DIR/$svn_rev"); + my ($from, $treeish) = @_; assert_tree($from); print "diff-tree $from $treeish\n"; my $pid = open my $diff_fh, '-|'; @@ -1222,8 +1229,8 @@ sub get_diff { } sub svn_checkout_tree { - my ($svn_rev, $treeish) = @_; - my $mods = get_diff($svn_rev, $treeish); + my ($from, $treeish) = @_; + my $mods = get_diff($from->{commit}, $treeish); return $mods unless (scalar @$mods); my ($rm, $add) = precommit_check($mods); @@ -1268,8 +1275,8 @@ sub svn_checkout_tree { } sub libsvn_checkout_tree { - my ($svn_rev, $treeish, $ed) = @_; - my $mods = get_diff($svn_rev, $treeish); + my ($from, $treeish, $ed) = @_; + my $mods = get_diff($from, $treeish); return $mods unless (scalar @$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) { @@ -1365,7 +1372,7 @@ sub get_commit_message { } sub svn_commit_tree { - my ($svn_rev, $commit) = @_; + my ($last, $commit) = @_; my $commit_msg = "$GIT_SVN_DIR/.svn-commit.tmp.$$"; my $log_msg = get_commit_message($commit, $commit_msg); my ($oneline) = ($log_msg->{msg} =~ /([^\n\r]+)/); @@ -1392,7 +1399,7 @@ sub svn_commit_tree { my @svn_up = qw(svn up); push @svn_up, '--ignore-externals' unless $_no_ignore_ext; - if ($committed == ($svn_rev + 1)) { + if ($_optimize_commits && ($committed == ($last->{revision} + 1))) { push @svn_up, "-r$committed"; sys(@svn_up); my $info = svn_info('.'); @@ -1408,14 +1415,14 @@ sub svn_commit_tree { $log_msg->{author} = $info->{'Last Changed Author'}; $log_msg->{revision} = $committed; $log_msg->{msg} .= "\n"; - my $parent = file_to_s("$REV_DIR/$svn_rev"); - git_commit($log_msg, $parent, $commit); - return $committed; + $log_msg->{parents} = [ $last->{commit} ]; + $log_msg->{commit} = git_commit($log_msg, $commit); + return $log_msg; } # resync immediately - push @svn_up, "-r$svn_rev"; + push @svn_up, "-r$last->{revision}"; sys(@svn_up); - return fetch("$committed=$commit")->{revision}; + return fetch("$committed=$commit"); } sub rev_list_raw { @@ -1671,10 +1678,9 @@ sub file_to_s { } sub assert_revision_unknown { - my $revno = shift; - if (-f "$REV_DIR/$revno") { - croak "$REV_DIR/$revno already exists! ", - "Why are we refetching it?"; + my $r = shift; + if (my $c = revdb_get($REVDB, $r)) { + croak "$r = $c already exists! Why are we refetching it?"; } } @@ -1690,18 +1696,6 @@ sub trees_eq { return 1; } -sub assert_revision_eq_or_unknown { - my ($revno, $commit) = @_; - if (-f "$REV_DIR/$revno") { - my $current = file_to_s("$REV_DIR/$revno"); - if (($commit ne $current) && !trees_eq($commit, $current)) { - croak "$REV_DIR/$revno already exists!\n", - "current: $current\nexpected: $commit\n"; - } - return; - } -} - sub git_commit { my ($log_msg, @parents) = @_; assert_revision_unknown($log_msg->{revision}); @@ -1763,19 +1757,12 @@ sub git_commit { } my @update_ref = ('git-update-ref',"refs/remotes/$GIT_SVN",$commit); if (my $primary_parent = shift @exec_parents) { - $pid = fork; - defined $pid or croak $!; - if (!$pid) { - close STDERR; - close STDOUT; - exec 'git-rev-parse','--verify', - "refs/remotes/$GIT_SVN^0" or croak $!; - } - waitpid $pid, 0; + quiet_run(qw/git-rev-parse --verify/,"refs/remotes/$GIT_SVN^0"); push @update_ref, $primary_parent unless $?; } sys(@update_ref); - sys('git-update-ref',"svn/$GIT_SVN/revs/$log_msg->{revision}",$commit); + revdb_set($REVDB, $log_msg->{revision}, $commit); + # this output is read via pipe, do not change: print "r$log_msg->{revision} = $commit\n"; if ($_repack && (--$_repack_nr == 0)) { @@ -1990,7 +1977,29 @@ sub git_svn_each { } } +sub migrate_revdb { + git_svn_each(sub { + my $id = shift; + defined(my $pid = fork) or croak $!; + if (!$pid) { + $GIT_SVN = $ENV{GIT_SVN_ID} = $id; + init_vars(); + exit 0 if -r $REVDB; + print "Upgrading svn => git mapping...\n"; + open my $fh, '>>',$REVDB or croak $!; + close $fh; + rebuild(); + print "Done upgrading. You may now delete the ", + "deprecated $GIT_SVN_DIR/revs directory\n"; + exit 0; + } + waitpid $pid, 0; + croak $? if $?; + }); +} + sub migration_check { + migrate_revdb() unless (-e $REVDB); return if (-d "$GIT_DIR/svn" || !-d $GIT_DIR); print "Upgrading repository...\n"; unless (-d "$GIT_DIR/svn") { @@ -2013,15 +2022,19 @@ sub migration_check { s_to_file($url, "$GIT_DIR/svn/$x/info/repo_url"); s_to_file($path, "$GIT_DIR/svn/$x/info/repo_path"); } + migrate_revdb() if (-d $GIT_SVN_DIR && !-w $REVDB); print "Done upgrading.\n"; } sub find_rev_before { - my ($r, $git_svn_id) = @_; - my @revs = map { basename $_ } <$GIT_DIR/svn/$git_svn_id/revs/*>; - foreach my $r0 (sort { $b <=> $a } @revs) { - next if $r0 >= $r; - return ($r0, file_to_s("$GIT_DIR/svn/$git_svn_id/revs/$r0")); + my ($r, $id, $eq_ok) = @_; + my $f = "$GIT_DIR/svn/$id/.rev_db"; + # --$r unless $eq_ok; + while ($r > 0) { + if (my $c = revdb_get($f, $r)) { + return ($r, $c); + } + --$r; } return (undef, undef); } @@ -2029,9 +2042,9 @@ sub find_rev_before { sub init_vars { $GIT_SVN ||= $ENV{GIT_SVN_ID} || 'git-svn'; $GIT_SVN_DIR = "$GIT_DIR/svn/$GIT_SVN"; + $REVDB = "$GIT_SVN_DIR/.rev_db"; $GIT_SVN_INDEX = "$GIT_SVN_DIR/index"; $SVN_URL = undef; - $REV_DIR = "$GIT_SVN_DIR/revs"; $SVN_WC = "$GIT_SVN_DIR/tree"; } @@ -2491,7 +2504,27 @@ sub libsvn_traverse_ignore { $pool->clear; } -sub libsvn_new_tree { +sub revisions_eq { + my ($path, $r0, $r1) = @_; + return 1 if $r0 == $r1; + my $nr = 0; + if ($_use_lib) { + # should be OK to use Pool here (r1 - r0) should be small + my $pool = SVN::Pool->new; + $SVN->get_log("/$path", $r0, $r1, 0, 1, 1, sub {$nr++},$pool); + $pool->clear; + } else { + my ($url, undef) = repo_path_split($SVN_URL); + my $svn_log = svn_log_raw("$url/$path","-r$r0:$r1"); + while (next_log_entry($svn_log)) { $nr++ } + close $svn_log->{fh}; + } + return 0 if ($nr > 1); + return 1; +} + +sub libsvn_find_parent_branch { + return undef; # XXX this function is disabled atm (not tested enough) my ($paths, $rev, $author, $date, $msg) = @_; my $svn_path = '/'.$SVN_PATH; @@ -2502,27 +2535,33 @@ sub libsvn_new_tree { my $branch_from = $i->copyfrom_path or next; my $r = $i->copyfrom_rev; print STDERR "Found possible branch point: ", - "$branch_from => $svn_path, $r\n"; + "$branch_from => $svn_path, $r\n"; $branch_from =~ s#^/##; my $l_map = read_url_paths(); my $url = $SVN->{url}; defined $l_map->{$url} or next; my $id = $l_map->{$url}->{$branch_from} or next; - my $f = "$GIT_DIR/svn/$id/revs/$r"; - while ($r && !-r $f) { - $r--; - $f = "$GIT_DIR/svn/$id/revs/$r"; - } - if (-r $f) { - my $parent = file_to_s($f); + my ($r0, $parent) = find_rev_before($r,$id,1); + if (defined $r0 && defined $parent && + revisions_eq($branch_from, $r0, $r)) { unlink $GIT_SVN_INDEX; print STDERR "Found branch parent: $parent\n"; sys(qw/git-read-tree/, $parent); return libsvn_fetch($parent, $paths, $rev, $author, $date, $msg); + } else { + print STDERR + "Nope, branch point not imported or unknown\n"; } - print STDERR "Nope, branch point not imported or unknown\n"; } + return undef; +} + +sub libsvn_new_tree { + if (my $log_entry = libsvn_find_parent_branch(@_)) { + return $log_entry; + } + my ($paths, $rev, $author, $date, $msg) = @_; open my $gui, '| git-update-index -z --index-info' or croak $!; my $pool = SVN::Pool->new; libsvn_traverse($gui, '', $SVN_PATH, $rev, $pool); @@ -2536,10 +2575,8 @@ sub find_graft_path_commit { foreach my $x (keys %$tree_paths) { next unless ($p1 =~ /^\Q$x\E/); my $i = $tree_paths->{$x}; - my $f = "$GIT_DIR/svn/$i/revs/$r1"; - - return file_to_s($f) if (-r $f); - + my ($r0, $parent) = find_rev_before($r1,$i,1); + return $parent if (defined $r0 && $r0 == $r1); print STDERR "r$r1 of $i not imported\n"; next; } @@ -2551,18 +2588,10 @@ sub find_graft_path_parents { foreach my $x (keys %$tree_paths) { next unless ($p0 =~ /^\Q$x\E/); my $i = $tree_paths->{$x}; - my $f = "$GIT_DIR/svn/$i/revs/$r0"; - while ($r0 && !-r $f) { - # could be an older revision, too... - $r0--; - $f = "$GIT_DIR/svn/$i/revs/$r0"; + my ($r, $parent) = find_rev_before($r0, $i, 1); + if (defined $r && defined $parent && revisions_eq($x,$r,$r0)) { + $grafts->{$c}->{$parent} = 1; } - unless (-r $f) { - print STDERR "r$r0 of $i not imported\n"; - next; - } - my $parent = file_to_s($f); - $grafts->{$c}->{$parent} = 1; } } @@ -2600,8 +2629,7 @@ sub restore_index { sub libsvn_commit_cb { my ($rev, $date, $committer, $c, $msg, $r_last, $cmt_last) = @_; - if ($rev == ($r_last + 1)) { - # optimized (avoid fetch) + if ($_optimize_commits && $rev == ($r_last + 1)) { my $log = libsvn_log_entry($rev,$committer,$date,$msg); $log->{tree} = get_tree_from_treeish($c); my $cmt = git_commit($log, $cmt_last, $c); @@ -2652,6 +2680,49 @@ sub libsvn_skip_unknown_revs { croak "Error from SVN, ($errno): ", $err->expanded_message,"\n"; }; +# Tie::File seems to be prone to offset errors if revisions get sparse, +# it's not that fast, either. Tie::File is also not in Perl 5.6. So +# one of my favorite modules is out :< Next up would be one of the DBM +# modules, but I'm not sure which is most portable... So I'll just +# go with something that's plain-text, but still capable of +# being randomly accessed. So here's my ultra-simple fixed-width +# database. All records are 40 characters + "\n", so it's easy to seek +# to a revision: (41 * rev) is the byte offset. +# A record of 40 0s denotes an empty revision. +# And yes, it's still pretty fast (faster than Tie::File). +sub revdb_set { + my ($file, $rev, $commit) = @_; + length $commit == 40 or croak "arg3 must be a full SHA1 hexsum\n"; + open my $fh, '+<', $file or croak $!; + my $offset = $rev * 41; + # assume that append is the common case: + seek $fh, 0, 2 or croak $!; + my $pos = tell $fh; + if ($pos < $offset) { + print $fh (('0' x 40),"\n") x (($offset - $pos) / 41); + } + seek $fh, $offset, 0 or croak $!; + print $fh $commit,"\n"; + close $fh or croak $!; +} + +sub revdb_get { + my ($file, $rev) = @_; + my $ret; + my $offset = $rev * 41; + open my $fh, '<', $file or croak $!; + seek $fh, $offset, 0; + if (tell $fh == $offset) { + $ret = readline $fh; + if (defined $ret) { + chomp $ret; + $ret = undef if ($ret =~ /^0{40}$/); + } + } + close $fh or croak $!; + return $ret; +} + package SVN::Git::Editor; use vars qw/@ISA/; use strict; diff --git a/contrib/git-svn/t/t0000-contrib-git-svn.sh b/contrib/git-svn/t/t0000-contrib-git-svn.sh index c33b522d083..f896e2c2a85 100644 --- a/contrib/git-svn/t/t0000-contrib-git-svn.sh +++ b/contrib/git-svn/t/t0000-contrib-git-svn.sh @@ -193,5 +193,18 @@ test_expect_success "$name" \ git-rev-list --pretty=raw remotes/alt | grep ^tree | uniq > b && diff -u a b" +name='check imported tree checksums expected tree checksums' +cat > expected <<\EOF +tree f735671b89a7eb30cab1d8597de35bd4271ab813 +tree 4b9af72bb861eaed053854ec502cf7df72618f0f +tree 031b8d557afc6fea52894eaebb45bec52f1ba6d1 +tree 0b094cbff17168f24c302e297f55bfac65eb8bd3 +tree d667270a1f7b109f5eb3aaea21ede14b56bfdd6e +tree 56a30b966619b863674f5978696f4a3594f2fca9 +tree d667270a1f7b109f5eb3aaea21ede14b56bfdd6e +tree 8f51f74cf0163afc9ad68a4b1537288c4558b5a4 +EOF +test_expect_success "$name" "diff -u a expected" + test_done diff --git a/contrib/git-svn/t/t0001-contrib-git-svn-props.sh b/contrib/git-svn/t/t0001-contrib-git-svn-props.sh index 23a5a2a2236..54e0ed73539 100644 --- a/contrib/git-svn/t/t0001-contrib-git-svn-props.sh +++ b/contrib/git-svn/t/t0001-contrib-git-svn-props.sh @@ -52,49 +52,49 @@ EOF cd .. rm -rf import -svn co "$svnrepo" test_wc +test_expect_success 'checkout working copy from svn' "svn co $svnrepo test_wc" +test_expect_success 'setup some commits to svn' \ + 'cd test_wc && + echo Greetings >> kw.c && + svn commit -m "Not yet an Id" && + svn up && + echo Hello world >> kw.c && + svn commit -m "Modified file, but still not yet an Id" && + svn up && + svn propset svn:keywords Id kw.c && + svn commit -m "Propset Id" && + svn up && + cd ..' -cd test_wc - echo 'Greetings' >> kw.c - svn commit -m 'Not yet an $Id$' - svn up +test_expect_success 'initialize git-svn' "git-svn init $svnrepo" +test_expect_success 'fetch revisions from svn' 'git-svn fetch' - echo 'Hello world' >> kw.c - svn commit -m 'Modified file, but still not yet an $Id$' - svn up - - svn propset svn:keywords Id kw.c - svn commit -m 'Propset $Id$' - svn up -cd .. - -git-svn init "$svnrepo" -git-svn fetch - -git checkout -b mybranch remotes/git-svn -echo 'Hi again' >> kw.c name='test svn:keywords ignoring' - -git commit -a -m "$name" -git-svn commit remotes/git-svn..mybranch -git pull . remotes/git-svn +test_expect_success "$name" \ + 'git checkout -b mybranch remotes/git-svn && + echo Hi again >> kw.c && + git commit -a -m "test keywoards ignoring" && + git-svn commit remotes/git-svn..mybranch && + git pull . remotes/git-svn' expect='/* $Id$ */' got="`sed -ne 2p kw.c`" test_expect_success 'raw $Id$ found in kw.c' "test '$expect' = '$got'" -cd test_wc - svn propset svn:eol-style CR empty - svn propset svn:eol-style CR crlf - svn propset svn:eol-style CR ne_crlf - svn commit -m 'propset CR on crlf files' - svn up -cd .. +test_expect_success "propset CR on crlf files" \ + 'cd test_wc && + svn propset svn:eol-style CR empty && + svn propset svn:eol-style CR crlf && + svn propset svn:eol-style CR ne_crlf && + svn commit -m "propset CR on crlf files" && + svn up && + cd ..' -git-svn fetch -git pull . remotes/git-svn +test_expect_success 'fetch and pull latest from svn and checkout a new wc' \ + "git-svn fetch && + git pull . remotes/git-svn && + svn co $svnrepo new_wc" -svn co "$svnrepo" new_wc for i in crlf ne_crlf lf ne_lf cr ne_cr empty_cr empty_lf empty empty_crlf do test_expect_success "Comparing $i" "cmp $i new_wc/$i" @@ -106,16 +106,16 @@ cd test_wc printf '$Id$\rHello\rWorld' > ne_cr a_cr=`printf '$Id$\r\nHello\r\nWorld\r\n' | git-hash-object --stdin` a_ne_cr=`printf '$Id$\r\nHello\r\nWorld' | git-hash-object --stdin` - svn propset svn:eol-style CRLF cr - svn propset svn:eol-style CRLF ne_cr - svn propset svn:keywords Id cr - svn propset svn:keywords Id ne_cr - svn commit -m 'propset CRLF on cr files' - svn up + test_expect_success 'Set CRLF on cr files' \ + 'svn propset svn:eol-style CRLF cr && + svn propset svn:eol-style CRLF ne_cr && + svn propset svn:keywords Id cr && + svn propset svn:keywords Id ne_cr && + svn commit -m "propset CRLF on cr files" && + svn up' cd .. - -git-svn fetch -git pull . remotes/git-svn +test_expect_success 'fetch and pull latest from svn' \ + 'git-svn fetch && git pull . remotes/git-svn' b_cr="`git-hash-object cr`" b_ne_cr="`git-hash-object ne_cr`" From 6c5cda89e93915ed2984ba560f3c3d048c7a5702 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Wed, 14 Jun 2006 21:24:03 -0700 Subject: [PATCH 20/43] git-svn: avoid creating some small files repo_path_split() is already pretty fast, and is already optimized via caching. We also don't need to create an exclude file if we're relying on the SVN libraries. Signed-off-by: Eric Wong --- contrib/git-svn/git-svn.perl | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/contrib/git-svn/git-svn.perl b/contrib/git-svn/git-svn.perl index 884969ebdd5..88af9c57047 100755 --- a/contrib/git-svn/git-svn.perl +++ b/contrib/git-svn/git-svn.perl @@ -1005,12 +1005,6 @@ sub setup_git_svn { close $fh; s_to_file($SVN_URL,"$GIT_SVN_DIR/info/url"); - open my $fd, '>>', "$GIT_SVN_DIR/info/exclude" or croak $!; - print $fd '.svn',"\n"; - close $fd or croak $!; - my ($url, $path) = repo_path_split($SVN_URL); - s_to_file($url, "$GIT_SVN_DIR/info/repo_url"); - s_to_file($path, "$GIT_SVN_DIR/info/repo_path"); } sub assert_svn_wc_clean { @@ -1649,6 +1643,12 @@ sub do_update_index { sub index_changes { return if $_use_lib; + + if (!-f "$GIT_SVN_DIR/info/exclude") { + open my $fd, '>>', "$GIT_SVN_DIR/info/exclude" or croak $!; + print $fd '.svn',"\n"; + close $fd or croak $!; + } my $no_text_base = shift; do_update_index([qw/git-diff-files --name-only -z/], 'remove', @@ -2018,9 +2018,6 @@ sub migration_check { my $dn = dirname("$GIT_DIR/svn/$x"); mkpath([$dn]) unless -d $dn; rename "$GIT_DIR/$x", "$GIT_DIR/svn/$x" or croak "$!: $x"; - my ($url, $path) = repo_path_split($u); - s_to_file($url, "$GIT_DIR/svn/$x/info/repo_url"); - s_to_file($path, "$GIT_DIR/svn/$x/info/repo_path"); } migrate_revdb() if (-d $GIT_SVN_DIR && !-w $REVDB); print "Done upgrading.\n"; @@ -2138,15 +2135,8 @@ sub write_grafts { sub read_url_paths { my $l_map = {}; git_svn_each(sub { my $x = shift; - my $u = file_to_s("$GIT_DIR/svn/$x/info/repo_url"); - my $p = file_to_s("$GIT_DIR/svn/$x/info/repo_path"); - # we hate trailing slashes - if ($u =~ s#(?:^\/+|\/+$)##g) { - s_to_file($u,"$GIT_DIR/svn/$x/info/repo_url"); - } - if ($p =~ s#(?:^\/+|\/+$)##g) { - s_to_file($p,"$GIT_DIR/svn/$x/info/repo_path"); - } + my $url = file_to_s("$GIT_DIR/svn/$x/info/url"); + my ($u, $p) = repo_path_split($url); $l_map->{$u}->{$p} = $x; }); return $l_map; From cf7424b021f99fbc5dc1127725e57ca3d48e981c Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Thu, 15 Jun 2006 12:50:12 -0700 Subject: [PATCH 21/43] git-svn: fix several small bugs, enable branch optimization Share the repack counter between branches when doing multi-fetch. Pass the -d flag to git repack by default. That's the main reason we will want automatic pack generation, to save space and improve disk cache performance. I won't add -a by default since it can generate extremely large packs that make RAM-starved systems unhappy. We no longer generate the .git/svn/$GIT_SVN_ID/info/uuid file, either. It was never read in the first place. Check for and create .rev_db if we need to during fetch (in case somebody manually blew away their .rev_db and wanted to start over. Mainly makes debugging easier). Croak with $? instead of $! if there's an error closing pipes Quiet down some of the chatter, too. Signed-off-by: Eric Wong --- contrib/git-svn/git-svn.perl | 144 +++++++++++++++++++---------------- 1 file changed, 80 insertions(+), 64 deletions(-) diff --git a/contrib/git-svn/git-svn.perl b/contrib/git-svn/git-svn.perl index 88af9c57047..27f1d682c89 100755 --- a/contrib/git-svn/git-svn.perl +++ b/contrib/git-svn/git-svn.perl @@ -368,7 +368,6 @@ sub fetch_lib { defined(my $pid = fork) or croak $!; if (!$pid) { $SVN::Error::handler = \&libsvn_skip_unknown_revs; - print "Fetching revisions $min .. $max\n"; # Yes I'm perfectly aware that the fourth argument # below is the limit revisions number. Unfortunately @@ -391,7 +390,6 @@ sub fetch_lib { $log_msg, @parents); } }); - $SVN::Error::handler = sub { 'quiet warnings' }; exit 0; } waitpid $pid, 0; @@ -463,7 +461,7 @@ sub commit_lib { my (@revs) = @_; my ($r_last, $cmt_last) = svn_grab_base_rev(); defined $r_last or die "Must have an existing revision to commit\n"; - my $fetched = fetch_lib(); + my $fetched = fetch(); if ($r_last != $fetched->{revision}) { print STDERR "There are new revisions that were fetched ", "and need to be merged (or acknowledged) ", @@ -523,7 +521,7 @@ sub commit_lib { $no = 1; } } - close $fh or croak $!; + close $fh or croak $?; if (! defined $r_new && ! defined $cmt_new) { unless ($no) { die "Failed to parse revision information\n"; @@ -633,17 +631,8 @@ sub multi_init { sub multi_fetch { # try to do trunk first, since branches/tags # may be descended from it. - if (-d "$GIT_DIR/svn/trunk") { - print "Fetching trunk\n"; - defined(my $pid = fork) or croak $!; - if (!$pid) { - $GIT_SVN = $ENV{GIT_SVN_ID} = 'trunk'; - init_vars(); - fetch(@_); - exit 0; - } - waitpid $pid, 0; - croak $? if $?; + if (-e "$GIT_DIR/svn/trunk/info/url") { + fetch_child_id('trunk', @_); } rec_fetch('', "$GIT_DIR/svn", @_); } @@ -725,6 +714,41 @@ out: ########################### utility functions ######################### +sub fetch_child_id { + my $id = shift; + print "Fetching $id\n"; + my $ref = "$GIT_DIR/refs/remotes/$id"; + my $ca = file_to_s($ref) if (-r $ref); + defined(my $pid = fork) or croak $!; + if (!$pid) { + $GIT_SVN = $ENV{GIT_SVN_ID} = $id; + init_vars(); + fetch(@_); + exit 0; + } + waitpid $pid, 0; + croak $? if $?; + return unless $_repack || -r $ref; + + my $cb = file_to_s($ref); + + defined($pid = open my $fh, '-|') or croak $!; + my $url = file_to_s("$GIT_DIR/svn/$id/info/url"); + $url = qr/\Q$url\E/; + if (!$pid) { + exec qw/git-rev-list --pretty=raw/, + $ca ? "$ca..$cb" : $cb or croak $!; + } + while (<$fh>) { + if (/^ git-svn-id: $url\@\d+ [a-f0-9\-]+$/) { + check_repack(); + } elsif (/^ git-svn-id: \S+\@\d+ [a-f0-9\-]+$/) { + last; + } + } + close $fh; +} + sub rec_fetch { my ($pfx, $p, @args) = @_; my @dir; @@ -733,16 +757,7 @@ sub rec_fetch { $pfx .= '/' if $pfx && $pfx !~ m!/$!; my $id = $pfx . basename $_; next if $id eq 'trunk'; - print "Fetching $id\n"; - defined(my $pid = fork) or croak $!; - if (!$pid) { - $GIT_SVN = $ENV{GIT_SVN_ID} = $id; - init_vars(); - fetch(@args); - exit 0; - } - waitpid $pid, 0; - croak $? if $?; + fetch_child_id($id, @args); } elsif (-d $_) { push @dir, $_; } @@ -943,7 +958,6 @@ sub read_uuid { $SVN_UUID = $info->{'Repository UUID'} or croak "Repository UUID unreadable\n"; } - s_to_file($SVN_UUID,"$GIT_SVN_DIR/info/uuid"); } sub quiet_run { @@ -1107,7 +1121,7 @@ sub parse_diff_tree { croak "Error parsing $_\n"; } } - close $diff_fh or croak $!; + close $diff_fh or croak $?; return \@mods; } @@ -1348,7 +1362,7 @@ sub get_commit_message { print $msg $_ or croak $!; } } - close $msg_fh or croak $!; + close $msg_fh or croak $?; } close $msg or croak $!; @@ -1562,7 +1576,7 @@ sub svn_info { push @{$ret->{-order}}, $1; } } - close $info_fh or croak $!; + close $info_fh or croak $?; return $ret; } @@ -1638,7 +1652,7 @@ sub do_update_index { } print $ui $x,"\0"; } - close $ui or croak $!; + close $ui or croak $?; } sub index_changes { @@ -1765,11 +1779,15 @@ sub git_commit { # this output is read via pipe, do not change: print "r$log_msg->{revision} = $commit\n"; + check_repack(); + return $commit; +} + +sub check_repack { if ($_repack && (--$_repack_nr == 0)) { $_repack_nr = $_repack; sys("git repack $_repack_flags"); } - return $commit; } sub set_commit_env { @@ -1877,6 +1895,10 @@ sub svn_cmd_checkout { } sub check_upgrade_needed { + if (!-r $REVDB) { + open my $fh, '>>',$REVDB or croak $!; + close $fh; + } my $old = eval { my $pid = open my $child, '-|'; defined $pid or croak $!; @@ -2026,7 +2048,8 @@ sub migration_check { sub find_rev_before { my ($r, $id, $eq_ok) = @_; my $f = "$GIT_DIR/svn/$id/.rev_db"; - # --$r unless $eq_ok; + return (undef,undef) unless -r $f; + --$r unless $eq_ok; while ($r > 0) { if (my $c = revdb_get($f, $r)) { return ($r, $c); @@ -2072,7 +2095,7 @@ sub set_default_vals { if (defined $_repack) { $_repack = 1000 if ($_repack <= 0); $_repack_nr = $_repack; - $_repack_flags ||= ''; + $_repack_flags ||= '-d'; } } @@ -2352,7 +2375,7 @@ sub libsvn_get_file { close $ho or croak $?; $hash =~ /^$sha1$/o or die "not a sha1: $hash\n"; print $gui $mode,' ',$hash,"\t",$p,"\0" or croak $!; - close $fd or croak $!; + close $fd or croak $?; } sub libsvn_log_entry { @@ -2381,7 +2404,7 @@ sub process_rm { while (<$ls>) { print $gui '0 ',0 x 40,"\t",$_ or croak $!; } - close $ls or croak $!; + close $ls or croak $?; } else { print $gui '0 ',0 x 40,"\t",$f,"\0" or croak $!; } @@ -2411,7 +2434,7 @@ sub libsvn_fetch { $pool->clear; } libsvn_get_file($gui, $_, $rev) foreach (@amr); - close $gui or croak $!; + close $gui or croak $?; return libsvn_log_entry($rev, $author, $date, $msg, [$last_commit]); } @@ -2514,36 +2537,30 @@ sub revisions_eq { } sub libsvn_find_parent_branch { - return undef; # XXX this function is disabled atm (not tested enough) my ($paths, $rev, $author, $date, $msg) = @_; my $svn_path = '/'.$SVN_PATH; # look for a parent from another branch: - foreach (keys %$paths) { - next if ($_ ne $svn_path); - my $i = $paths->{$_}; - my $branch_from = $i->copyfrom_path or next; - my $r = $i->copyfrom_rev; - print STDERR "Found possible branch point: ", - "$branch_from => $svn_path, $r\n"; - $branch_from =~ s#^/##; - my $l_map = read_url_paths(); - my $url = $SVN->{url}; - defined $l_map->{$url} or next; - my $id = $l_map->{$url}->{$branch_from} or next; - my ($r0, $parent) = find_rev_before($r,$id,1); - if (defined $r0 && defined $parent && - revisions_eq($branch_from, $r0, $r)) { - unlink $GIT_SVN_INDEX; - print STDERR "Found branch parent: $parent\n"; - sys(qw/git-read-tree/, $parent); - return libsvn_fetch($parent, $paths, $rev, - $author, $date, $msg); - } else { - print STDERR - "Nope, branch point not imported or unknown\n"; - } + my $i = $paths->{$svn_path} or return; + my $branch_from = $i->copyfrom_path or return; + my $r = $i->copyfrom_rev; + print STDERR "Found possible branch point: ", + "$branch_from => $svn_path, $r\n"; + $branch_from =~ s#^/##; + my $l_map = read_url_paths(); + my $url = $SVN->{url}; + defined $l_map->{$url} or return; + my $id = $l_map->{$url}->{$branch_from} or return; + my ($r0, $parent) = find_rev_before($r,$id,1); + return unless (defined $r0 && defined $parent); + if (revisions_eq($branch_from, $r0, $r)) { + unlink $GIT_SVN_INDEX; + print STDERR "Found branch parent: $parent\n"; + sys(qw/git-read-tree/, $parent); + return libsvn_fetch($parent, $paths, $rev, + $author, $date, $msg); } + print STDERR "Nope, branch point not imported or unknown\n"; return undef; } @@ -2556,7 +2573,7 @@ sub libsvn_new_tree { my $pool = SVN::Pool->new; libsvn_traverse($gui, '', $SVN_PATH, $rev, $pool); $pool->clear; - close $gui or croak $!; + close $gui or croak $?; return libsvn_log_entry($rev, $author, $date, $msg); } @@ -2630,7 +2647,7 @@ sub libsvn_commit_cb { exit 1; } } else { - fetch_lib("$rev=$c"); + fetch("$rev=$c"); } } @@ -2664,7 +2681,6 @@ sub libsvn_skip_unknown_revs { # 175002 - http(s):// # More codes may be discovered later... if ($errno == 175002 || $errno == 160013) { - print STDERR "directory non-existent\n"; return; } croak "Error from SVN, ($errno): ", $err->expanded_message,"\n"; From 968bdf1f3da677255c8950bb5b5a9de7e1150279 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Thu, 15 Jun 2006 13:36:12 -0700 Subject: [PATCH 22/43] git-svn: Eliminate temp file usage in libsvn_get_file() This means we'll have a loose object when we encounter a symlink but that's not the common case. We also don't have to worry about svn:eol-style when using the SVN libraries, either. So remove the code to deal with that. Signed-off-by: Eric Wong --- contrib/git-svn/git-svn.perl | 56 +++++++++++++++--------------------- 1 file changed, 23 insertions(+), 33 deletions(-) diff --git a/contrib/git-svn/git-svn.perl b/contrib/git-svn/git-svn.perl index 27f1d682c89..149149f0ef5 100755 --- a/contrib/git-svn/git-svn.perl +++ b/contrib/git-svn/git-svn.perl @@ -31,6 +31,7 @@ use File::Path qw/mkpath/; use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev pass_through/; use File::Spec qw//; use POSIX qw/strftime/; +use IPC::Open3; use Memoize; memoize('revisions_eq'); @@ -2335,47 +2336,36 @@ sub libsvn_get_file { my $p = $f; return unless ($p =~ s#^\Q$SVN_PATH\E/?##); - my $fd = IO::File->new_tmpfile or croak $!; + my ($hash, $pid, $in, $out); my $pool = SVN::Pool->new; - my ($r, $props) = $SVN->get_file($f, $rev, $fd, $pool); + defined($pid = open3($in, $out, '>&STDERR', + qw/git-hash-object -w --stdin/)) or croak $!; + my ($r, $props) = $SVN->get_file($f, $rev, $in, $pool); + $in->flush == 0 or croak $!; + close $in or croak $!; $pool->clear; - $fd->flush == 0 or croak $!; - seek $fd, 0, 0 or croak $!; - if (my $es = $props->{'svn:eol-style'}) { - my $new_fd = IO::File->new_tmpfile or croak $!; - eol_cp_fd($fd, $new_fd, $es); - close $fd or croak $!; - $fd = $new_fd; - seek $fd, 0, 0 or croak $!; - $fd->flush == 0 or croak $!; - } - my $mode = '100644'; - if (exists $props->{'svn:executable'}) { - $mode = '100755'; - } + chomp($hash = do { local $/; <$out> }); + close $out or croak $!; + waitpid $pid, 0; + $hash =~ /^$sha1$/o or die "not a sha1: $hash\n"; + + my $mode = exists $props->{'svn:executable'} ? '100755' : '100644'; if (exists $props->{'svn:special'}) { $mode = '120000'; - local $/; - my $link = <$fd>; + my $link = `git-cat-file blob $hash`; $link =~ s/^link // or die "svn:special file with contents: <", $link, "> is not understood\n"; - seek $fd, 0, 0 or croak $!; - truncate $fd, 0 or croak $!; - print $fd $link or croak $!; - seek $fd, 0, 0 or croak $!; - $fd->flush == 0 or croak $!; + defined($pid = open3($in, $out, '>&STDERR', + qw/git-hash-object -w --stdin/)) or croak $!; + print $in $link; + $in->flush == 0 or croak $!; + close $in or croak $!; + chomp($hash = do { local $/; <$out> }); + close $out or croak $!; + waitpid $pid, 0; + $hash =~ /^$sha1$/o or die "not a sha1: $hash\n"; } - my $pid = open my $ho, '-|'; - defined $pid or croak $!; - if (!$pid) { - open STDIN, '<&', $fd or croak $!; - exec qw/git-hash-object -w --stdin/ or croak $!; - } - chomp(my $hash = do { local $/; <$ho> }); - close $ho or croak $?; - $hash =~ /^$sha1$/o or die "not a sha1: $hash\n"; print $gui $mode,' ',$hash,"\t",$p,"\0" or croak $!; - close $fd or croak $?; } sub libsvn_log_entry { From c0d4822268d30b1668e94986a845b8bb5441e8b3 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Thu, 15 Jun 2006 18:48:22 -0700 Subject: [PATCH 23/43] git-svn: bugfix and optimize the 'log' command Revisions with long commit messages were being skipped, since the 'git-svn-id' metadata line was at the end and git-log uses a 32k buffer to print the commits. Also the last 'git-svn-id' metadata line in a commit is always the valid one, so make sure we use that, as well. Made the verbose flag work by passing the correct option switch ('--summary') to git-log. Finally, optimize -r/--revision argument handling by passing the appropriate limits to revision Signed-off-by: Eric Wong --- contrib/git-svn/git-svn.perl | 60 +++++++++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/contrib/git-svn/git-svn.perl b/contrib/git-svn/git-svn.perl index 149149f0ef5..417fcf1feba 100755 --- a/contrib/git-svn/git-svn.perl +++ b/contrib/git-svn/git-svn.perl @@ -663,17 +663,15 @@ sub show_log { my $pid = open(my $log,'-|'); defined $pid or croak $!; if (!$pid) { - my @rl = (qw/git-log --abbrev-commit --pretty=raw - --default/, "remotes/$GIT_SVN"); - push @rl, '--raw' if $_verbose; - exec(@rl, @args) or croak $!; + exec(git_svn_log_cmd($r_min,$r_max), @args) or croak $!; } setup_pager(); my (@k, $c, $d); + while (<$log>) { if (/^commit ($sha1_short)/o) { my $cmt = $1; - if ($c && defined $c->{r} && $c->{r} != $r_last) { + if ($c && cmt_showable($c) && $c->{r} != $r_last) { $r_last = $c->{r}; process_commit($c, $r_min, $r_max, \@k) or goto out; @@ -692,8 +690,7 @@ sub show_log { } elsif ($d) { push @{$c->{diff}}, $_; } elsif (/^ (git-svn-id:.+)$/) { - my ($url, $rev, $uuid) = extract_metadata($1); - $c->{r} = $rev; + (undef, $c->{r}, undef) = extract_metadata($1); } elsif (s/^ //) { push @{$c->{l}}, $_; } @@ -715,6 +712,52 @@ out: ########################### utility functions ######################### +sub cmt_showable { + my ($c) = @_; + return 1 if defined $c->{r}; + if ($c->{l} && $c->{l}->[-1] eq "...\n" && + $c->{a_raw} =~ /\@([a-f\d\-]+)>$/) { + my @msg = safe_qx(qw/git-cat-file commit/, $c->{c}); + shift @msg while ($msg[0] ne "\n"); + shift @msg; + @{$c->{l}} = grep !/^git-svn-id: /, @msg; + + (undef, $c->{r}, undef) = extract_metadata( + (grep(/^git-svn-id: /, @msg))[-1]); + } + return defined $c->{r}; +} + +sub git_svn_log_cmd { + my ($r_min, $r_max) = @_; + my @cmd = (qw/git-log --abbrev-commit --pretty=raw + --default/, "refs/remotes/$GIT_SVN"); + push @cmd, '--summary' if $_verbose; + return @cmd unless defined $r_max; + if ($r_max == $r_min) { + push @cmd, '--max-count=1'; + if (my $c = revdb_get($REVDB, $r_max)) { + push @cmd, $c; + } + } else { + my ($c_min, $c_max); + $c_max = revdb_get($REVDB, $r_max); + $c_min = revdb_get($REVDB, $r_min); + if ($c_min && $c_max) { + if ($r_max > $r_max) { + push @cmd, "$c_min..$c_max"; + } else { + push @cmd, "$c_max..$c_min"; + } + } elsif ($r_max > $r_min) { + push @cmd, $c_max; + } else { + push @cmd, $c_min; + } + } + return @cmd; +} + sub fetch_child_id { my $id = shift; print "Fetching $id\n"; @@ -2206,6 +2249,7 @@ sub setup_pager { # translated to Perl from pager.c sub get_author_info { my ($dest, $author, $t, $tz) = @_; $author =~ s/(?:^\s*|\s*$)//g; + $dest->{a_raw} = $author; my $_a; if ($_authors) { $_a = $rusers{$author} || undef; @@ -2440,7 +2484,7 @@ sub svn_grab_base_rev { close $fh; if (defined $c && length $c) { my ($url, $rev, $uuid) = extract_metadata((grep(/^git-svn-id: /, - safe_qx(qw/git-cat-file commit/, $c)))[0]); + safe_qx(qw/git-cat-file commit/, $c)))[-1]); return ($rev, $c); } return (undef, undef); From 86f363791b281fb916414a89282b2e67cdaa36c0 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Thu, 15 Jun 2006 19:13:56 -0700 Subject: [PATCH 24/43] git-svn: tests no longer fail if LC_ALL is not a UTF-8 locale Signed-off-by: Eric Wong --- contrib/git-svn/Makefile | 5 +++-- contrib/git-svn/t/t0000-contrib-git-svn.sh | 8 ++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/contrib/git-svn/Makefile b/contrib/git-svn/Makefile index d73aa5641c9..6aedb10f126 100644 --- a/contrib/git-svn/Makefile +++ b/contrib/git-svn/Makefile @@ -32,9 +32,10 @@ test: git-svn cd t && $(SHELL) ./t0000-contrib-git-svn.sh $(TEST_FLAGS) cd t && $(SHELL) ./t0001-contrib-git-svn-props.sh $(TEST_FLAGS) +# we can test NO_OPTIMIZE_COMMITS independently of LC_ALL full-test: - $(MAKE) test GIT_SVN_NO_LIB=1 GIT_SVN_NO_OPTIMIZE_COMMITS=1 - $(MAKE) test GIT_SVN_NO_LIB=0 GIT_SVN_NO_OPTIMIZE_COMMITS=1 + $(MAKE) test GIT_SVN_NO_LIB=1 GIT_SVN_NO_OPTIMIZE_COMMITS=1 LC_ALL=C + $(MAKE) test GIT_SVN_NO_LIB=0 GIT_SVN_NO_OPTIMIZE_COMMITS=1 LC_ALL=C $(MAKE) test GIT_SVN_NO_LIB=1 GIT_SVN_NO_OPTIMIZE_COMMITS=0 \ LC_ALL=en_US.UTF-8 $(MAKE) test GIT_SVN_NO_LIB=0 GIT_SVN_NO_OPTIMIZE_COMMITS=0 \ diff --git a/contrib/git-svn/t/t0000-contrib-git-svn.sh b/contrib/git-svn/t/t0000-contrib-git-svn.sh index f896e2c2a85..0f52746647a 100644 --- a/contrib/git-svn/t/t0000-contrib-git-svn.sh +++ b/contrib/git-svn/t/t0000-contrib-git-svn.sh @@ -194,8 +194,12 @@ test_expect_success "$name" \ diff -u a b" name='check imported tree checksums expected tree checksums' -cat > expected <<\EOF -tree f735671b89a7eb30cab1d8597de35bd4271ab813 +rm -f expected +if test -n "$GIT_SVN_LC_ALL" && echo $GIT_SVN_LC_ALL | grep -q '\.UTF-8$' +then + echo tree f735671b89a7eb30cab1d8597de35bd4271ab813 > expected +fi +cat >> expected <<\EOF tree 4b9af72bb861eaed053854ec502cf7df72618f0f tree 031b8d557afc6fea52894eaebb45bec52f1ba6d1 tree 0b094cbff17168f24c302e297f55bfac65eb8bd3 From 0e8a002c59cbaca692dac81320be44c965588291 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Thu, 15 Jun 2006 19:51:05 -0700 Subject: [PATCH 25/43] git-svn: svn (command-line) 1.0.x compatibility Tested on a plain Ubuntu Warty installation using subversion 1.0.6-1.2ubuntu3 svn add --force was never needed, as it only affected directories, which git (thankfully) doesn't track The 1.0.x also didn't support symlinks(!), so allow NO_SYMLINK to be defined for running tests Signed-off-by: Eric Wong --- contrib/git-svn/git-svn.perl | 4 +- contrib/git-svn/t/t0000-contrib-git-svn.sh | 80 ++++++++++++---------- 2 files changed, 46 insertions(+), 38 deletions(-) diff --git a/contrib/git-svn/git-svn.perl b/contrib/git-svn/git-svn.perl index 417fcf1feba..ab1d06500dd 100755 --- a/contrib/git-svn/git-svn.perl +++ b/contrib/git-svn/git-svn.perl @@ -1306,12 +1306,12 @@ sub svn_checkout_tree { } elsif ($m->{chg} eq 'T') { sys(qw(svn rm --force),$m->{file_b}); apply_mod_line_blob($m); - sys(qw(svn add --force), $m->{file_b}); + sys(qw(svn add), $m->{file_b}); svn_check_prop_executable($m); } elsif ($m->{chg} eq 'A') { svn_ensure_parent_path( $m->{file_b} ); apply_mod_line_blob($m); - sys(qw(svn add --force), $m->{file_b}); + sys(qw(svn add), $m->{file_b}); svn_check_prop_executable($m); } else { croak "Invalid chg: $m->{chg}\n"; diff --git a/contrib/git-svn/t/t0000-contrib-git-svn.sh b/contrib/git-svn/t/t0000-contrib-git-svn.sh index 0f52746647a..443d5183670 100644 --- a/contrib/git-svn/t/t0000-contrib-git-svn.sh +++ b/contrib/git-svn/t/t0000-contrib-git-svn.sh @@ -11,7 +11,10 @@ mkdir import cd import echo foo > foo -ln -s foo foo.link +if test -z "$NO_SYMLINK" +then + ln -s foo foo.link +fi mkdir -p dir/a/b/c/d/e echo 'deep dir' > dir/a/b/c/d/e/file mkdir -p bar @@ -129,46 +132,45 @@ test_expect_success "$name" \ -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" +if test -z "$NO_SYMLINK" +then + 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 remotes/git-svn..mybranch5 && - svn up $SVN_TREE && - test -L $SVN_TREE/exec.sh" + test_expect_success "$name" \ + "git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch5 && + svn up $SVN_TREE && + 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 remotes/git-svn..mybranch5 && + svn up $SVN_TREE && + test -x $SVN_TREE/bar/zzz && + test -L $SVN_TREE/exec-2.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" + 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 remotes/git-svn..mybranch5 && - svn up $SVN_TREE && - 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 remotes/git-svn..mybranch5 && - svn up $SVN_TREE && - test -f $SVN_TREE/exec-2.sh && - test ! -L $SVN_TREE/exec-2.sh && - diff -u help $SVN_TREE/exec-2.sh" + test_expect_success "$name" \ + "git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch5 && + svn up $SVN_TREE && + test -f $SVN_TREE/exec-2.sh && + test ! -L $SVN_TREE/exec-2.sh && + diff -u help $SVN_TREE/exec-2.sh" +fi if test -n "$GIT_SVN_LC_ALL" && echo $GIT_SVN_LC_ALL | grep -q '\.UTF-8$' @@ -193,6 +195,12 @@ test_expect_success "$name" \ git-rev-list --pretty=raw remotes/alt | grep ^tree | uniq > b && diff -u a b" +if test -n "$NO_SYMLINK" +then + test_done + exit 0 +fi + name='check imported tree checksums expected tree checksums' rm -f expected if test -n "$GIT_SVN_LC_ALL" && echo $GIT_SVN_LC_ALL | grep -q '\.UTF-8$' From 1a82e79315ed633f6b0b1fc4076054950c5380d3 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Fri, 16 Jun 2006 02:55:13 -0700 Subject: [PATCH 26/43] git-svn: rebuild convenience and bugfixes We will now automatically fetch the refs/remotes/git-svn ref from origin and store a Pull: line for it. --remote= may be passed if your remote is named something other than 'origin' Also, remember to make GIT_SVN_DIR whenever we need to create .rev_db Signed-off-by: Eric Wong --- contrib/git-svn/git-svn.perl | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/contrib/git-svn/git-svn.perl b/contrib/git-svn/git-svn.perl index ab1d06500dd..da0ff9ad8a5 100755 --- a/contrib/git-svn/git-svn.perl +++ b/contrib/git-svn/git-svn.perl @@ -42,7 +42,7 @@ my $_optimize_commits = 1 unless $ENV{GIT_SVN_NO_OPTIMIZE_COMMITS}; my $sha1 = qr/[a-f\d]{40}/; my $sha1_short = qr/[a-f\d]{4,40}/; my ($_revision,$_stdin,$_no_ignore_ext,$_no_stop_copy,$_help,$_rmdir,$_edit, - $_find_copies_harder, $_l, $_cp_similarity, + $_find_copies_harder, $_l, $_cp_similarity, $_cp_remote, $_repack, $_repack_nr, $_repack_flags, $_template, $_shared, $_no_default_regex, $_no_graft_copy, $_limit, $_verbose, $_incremental, $_oneline, $_l_fmt, $_show_commit, @@ -86,6 +86,7 @@ my %cmd = ( { 'revision|r=i' => \$_revision } ], rebuild => [ \&rebuild, "Rebuild git-svn metadata (after git clone)", { 'no-ignore-externals' => \$_no_ignore_ext, + 'copy-remote|remote=s' => \$_cp_remote, 'upgrade' => \$_upgrade } ], 'graft-branches' => [ \&graft_branches, 'Detect merges/branches from already imported history', @@ -134,7 +135,7 @@ init_vars(); load_authors() if $_authors; load_all_refs() if $_branch_all_refs; svn_compat_check(); -migration_check() unless $cmd =~ /^(?:init|multi-init)$/; +migration_check() unless $cmd =~ /^(?:init|rebuild|multi-init)$/; $cmd{$cmd}->[0]->(@ARGV); exit 0; @@ -174,6 +175,9 @@ sub version { } sub rebuild { + if (quiet_run(qw/git-rev-parse --verify/,"refs/remotes/$GIT_SVN^0")) { + copy_remote_ref(); + } $SVN_URL = shift or undef; my $newest_rev = 0; if ($_upgrade) { @@ -1940,6 +1944,7 @@ sub svn_cmd_checkout { sub check_upgrade_needed { if (!-r $REVDB) { + -d $GIT_SVN_DIR or mkpath([$GIT_SVN_DIR]); open my $fh, '>>',$REVDB or croak $!; close $fh; } @@ -2052,6 +2057,7 @@ sub migrate_revdb { init_vars(); exit 0 if -r $REVDB; print "Upgrading svn => git mapping...\n"; + -d $GIT_SVN_DIR or mkpath([$GIT_SVN_DIR]); open my $fh, '>>',$REVDB or croak $!; close $fh; rebuild(); @@ -2763,6 +2769,17 @@ sub revdb_get { return $ret; } +sub copy_remote_ref { + my $origin = $_cp_remote ? $_cp_remote : 'origin'; + my $ref = "refs/remotes/$GIT_SVN"; + if (safe_qx('git-ls-remote', $origin, $ref)) { + sys(qw/git fetch/, $origin, "$ref:$ref"); + } else { + die "Unable to find remote reference: ", + "refs/remotes/$GIT_SVN on $origin\n"; + } +} + package SVN::Git::Editor; use vars qw/@ISA/; use strict; From ada7781dc3602e6efc052e5e0da37a63caae0489 Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 15 Jun 2006 17:26:21 -0400 Subject: [PATCH 27/43] Add a "--notags" option for git-p4import. P4import currently creates a git tag for every commit it imports. When importing from a large repository too many tags can be created for git to manage, so this provides an option to shut that feature off if necessary. Signed-off-by: Sean Estabrooks --- Documentation/git-p4import.txt | 5 ++++- git-p4import.py | 12 ++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Documentation/git-p4import.txt b/Documentation/git-p4import.txt index c198ff2502d..0858e5efbe0 100644 --- a/Documentation/git-p4import.txt +++ b/Documentation/git-p4import.txt @@ -8,7 +8,7 @@ git-p4import - Import a Perforce repository into git SYNOPSIS -------- -`git-p4import` [-q|-v] [--authors ] [-t ] +`git-p4import` [-q|-v] [--notags] [--authors ] [-t ] `git-p4import` --stitch @@ -43,6 +43,9 @@ OPTIONS Specify an authors file containing a mapping of Perforce user ids to full names and email addresses (see Notes below). +\--notags:: + Do not create a tag for each imported commit. + \--stitch:: Import the contents of the given perforce branch into the currently checked out git branch. diff --git a/git-p4import.py b/git-p4import.py index 74172ab4500..908941dd770 100644 --- a/git-p4import.py +++ b/git-p4import.py @@ -23,7 +23,6 @@ s = signal(SIGINT, SIG_DFL) if s != default_int_handler: signal(SIGINT, s) - def die(msg, *args): for a in args: msg = "%s %s" % (msg, a) @@ -38,6 +37,7 @@ verbosity = 1 logfile = "/dev/null" ignore_warnings = False stitch = 0 +tagall = True def report(level, msg, *args): global verbosity @@ -261,10 +261,9 @@ class git_command: self.make_tag("p4/%s"%id, commit) self.git("update-ref HEAD %s %s" % (commit, current) ) - try: opts, args = getopt.getopt(sys.argv[1:], "qhvt:", - ["authors=","help","stitch=","timezone=","log=","ignore"]) + ["authors=","help","stitch=","timezone=","log=","ignore","notags"]) except getopt.GetoptError: usage() @@ -275,6 +274,8 @@ for o, a in opts: verbosity += 1 if o in ("--log"): logfile = a + if o in ("--notags"): + tagall = False if o in ("-h", "--help"): usage() if o in ("--ignore"): @@ -350,7 +351,10 @@ for id in changes: report(1, "Importing changeset", id) change = p4.describe(id) p4.sync(id) - git.commit(change.author, change.email, change.date, change.msg, id) + if tagall : + git.commit(change.author, change.email, change.date, change.msg, id) + else: + git.commit(change.author, change.email, change.date, change.msg, "import") if stitch == 1: git.clean_directories() stitch = 0 From d8498500ba5cf348577202e0bb7810cbd68fa120 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Wed, 14 Jun 2006 06:01:05 -0700 Subject: [PATCH 28/43] fix git alias When extra command line arguments are given to a command that was alias-expanded, the code generated a wrong argument list, leaving the original alias in the result, and forgetting to terminate the new argv list. Signed-off-by: Junio C Hamano --- git.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/git.c b/git.c index 9469d44b4bc..329ebec78cd 100644 --- a/git.c +++ b/git.c @@ -122,9 +122,9 @@ static int handle_alias(int *argcp, const char ***argv) /* insert after command name */ if (*argcp > 1) { new_argv = realloc(new_argv, sizeof(char*) * - (count + *argcp - 1)); - memcpy(new_argv + count, *argv, sizeof(char*) * - (*argcp - 1)); + (count + *argcp)); + memcpy(new_argv + count, *argv + 1, + sizeof(char*) * *argcp); } *argv = new_argv; From b19ee24b22b3c1d08f06e8f868f11d9e8639fd30 Mon Sep 17 00:00:00 2001 From: Fredrik Kuivinen Date: Tue, 13 Jun 2006 08:08:31 +0200 Subject: [PATCH 29/43] blame: Add --time to produce raw timestamps fix the usage string and clean up the docs while we are at it Signed-off-by: Fredrik Kuivinen Signed-off-by: Junio C Hamano --- Documentation/git-blame.txt | 5 ++++- blame.c | 24 ++++++++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/Documentation/git-blame.txt b/Documentation/git-blame.txt index 0a1fa00db0a..bfed945914c 100644 --- a/Documentation/git-blame.txt +++ b/Documentation/git-blame.txt @@ -20,7 +20,10 @@ OPTIONS Use the same output mode as git-annotate (Default: off). -l, --long:: - Show long rev (Defaults off). + Show long rev (Default: off). + +-t, --time:: + Show raw timestamp (Default: off). -S, --rev-file :: Use revs from revs-file instead of calling git-rev-list. diff --git a/blame.c b/blame.c index 88bfec262f7..25d3bcf647f 100644 --- a/blame.c +++ b/blame.c @@ -20,9 +20,11 @@ #define DEBUG 0 -static const char blame_usage[] = "[-c] [-l] [--] file [commit]\n" +static const char blame_usage[] = "[-c] [-l] [-t] [-S ] [--] file [commit]\n" " -c, --compability Use the same output mode as git-annotate (Default: off)\n" " -l, --long Show long commit SHA1 (Default: off)\n" + " -t, --time Show raw timestamp (Default: off)\n" + " -S, --revs-file Use revisions from revs-file instead of calling git-rev-list\n" " -h, --help This message"; static struct commit **blame_lines; @@ -680,13 +682,19 @@ static void get_commit_info(struct commit* commit, struct commit_info* ret) *tmp = 0; } -static const char* format_time(unsigned long time, const char* tz_str) +static const char* format_time(unsigned long time, const char* tz_str, + int show_raw_time) { static char time_buf[128]; time_t t = time; int minutes, tz; struct tm *tm; + if (show_raw_time) { + sprintf(time_buf, "%lu %s", time, tz_str); + return time_buf; + } + tz = atoi(tz_str); minutes = tz < 0 ? -tz : tz; minutes = (minutes / 100)*60 + (minutes % 100); @@ -740,6 +748,7 @@ int main(int argc, const char **argv) char filename_buf[256]; int sha1_len = 8; int compability = 0; + int show_raw_time = 0; int options = 1; struct commit* start_commit; @@ -768,6 +777,10 @@ int main(int argc, const char **argv) !strcmp(argv[i], "--compability")) { compability = 1; continue; + } else if(!strcmp(argv[i], "-t") || + !strcmp(argv[i], "--time")) { + show_raw_time = 1; + continue; } else if(!strcmp(argv[i], "-S")) { if (i + 1 < argc && !read_ancestry(argv[i + 1], &sha1_p)) { @@ -873,14 +886,17 @@ int main(int argc, const char **argv) fwrite(sha1_to_hex(c->object.sha1), sha1_len, 1, stdout); if(compability) { printf("\t(%10s\t%10s\t%d)", ci.author, - format_time(ci.author_time, ci.author_tz), i+1); + format_time(ci.author_time, ci.author_tz, + show_raw_time), + i+1); } else { if (found_rename) printf(" %-*.*s", longest_file, longest_file, u->pathname); printf(" (%-*.*s %10s %*d) ", longest_author, longest_author, ci.author, - format_time(ci.author_time, ci.author_tz), + format_time(ci.author_time, ci.author_tz, + show_raw_time), max_digits, i+1); } From 71b08148366bbc4d02b3477e7ded35b049206f89 Mon Sep 17 00:00:00 2001 From: Martin Langhoff Date: Sun, 11 Jun 2006 20:12:09 +1200 Subject: [PATCH 30/43] cvsimport: ignore CVSPS_NO_BRANCH and impossible branches cvsps output often contains references to CVSPS_NO_BRANCH, commits that it could not trace to a branch. Ignore that branch. Additionally, cvsps will sometimes draw circular relationships between branches -- where two branches are recorded as opening from the other. In those cases, and where the ancestor branch hasn't been seen, ignore it. Signed-off-by: Martin Langhoff Signed-off-by: Junio C Hamano --- git-cvsimport.perl | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/git-cvsimport.perl b/git-cvsimport.perl index 76f6246a31b..d83c52fcb20 100755 --- a/git-cvsimport.perl +++ b/git-cvsimport.perl @@ -595,7 +595,11 @@ sub write_tree () { } my($patchset,$date,$author_name,$author_email,$branch,$ancestor,$tag,$logmsg); -my(@old,@new,@skipped); +my(@old,@new,@skipped,%ignorebranch); + +# commits that cvsps cannot place anywhere... +$ignorebranch{'#CVSPS_NO_BRANCH'} = 1; + sub commit { update_index(@old, @new); @old = @new = (); @@ -751,7 +755,16 @@ while() { $state = 11; next; } + if (exists $ignorebranch{$branch}) { + print STDERR "Skipping $branch\n"; + $state = 11; + next; + } if($ancestor) { + if($ancestor eq $branch) { + print STDERR "Branch $branch erroneously stems from itself -- changed ancestor to $opt_o\n"; + $ancestor = $opt_o; + } if(-f "$git_dir/refs/heads/$branch") { print STDERR "Branch $branch already exists!\n"; $state=11; @@ -759,6 +772,7 @@ while() { } unless(open(H,"$git_dir/refs/heads/$ancestor")) { print STDERR "Branch $ancestor does not exist!\n"; + $ignorebranch{$branch} = 1; $state=11; next; } @@ -766,6 +780,7 @@ while() { close(H); unless(open(H,"> $git_dir/refs/heads/$branch")) { print STDERR "Could not create branch $branch: $!\n"; + $ignorebranch{$branch} = 1; $state=11; next; } From 2f57c69792146e6c178a05edec18ab15d0138ad6 Mon Sep 17 00:00:00 2001 From: Martin Langhoff Date: Sun, 11 Jun 2006 20:12:20 +1200 Subject: [PATCH 31/43] cvsimport: complete the cvsps run before starting the import We now capture the output of cvsps to a tempfile, and then read it in. cvsps 2.1 works quite a bit "in memory", and only prints its patchset info once it has finished talking with cvs, but apparently retaining all that memory allocation. With this patch, cvsps is finished and reaped before cvsimport start working (and growing). So the footprint of the whole process is much lower. Signed-off-by: Martin Langhoff Signed-off-by: Junio C Hamano --- git-cvsimport.perl | 42 ++++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/git-cvsimport.perl b/git-cvsimport.perl index d83c52fcb20..9f5031a61a2 100755 --- a/git-cvsimport.perl +++ b/git-cvsimport.perl @@ -529,25 +529,39 @@ if ($opt_A) { write_author_info("$git_dir/cvs-authors"); } -my $pid = open(CVS,"-|"); -die "Cannot fork: $!\n" unless defined $pid; -unless($pid) { - my @opt; - @opt = split(/,/,$opt_p) if defined $opt_p; - unshift @opt, '-z', $opt_z if defined $opt_z; - unshift @opt, '-q' unless defined $opt_v; - unless (defined($opt_p) && $opt_p =~ m/--no-cvs-direct/) { - push @opt, '--cvs-direct'; + +# +# run cvsps into a file unless we are getting +# it passed as a file via $opt_P +# +unless ($opt_P) { + print "Running cvsps...\n" if $opt_v; + my $pid = open(CVSPS,"-|"); + die "Cannot fork: $!\n" unless defined $pid; + unless($pid) { + my @opt; + @opt = split(/,/,$opt_p) if defined $opt_p; + unshift @opt, '-z', $opt_z if defined $opt_z; + unshift @opt, '-q' unless defined $opt_v; + unless (defined($opt_p) && $opt_p =~ m/--no-cvs-direct/) { + push @opt, '--cvs-direct'; + } + exec("cvsps","--norc",@opt,"-u","-A",'--root',$opt_d,$cvs_tree); + die "Could not start cvsps: $!\n"; } - if ($opt_P) { - exec("cat", $opt_P); - } else { - exec("cvsps","--norc",@opt,"-u","-A",'--root',$opt_d,$cvs_tree); - die "Could not start cvsps: $!\n"; + my ($cvspsfh, $cvspsfile) = tempfile('gitXXXXXX', SUFFIX => '.cvsps', + DIR => File::Spec->tmpdir()); + while () { + print $cvspsfh $_; } + close CVSPS; + close $cvspsfh; + $opt_P = $cvspsfile; } +open(CVS, "<$opt_P") or die $!; + ## cvsps output: #--------------------- #PatchSet 314 From 8f732649bc4d5619a1b399e5808b3f4c662ad200 Mon Sep 17 00:00:00 2001 From: Martin Langhoff Date: Mon, 12 Jun 2006 23:50:49 +1200 Subject: [PATCH 32/43] cvsimport: keep one index per branch during import With this patch we have a speedup and much lower IO when importing trees with many branches. Instead of forcing index re-population for each branch switch, we keep many index files around, one per branch. Signed-off-by: Martin Langhoff Signed-off-by: Junio C Hamano --- git-cvsimport.perl | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) mode change 100755 => 100644 git-cvsimport.perl diff --git a/git-cvsimport.perl b/git-cvsimport.perl old mode 100755 new mode 100644 index 9f5031a61a2..f3daa6c059f --- a/git-cvsimport.perl +++ b/git-cvsimport.perl @@ -465,10 +465,15 @@ $git_dir = getwd()."/".$git_dir unless $git_dir =~ m#^/#; $ENV{"GIT_DIR"} = $git_dir; my $orig_git_index; $orig_git_index = $ENV{GIT_INDEX_FILE} if exists $ENV{GIT_INDEX_FILE}; -my ($git_ih, $git_index) = tempfile('gitXXXXXX', SUFFIX => '.idx', - DIR => File::Spec->tmpdir()); -close ($git_ih); -$ENV{GIT_INDEX_FILE} = $git_index; + +my %index; # holds filenames of one index per branch +{ # init with an index for origin + my ($fh, $fn) = tempfile('gitXXXXXX', SUFFIX => '.idx', + DIR => File::Spec->tmpdir()); + close ($fh); + $index{$opt_o} = $fn; +} +$ENV{GIT_INDEX_FILE} = $index{$opt_o}; unless(-d $git_dir) { system("git-init-db"); die "Cannot init the GIT db at $git_tree: $?\n" if $?; @@ -496,6 +501,13 @@ unless(-d $git_dir) { $tip_at_start = `git-rev-parse --verify HEAD`; # populate index + unless ($index{$last_branch}) { + my ($fh, $fn) = tempfile('gitXXXXXX', SUFFIX => '.idx', + DIR => File::Spec->tmpdir()); + close ($fh); + $index{$last_branch} = $fn; + } + $ENV{GIT_INDEX_FILE} = $index{$last_branch}; system('git-read-tree', $last_branch); die "read-tree failed: $?\n" if $?; @@ -805,8 +817,17 @@ while() { } if(($ancestor || $branch) ne $last_branch) { print "Switching from $last_branch to $branch\n" if $opt_v; - system("git-read-tree", $branch); - die "read-tree failed: $?\n" if $?; + unless ($index{$branch}) { + my ($fh, $fn) = tempfile('gitXXXXXX', SUFFIX => '.idx', + DIR => File::Spec->tmpdir()); + close ($fh); + $index{$branch} = $fn; + $ENV{GIT_INDEX_FILE} = $index{$branch}; + system("git-read-tree", $branch); + die "read-tree failed: $?\n" if $?; + } else { + $ENV{GIT_INDEX_FILE} = $index{$branch}; + } } $last_branch = $branch if $branch ne $last_branch; $state = 9; @@ -870,7 +891,9 @@ while() { } commit() if $branch and $state != 11; -unlink($git_index); +foreach my $git_index (values %index) { + unlink($git_index); +} if (defined $orig_git_index) { $ENV{GIT_INDEX_FILE} = $orig_git_index; From e34ef621482a14143187bb99a8e341fad885c5d5 Mon Sep 17 00:00:00 2001 From: Florian Forster Date: Sun, 11 Jun 2006 17:45:19 +0200 Subject: [PATCH 33/43] gitweb: Adding a `blame' interface. This patch adds an interface for `git-blame' to `gitweb.cgi'. Links to it are placed in `git_blob'. Internally the code uses `git-annotate' because `git-blame's output differs for files that have been renamed in the past. However, I like the term `blame' better. [jc: blame can be told to produce the compatible format btw...] Signed-off-by: Florian Forster Signed-off-by: Junio C Hamano --- gitweb/gitweb.cgi | 108 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 107 insertions(+), 1 deletion(-) diff --git a/gitweb/gitweb.cgi b/gitweb/gitweb.cgi index ea21fbe88a9..91c075df3a6 100755 --- a/gitweb/gitweb.cgi +++ b/gitweb/gitweb.cgi @@ -203,6 +203,9 @@ if (!defined $action || $action eq "summary") { } elsif ($action eq "tag") { git_tag(); exit; +} elsif ($action eq "blame") { + git_blame(); + exit; } else { undef $action; die_error(undef, "Unknown action."); @@ -1228,6 +1231,107 @@ sub git_tag { git_footer_html(); } +sub git_blame { + my $fd; + die_error('404 Not Found', "What file will it be, master?") if (!$file_name); + $hash_base ||= git_read_head($project); + die_error(undef, "Reading commit failed.") unless ($hash_base); + my %co = git_read_commit($hash_base) + or die_error(undef, "Reading commit failed."); + if (!defined $hash) { + $hash = git_get_hash_by_path($hash_base, $file_name, "blob") + or die_error(undef, "Error lookup file."); + } + open ($fd, "-|", "$gitbin/git-annotate", '-l', '-t', '-r', $file_name, $hash_base) + or die_error(undef, "Open failed."); + git_header_html(); + print "
\n" . + $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=summary")}, "summary") . + " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=shortlog")}, "shortlog") . + " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=log")}, "log") . + " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$hash_base")}, "commit") . + " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commitdiff;h=$hash_base")}, "commitdiff") . + " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tree;h=$co{'tree'};hb=$hash_base")}, "tree") . "
\n"; + print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$hash;hb=$hash_base;f=$file_name")}, "blob") . + " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blame;f=$file_name")}, "head") . "
\n"; + print "
\n". + "
" . + $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$hash_base"), -class => "title"}, esc_html($co{'title'})) . + "
\n"; + print "
" . esc_html($file_name) . "
\n"; + print "
\n"; + print < + + Commit + Age + Author + Line + Data + +HTML + my @line_class = (qw(light dark)); + my $line_class_len = scalar (@line_class); + my $line_class_num = $#line_class; + while (my $line = <$fd>) { + my $long_rev; + my $short_rev; + my $author; + my $time; + my $lineno; + my $data; + my $age; + my $age_str; + my $age_style; + + chomp $line; + $line_class_num = ($line_class_num + 1) % $line_class_len; + + if ($line =~ m/^([0-9a-fA-F]{40})\t\(\s*([^\t]+)\t(\d+) \+\d\d\d\d\t(\d+)\)(.*)$/) { + $long_rev = $1; + $author = $2; + $time = $3; + $lineno = $4; + $data = $5; + } else { + print qq( Unable to parse: $line\n); + next; + } + $short_rev = substr ($long_rev, 0, 8); + $age = time () - $time; + $age_str = age_string ($age); + $age_str =~ s/ / /g; + $age_style = 'font-style: italic;'; + $age_style .= ' color: #009900; background: transparent;' if ($age < 60*60*24*2); + $age_style .= ' font-weight: bold;' if ($age < 60*60*2); + $author = esc_html ($author); + $author =~ s/ / /g; + # escape tabs + while ((my $pos = index($data, "\t")) != -1) { + if (my $count = (8 - ($pos % 8))) { + my $spaces = ' ' x $count; + $data =~ s/\t/$spaces/; + } + } + $data = esc_html ($data); + $data =~ s/ / /g; + + print < + $short_rev.. + $age_str + $author + $lineno + $data + +HTML + } # while (my $line = <$fd>) + print "\n\n"; + close $fd or print "Reading blob failed.\n"; + print "
"; + git_footer_html(); +} + sub git_tags { my $head = git_read_head($project); git_header_html(); @@ -1375,7 +1479,8 @@ sub git_blob { " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commitdiff;h=$hash_base")}, "commitdiff") . " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tree;h=$co{'tree'};hb=$hash_base")}, "tree") . "
\n"; if (defined $file_name) { - print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob_plain;h=$hash;f=$file_name")}, "plain") . + print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blame;h=$hash;hb=$hash_base;f=$file_name")}, "blame") . + " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob_plain;h=$hash;f=$file_name")}, "plain") . " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;hb=HEAD;f=$file_name")}, "head") . "
\n"; } else { print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob_plain;h=$hash")}, "plain") . "
\n"; @@ -1496,6 +1601,7 @@ sub git_tree { "\n" . "" . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$t_hash$base_key;f=$base$t_name")}, "blob") . +# " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blame;h=$t_hash$base_key;f=$base$t_name")}, "blame") . " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=history;h=$hash_base;f=$base$t_name")}, "history") . "\n"; } elsif ($t_type eq "tree") { From 5996ca0836e21ab4e6be0a16a615eff965f18b8b Mon Sep 17 00:00:00 2001 From: Florian Forster Date: Mon, 12 Jun 2006 10:31:57 +0200 Subject: [PATCH 34/43] gitweb: Make the `blame' interface in gitweb optional. Since `git-annotate' is an expensive operation to run it may be desirable to deactivate this functionality. This patch introduces the `gitweb.blame' option to git-repo-config and disables the blame support by default. Signed-off-by: Florian Forster Signed-off-by: Junio C Hamano --- gitweb/gitweb.cgi | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/gitweb/gitweb.cgi b/gitweb/gitweb.cgi index 91c075df3a6..5eabe06dafa 100755 --- a/gitweb/gitweb.cgi +++ b/gitweb/gitweb.cgi @@ -837,6 +837,25 @@ sub git_read_projects { return @list; } +sub git_get_project_config { + my $key = shift; + + return unless ($key); + $key =~ s/^gitweb\.//; + return if ($key =~ m/\W/); + + my $val = qx(git-repo-config --get gitweb.$key); + return ($val); +} + +sub git_get_project_config_bool { + my $val = git_get_project_config (@_); + if ($val and $val =~ m/true|yes|on/) { + return (1); + } + return; # implicit false +} + sub git_project_list { my @list = git_read_projects(); my @projects; @@ -1233,6 +1252,7 @@ sub git_tag { sub git_blame { my $fd; + die_error('403 Permission denied', "Permission denied.") if (!git_get_project_config_bool ('blame')); die_error('404 Not Found', "What file will it be, master?") if (!$file_name); $hash_base ||= git_read_head($project); die_error(undef, "Reading commit failed.") unless ($hash_base); @@ -1468,6 +1488,7 @@ sub git_blob { my $base = $hash_base || git_read_head($project); $hash = git_get_hash_by_path($base, $file_name, "blob") || die_error(undef, "Error lookup file."); } + my $have_blame = git_get_project_config_bool ('blame'); open my $fd, "-|", "$gitbin/git-cat-file blob $hash" or die_error(undef, "Open failed."); git_header_html(); if (defined $hash_base && (my %co = git_read_commit($hash_base))) { @@ -1479,8 +1500,10 @@ sub git_blob { " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commitdiff;h=$hash_base")}, "commitdiff") . " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tree;h=$co{'tree'};hb=$hash_base")}, "tree") . "
\n"; if (defined $file_name) { - print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blame;h=$hash;hb=$hash_base;f=$file_name")}, "blame") . - " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob_plain;h=$hash;f=$file_name")}, "plain") . + if ($have_blame) { + print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blame;h=$hash;hb=$hash_base;f=$file_name")}, "blame") . " | "; + } + print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob_plain;h=$hash;f=$file_name")}, "plain") . " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;hb=HEAD;f=$file_name")}, "head") . "
\n"; } else { print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob_plain;h=$hash")}, "plain") . "
\n"; From bfbd0bb6ecbbbf75a5caaff6afaf5a6af8fa518e Mon Sep 17 00:00:00 2001 From: Peter Eriksen Date: Sun, 11 Jun 2006 14:03:28 +0200 Subject: [PATCH 35/43] Implement safe_strncpy() as strlcpy() and use it more. Signed-off-by: Peter Eriksen Signed-off-by: Junio C Hamano --- builtin-log.c | 2 +- builtin-tar-tree.c | 4 ++-- cache.h | 2 +- config.c | 6 +++--- http-fetch.c | 10 ++++------ http-push.c | 10 +++++----- ident.c | 5 ++--- path.c | 13 +++++++++---- sha1_name.c | 3 +-- 9 files changed, 28 insertions(+), 27 deletions(-) diff --git a/builtin-log.c b/builtin-log.c index 29a885121dd..5b0ea28346e 100644 --- a/builtin-log.c +++ b/builtin-log.c @@ -112,7 +112,7 @@ static void reopen_stdout(struct commit *commit, int nr, int keep_subject) int len = 0; if (output_directory) { - strncpy(filename, output_directory, 1010); + safe_strncpy(filename, output_directory, 1010); len = strlen(filename); if (filename[len - 1] != '/') filename[len++] = '/'; diff --git a/builtin-tar-tree.c b/builtin-tar-tree.c index 58a8ccd4d6a..f6310b90323 100644 --- a/builtin-tar-tree.c +++ b/builtin-tar-tree.c @@ -240,8 +240,8 @@ static void write_entry(const unsigned char *sha1, struct strbuf *path, /* XXX: should we provide more meaningful info here? */ sprintf(header.uid, "%07o", 0); sprintf(header.gid, "%07o", 0); - strncpy(header.uname, "git", 31); - strncpy(header.gname, "git", 31); + safe_strncpy(header.uname, "git", sizeof(header.uname)); + safe_strncpy(header.gname, "git", sizeof(header.gname)); sprintf(header.devmajor, "%07o", 0); sprintf(header.devminor, "%07o", 0); diff --git a/cache.h b/cache.h index d5d7fe4f8c6..f630cf4bfa9 100644 --- a/cache.h +++ b/cache.h @@ -210,7 +210,7 @@ int git_mkstemp(char *path, size_t n, const char *template); int adjust_shared_perm(const char *path); int safe_create_leading_directories(char *path); -char *safe_strncpy(char *, const char *, size_t); +size_t safe_strncpy(char *, const char *, size_t); char *enter_repo(char *path, int strict); /* Read and unpack a sha1 file into memory, write memory to a sha1 file */ diff --git a/config.c b/config.c index c47497001e1..984c75f5dd0 100644 --- a/config.c +++ b/config.c @@ -280,17 +280,17 @@ int git_default_config(const char *var, const char *value) } if (!strcmp(var, "user.name")) { - strncpy(git_default_name, value, sizeof(git_default_name)); + safe_strncpy(git_default_name, value, sizeof(git_default_name)); return 0; } if (!strcmp(var, "user.email")) { - strncpy(git_default_email, value, sizeof(git_default_email)); + safe_strncpy(git_default_email, value, sizeof(git_default_email)); return 0; } if (!strcmp(var, "i18n.commitencoding")) { - strncpy(git_commit_encoding, value, sizeof(git_commit_encoding)); + safe_strncpy(git_commit_encoding, value, sizeof(git_commit_encoding)); return 0; } diff --git a/http-fetch.c b/http-fetch.c index d3602b7d7d8..da1a7f5416a 100644 --- a/http-fetch.c +++ b/http-fetch.c @@ -584,10 +584,8 @@ static void process_alternates_response(void *callback_data) // skip 'objects' at end if (okay) { target = xmalloc(serverlen + posn - i - 6); - strncpy(target, base, serverlen); - strncpy(target + serverlen, data + i, - posn - i - 7); - target[serverlen + posn - i - 7] = '\0'; + safe_strncpy(target, base, serverlen); + safe_strncpy(target + serverlen, data + i, posn - i - 6); if (get_verbosely) fprintf(stderr, "Also look at %s\n", target); @@ -728,8 +726,8 @@ xml_cdata(void *userData, const XML_Char *s, int len) struct xml_ctx *ctx = (struct xml_ctx *)userData; if (ctx->cdata) free(ctx->cdata); - ctx->cdata = xcalloc(len+1, 1); - strncpy(ctx->cdata, s, len); + ctx->cdata = xmalloc(len + 1); + safe_strncpy(ctx->cdata, s, len + 1); } static int remote_ls(struct alt_base *repo, const char *path, int flags, diff --git a/http-push.c b/http-push.c index b39b36b7679..2d9441ec602 100644 --- a/http-push.c +++ b/http-push.c @@ -1269,8 +1269,8 @@ xml_cdata(void *userData, const XML_Char *s, int len) struct xml_ctx *ctx = (struct xml_ctx *)userData; if (ctx->cdata) free(ctx->cdata); - ctx->cdata = xcalloc(len+1, 1); - strncpy(ctx->cdata, s, len); + ctx->cdata = xmalloc(len + 1); + safe_strncpy(ctx->cdata, s, len + 1); } static struct remote_lock *lock_remote(char *path, long timeout) @@ -1472,7 +1472,7 @@ static void process_ls_object(struct remote_ls_ctx *ls) return; path += 8; obj_hex = xmalloc(strlen(path)); - strncpy(obj_hex, path, 2); + safe_strncpy(obj_hex, path, 3); strcpy(obj_hex + 2, path + 3); one_remote_object(obj_hex); free(obj_hex); @@ -2160,8 +2160,8 @@ static void fetch_symref(char *path, char **symref, unsigned char *sha1) /* If it's a symref, set the refname; otherwise try for a sha1 */ if (!strncmp((char *)buffer.buffer, "ref: ", 5)) { - *symref = xcalloc(buffer.posn - 5, 1); - strncpy(*symref, (char *)buffer.buffer + 5, buffer.posn - 6); + *symref = xmalloc(buffer.posn - 5); + safe_strncpy(*symref, (char *)buffer.buffer + 5, buffer.posn - 5); } else { get_sha1_hex(buffer.buffer, sha1); } diff --git a/ident.c b/ident.c index 7c81fe8d8bf..7b44cbd2cc5 100644 --- a/ident.c +++ b/ident.c @@ -71,10 +71,9 @@ int setup_ident(void) len = strlen(git_default_email); git_default_email[len++] = '.'; if (he && (domainname = strchr(he->h_name, '.'))) - strncpy(git_default_email + len, domainname + 1, sizeof(git_default_email) - len); + safe_strncpy(git_default_email + len, domainname + 1, sizeof(git_default_email) - len); else - strncpy(git_default_email + len, "(none)", sizeof(git_default_email) - len); - git_default_email[sizeof(git_default_email) - 1] = 0; + safe_strncpy(git_default_email + len, "(none)", sizeof(git_default_email) - len); } /* And set the default date */ datestamp(git_default_date, sizeof(git_default_date)); diff --git a/path.c b/path.c index 5168b5f17df..194e0b553f7 100644 --- a/path.c +++ b/path.c @@ -83,14 +83,19 @@ int git_mkstemp(char *path, size_t len, const char *template) } -char *safe_strncpy(char *dest, const char *src, size_t n) +size_t safe_strncpy(char *dest, const char *src, size_t size) { - strncpy(dest, src, n); - dest[n - 1] = '\0'; + size_t ret = strlen(src); - return dest; + if (size) { + size_t len = (ret >= size) ? size - 1 : ret; + memcpy(dest, src, len); + dest[len] = '\0'; + } + return ret; } + int validate_symref(const char *path) { struct stat st; diff --git a/sha1_name.c b/sha1_name.c index fbbde1cf7d3..8fe9b7a75f0 100644 --- a/sha1_name.c +++ b/sha1_name.c @@ -262,8 +262,7 @@ static int get_sha1_basic(const char *str, int len, unsigned char *sha1) if (str[am] == '@' && str[am+1] == '{' && str[len-1] == '}') { int date_len = len - am - 3; char *date_spec = xmalloc(date_len + 1); - strncpy(date_spec, str + am + 2, date_len); - date_spec[date_len] = 0; + safe_strncpy(date_spec, str + am + 2, date_len + 1); at_time = approxidate(date_spec); free(date_spec); len = am; From 9202434cbdfb123f41fc677bbf36ff21f6094fc8 Mon Sep 17 00:00:00 2001 From: Linus Torvalds Date: Sun, 11 Jun 2006 10:57:35 -0700 Subject: [PATCH 36/43] gitweb.cgi history not shown This does: - add a "rev.simplify_history" flag which defaults to on - it turns it off for "git whatchanged" (which thus now has real semantics outside of "git log") - it adds a command line flag ("--full-history") to turn it off for others (ie you can make "git log" and "gitk" etc get the semantics if you want to. Now, just as an example of _why_ you really really really want to simplify history by default, apply this patch, install it, and try these two command lines: gitk --full-history -- git.c gitk -- git.c and compare the output. So with this, you can also now do git whatchanged -p -- gitweb.cgi git log -p --full-history -- gitweb.cgi and it will show the old history of gitweb.cgi, even though it's not relevant to the _current_ state of the name "gitweb.cgi" NOTE NOTE NOTE! It will still actually simplify away merges that didn't change anything at all into either child. That creates these bogus strange discontinuities if you look at it with "gitk" (look at the --full-history gitk output for git.c, and you'll see a few strange cases). So the whole "--parent" thing ends up somewhat bogus with --full-history because of this, but I'm not sure it's worth even worrying about. I don't think you'd ever want to really use "--full-history" with the graphical representation, I just give it as an example exactly to show _why_ doing so would be insane. I think this is trivial enough and useful enough to be worth merging into the stable branch. Signed-off-by: Linus Torvalds Signed-off-by: Junio C Hamano --- builtin-log.c | 1 + revision.c | 7 ++++++- revision.h | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/builtin-log.c b/builtin-log.c index 5b0ea28346e..f4d974a7b8b 100644 --- a/builtin-log.c +++ b/builtin-log.c @@ -51,6 +51,7 @@ int cmd_whatchanged(int argc, const char **argv, char **envp) init_revisions(&rev); rev.diff = 1; rev.diffopt.recursive = 1; + rev.simplify_history = 0; return cmd_log_wc(argc, argv, envp, &rev); } diff --git a/revision.c b/revision.c index 6a6952cd559..75c648c13c6 100644 --- a/revision.c +++ b/revision.c @@ -303,7 +303,7 @@ static void try_to_simplify_commit(struct rev_info *revs, struct commit *commit) parse_commit(p); switch (rev_compare_tree(revs, p->tree, commit->tree)) { case REV_TREE_SAME: - if (p->object.flags & UNINTERESTING) { + if (!revs->simplify_history || (p->object.flags & UNINTERESTING)) { /* Even if a merge with an uninteresting * side branch brought the entire change * we are interested in, we do not want @@ -519,6 +519,7 @@ void init_revisions(struct rev_info *revs) revs->abbrev = DEFAULT_ABBREV; revs->ignore_merges = 1; + revs->simplify_history = 1; revs->pruning.recursive = 1; revs->pruning.add_remove = file_add_remove; revs->pruning.change = file_change; @@ -756,6 +757,10 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, const ch revs->full_diff = 1; continue; } + if (!strcmp(arg, "--full-history")) { + revs->simplify_history = 0; + continue; + } opts = diff_opt_parse(&revs->diffopt, argv+i, argc-i); if (opts > 0) { revs->diff = 1; diff --git a/revision.h b/revision.h index 7d85b0f2e97..4020e25c336 100644 --- a/revision.h +++ b/revision.h @@ -30,6 +30,7 @@ struct rev_info { no_merges:1, no_walk:1, remove_empty_trees:1, + simplify_history:1, lifo:1, topo_order:1, tag_objects:1, From e968751573a4ded76201a0e4414ec36649a92dda Mon Sep 17 00:00:00 2001 From: Sven Verdoolaege Date: Sat, 17 Jun 2006 19:46:40 +0200 Subject: [PATCH 37/43] git-cvsexportcommit.perl: fix typo Signed-off-by: Junio C Hamano --- git-cvsexportcommit.perl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git-cvsexportcommit.perl b/git-cvsexportcommit.perl index 57088c3f0bb..d1051d074bc 100755 --- a/git-cvsexportcommit.perl +++ b/git-cvsexportcommit.perl @@ -134,7 +134,7 @@ foreach my $f (@afiles) { and $status[0] !~ m/^File: no file /) { $dirty = 1; warn "File $f is already known in your CVS checkout -- perhaps it has been added by another user. Or this may indicate that it exists on a different branch. If this is the case, use -f to force the merge.\n"; - warn "Status was: $status\n"; + warn "Status was: $status[0]\n"; } } foreach my $f (@mfiles, @dfiles) { From 175fb6c0406e63a10791ab793cdac2acf185eab0 Mon Sep 17 00:00:00 2001 From: Jakub Narebski Date: Sat, 17 Jun 2006 08:53:45 +0200 Subject: [PATCH 38/43] Update gitweb README: gitweb is now included with git Signed-off-by: Junio C Hamano --- gitweb/README | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/gitweb/README b/gitweb/README index 3014d7320bc..8d672762eab 100644 --- a/gitweb/README +++ b/gitweb/README @@ -3,14 +3,7 @@ GIT web Interface The one working on: http://www.kernel.org/git/ -Get the gitweb.cgi by ftp: - ftp://ftp.kernel.org/pub/software/scm/gitweb/ - -It reqires the git-core binaries installed on the system: - http://www.kernel.org/git/?p=git/git.git;a=summary - -The gitweb repository is here: - http://www.kernel.org/git/?p=git/gitweb.git;a=summary +From the git version 1.4.0 gitweb is bundled with git. Any comment/question/concern to: Kay Sievers From f0338bbd820f45df51663342827e5aefc4550bf7 Mon Sep 17 00:00:00 2001 From: Dennis Stosberg Date: Sat, 17 Jun 2006 17:02:45 +0200 Subject: [PATCH 39/43] Make t4101-apply-nonl bring along its patches Some versions of "diff" (e.g. on FreeBSD and older Linux systems) do not support the "\ No newline at end of file" remark and are not able to generate the patches needed for this test. This lets the test fail, although git-apply is working perfectly. This patch adds the pre-generated patches to t/t4100/ and makes the test use them. Signed-off-by: Dennis Stosberg Signed-off-by: Junio C Hamano --- t/t4101-apply-nonl.sh | 6 +----- t/t4101/diff.0-1 | 6 ++++++ t/t4101/diff.0-2 | 7 +++++++ t/t4101/diff.0-3 | 8 ++++++++ t/t4101/diff.1-0 | 6 ++++++ t/t4101/diff.1-2 | 8 ++++++++ t/t4101/diff.1-3 | 8 ++++++++ t/t4101/diff.2-0 | 7 +++++++ t/t4101/diff.2-1 | 8 ++++++++ t/t4101/diff.2-3 | 7 +++++++ t/t4101/diff.3-0 | 8 ++++++++ t/t4101/diff.3-1 | 8 ++++++++ t/t4101/diff.3-2 | 7 +++++++ 13 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 t/t4101/diff.0-1 create mode 100644 t/t4101/diff.0-2 create mode 100644 t/t4101/diff.0-3 create mode 100644 t/t4101/diff.1-0 create mode 100644 t/t4101/diff.1-2 create mode 100644 t/t4101/diff.1-3 create mode 100644 t/t4101/diff.2-0 create mode 100644 t/t4101/diff.2-1 create mode 100644 t/t4101/diff.2-3 create mode 100644 t/t4101/diff.3-0 create mode 100644 t/t4101/diff.3-1 create mode 100644 t/t4101/diff.3-2 diff --git a/t/t4101-apply-nonl.sh b/t/t4101-apply-nonl.sh index 26b131d0d55..026fac8c553 100755 --- a/t/t4101-apply-nonl.sh +++ b/t/t4101-apply-nonl.sh @@ -20,14 +20,10 @@ do for j in 0 1 2 3 do test $i -eq $j && continue - diff -u frotz.$i frotz.$j | - sed -e ' - /^---/s|.*|--- a/frotz| - /^+++/s|.*|+++ b/frotz|' >diff.$i-$j cat frotz.$i >frotz test_expect_success \ "apply diff between $i and $j" \ - "git-apply Date: Sat, 17 Jun 2006 15:20:36 -0700 Subject: [PATCH 40/43] t5100: mailinfo and mailsplit tests. Currently the test passes with 1.3.3 but not with the tip of "master". This is to verify the fixes from Eric W Biedermann. Signed-off-by: Junio C Hamano --- t/t5100-mailinfo.sh | 28 ++++ t/t5100/info0001 | 5 + t/t5100/info0002 | 5 + t/t5100/info0003 | 5 + t/t5100/info0004 | 5 + t/t5100/info0005 | 5 + t/t5100/msg0001 | 2 + t/t5100/msg0002 | 21 +++ t/t5100/msg0003 | 9 ++ t/t5100/msg0004 | 7 + t/t5100/msg0005 | 13 ++ t/t5100/patch0001 | 14 ++ t/t5100/patch0002 | 14 ++ t/t5100/patch0003 | 14 ++ t/t5100/patch0004 | 93 +++++++++++++ t/t5100/patch0005 | 69 ++++++++++ t/t5100/sample.mbox | 317 ++++++++++++++++++++++++++++++++++++++++++++ 17 files changed, 626 insertions(+) create mode 100755 t/t5100-mailinfo.sh create mode 100644 t/t5100/info0001 create mode 100644 t/t5100/info0002 create mode 100644 t/t5100/info0003 create mode 100644 t/t5100/info0004 create mode 100644 t/t5100/info0005 create mode 100644 t/t5100/msg0001 create mode 100644 t/t5100/msg0002 create mode 100644 t/t5100/msg0003 create mode 100644 t/t5100/msg0004 create mode 100644 t/t5100/msg0005 create mode 100644 t/t5100/patch0001 create mode 100644 t/t5100/patch0002 create mode 100644 t/t5100/patch0003 create mode 100644 t/t5100/patch0004 create mode 100644 t/t5100/patch0005 create mode 100644 t/t5100/sample.mbox diff --git a/t/t5100-mailinfo.sh b/t/t5100-mailinfo.sh new file mode 100755 index 00000000000..17c1b80b5be --- /dev/null +++ b/t/t5100-mailinfo.sh @@ -0,0 +1,28 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='git-mailinfo and git-mailsplit test' + +. ./test-lib.sh + +test_expect_success 'split sample box' \ + 'git-mailsplit -o. ../t5100/sample.mbox >last && + last=`cat last` && + echo total is $last && + test `cat last` = 5' + +for mail in `echo 00*` +do + test_expect_success "mailinfo $mail" \ + "git-mailinfo -u msg$mail patch$mail <$mail >info$mail && + echo msg && + diff ../t5100/msg$mail msg$mail && + echo patch && + diff ../t5100/patch$mail patch$mail && + echo info && + diff ../t5100/info$mail info$mail" +done + +test_done diff --git a/t/t5100/info0001 b/t/t5100/info0001 new file mode 100644 index 00000000000..8c052777e0d --- /dev/null +++ b/t/t5100/info0001 @@ -0,0 +1,5 @@ +Author: A U Thor +Email: a.u.thor@example.com +Subject: a commit. +Date: Fri, 9 Jun 2006 00:44:16 -0700 + diff --git a/t/t5100/info0002 b/t/t5100/info0002 new file mode 100644 index 00000000000..49bb0fec853 --- /dev/null +++ b/t/t5100/info0002 @@ -0,0 +1,5 @@ +Author: A U Thor +Email: a.u.thor@example.com +Subject: another patch +Date: Fri, 9 Jun 2006 00:44:16 -0700 + diff --git a/t/t5100/info0003 b/t/t5100/info0003 new file mode 100644 index 00000000000..bd0d1221aa3 --- /dev/null +++ b/t/t5100/info0003 @@ -0,0 +1,5 @@ +Author: A U Thor +Email: a.u.thor@example.com +Subject: third patch +Date: Fri, 9 Jun 2006 00:44:16 -0700 + diff --git a/t/t5100/info0004 b/t/t5100/info0004 new file mode 100644 index 00000000000..616c3092a28 --- /dev/null +++ b/t/t5100/info0004 @@ -0,0 +1,5 @@ +Author: YOSHIFUJI Hideaki / 吉藤英明 +Email: yoshfuji@linux-ipv6.org +Subject: GIT: Try all addresses for given remote name +Date: Thu, 21 Jul 2005 09:10:36 -0400 (EDT) + diff --git a/t/t5100/info0005 b/t/t5100/info0005 new file mode 100644 index 00000000000..46a46fc7728 --- /dev/null +++ b/t/t5100/info0005 @@ -0,0 +1,5 @@ +Author: David Kågedal +Email: davidk@lysator.liu.se +Subject: Fixed two bugs in git-cvsimport-script. +Date: Mon, 15 Aug 2005 20:18:25 +0200 + diff --git a/t/t5100/msg0001 b/t/t5100/msg0001 new file mode 100644 index 00000000000..b275a9a9b21 --- /dev/null +++ b/t/t5100/msg0001 @@ -0,0 +1,2 @@ +Here is a patch from A U Thor. + diff --git a/t/t5100/msg0002 b/t/t5100/msg0002 new file mode 100644 index 00000000000..e2546ec7332 --- /dev/null +++ b/t/t5100/msg0002 @@ -0,0 +1,21 @@ +Here is a patch from A U Thor. This addresses the issue raised in the +message: + +From: Nit Picker +Subject: foo is too old +Message-Id: + +Hopefully this would fix the problem stated there. + + +I have included an extra blank line above, but it does not have to be +stripped away here, along with the +whitespaces at the end of the above line. They are expected to be squashed +when the message is made into a commit log by stripspace, +Also, there are three blank lines after this paragraph, +two truly blank and another full of spaces in between. + + + +Hope this helps. + diff --git a/t/t5100/msg0003 b/t/t5100/msg0003 new file mode 100644 index 00000000000..1ac68101b13 --- /dev/null +++ b/t/t5100/msg0003 @@ -0,0 +1,9 @@ +Here is a patch from A U Thor. This addresses the issue raised in the +message: + +From: Nit Picker +Subject: foo is too old +Message-Id: + +Hopefully this would fix the problem stated there. + diff --git a/t/t5100/msg0004 b/t/t5100/msg0004 new file mode 100644 index 00000000000..6f8ba3b8e0e --- /dev/null +++ b/t/t5100/msg0004 @@ -0,0 +1,7 @@ +Hello. + +Try all addresses for given remote name until it succeeds. +Also supports IPv6. + +Signed-of-by: Hideaki YOSHIFUJI + diff --git a/t/t5100/msg0005 b/t/t5100/msg0005 new file mode 100644 index 00000000000..dd94cd7b9f5 --- /dev/null +++ b/t/t5100/msg0005 @@ -0,0 +1,13 @@ +The git-cvsimport-script had a copule of small bugs that prevented me +from importing a big CVS repository. + +The first was that it didn't handle removed files with a multi-digit +primary revision number. + +The second was that it was asking the CVS server for "F" messages, +although they were not handled. + +I also updated the documentation for that script to correspond to +actual flags. + +Signed-off-by: David Kågedal diff --git a/t/t5100/patch0001 b/t/t5100/patch0001 new file mode 100644 index 00000000000..8ce155167d5 --- /dev/null +++ b/t/t5100/patch0001 @@ -0,0 +1,14 @@ +--- + foo | 2 +- + 1 files changed, 1 insertions(+), 1 deletions(-) + +diff --git a/foo b/foo +index 9123cdc..918dcf8 100644 +--- a/foo ++++ b/foo +@@ -1 +1 @@ +-Fri Jun 9 00:44:04 PDT 2006 ++Fri Jun 9 00:44:13 PDT 2006 +-- +1.4.0.g6f2b + diff --git a/t/t5100/patch0002 b/t/t5100/patch0002 new file mode 100644 index 00000000000..8ce155167d5 --- /dev/null +++ b/t/t5100/patch0002 @@ -0,0 +1,14 @@ +--- + foo | 2 +- + 1 files changed, 1 insertions(+), 1 deletions(-) + +diff --git a/foo b/foo +index 9123cdc..918dcf8 100644 +--- a/foo ++++ b/foo +@@ -1 +1 @@ +-Fri Jun 9 00:44:04 PDT 2006 ++Fri Jun 9 00:44:13 PDT 2006 +-- +1.4.0.g6f2b + diff --git a/t/t5100/patch0003 b/t/t5100/patch0003 new file mode 100644 index 00000000000..8ce155167d5 --- /dev/null +++ b/t/t5100/patch0003 @@ -0,0 +1,14 @@ +--- + foo | 2 +- + 1 files changed, 1 insertions(+), 1 deletions(-) + +diff --git a/foo b/foo +index 9123cdc..918dcf8 100644 +--- a/foo ++++ b/foo +@@ -1 +1 @@ +-Fri Jun 9 00:44:04 PDT 2006 ++Fri Jun 9 00:44:13 PDT 2006 +-- +1.4.0.g6f2b + diff --git a/t/t5100/patch0004 b/t/t5100/patch0004 new file mode 100644 index 00000000000..196458e44e1 --- /dev/null +++ b/t/t5100/patch0004 @@ -0,0 +1,93 @@ +diff --git a/connect.c b/connect.c +--- a/connect.c ++++ b/connect.c +@@ -96,42 +96,57 @@ static enum protocol get_protocol(const + die("I don't handle protocol '%s'", name); + } + +-static void lookup_host(const char *host, struct sockaddr *in) +-{ +- struct addrinfo *res; +- int ret; +- +- ret = getaddrinfo(host, NULL, NULL, &res); +- if (ret) +- die("Unable to look up %s (%s)", host, gai_strerror(ret)); +- *in = *res->ai_addr; +- freeaddrinfo(res); +-} ++#define STR_(s) # s ++#define STR(s) STR_(s) + + static int git_tcp_connect(int fd[2], const char *prog, char *host, char *path) + { +- struct sockaddr addr; +- int port = DEFAULT_GIT_PORT, sockfd; +- char *colon; +- +- colon = strchr(host, ':'); +- if (colon) { +- char *end; +- unsigned long n = strtoul(colon+1, &end, 0); +- if (colon[1] && !*end) { +- *colon = 0; +- port = n; ++ int sockfd = -1; ++ char *colon, *end; ++ char *port = STR(DEFAULT_GIT_PORT); ++ struct addrinfo hints, *ai0, *ai; ++ int gai; ++ ++ if (host[0] == '[') { ++ end = strchr(host + 1, ']'); ++ if (end) { ++ *end = 0; ++ end++; ++ host++; ++ } else ++ end = host; ++ } else ++ end = host; ++ colon = strchr(end, ':'); ++ ++ if (colon) ++ port = colon + 1; ++ ++ memset(&hints, 0, sizeof(hints)); ++ hints.ai_socktype = SOCK_STREAM; ++ hints.ai_protocol = IPPROTO_TCP; ++ ++ gai = getaddrinfo(host, port, &hints, &ai); ++ if (gai) ++ die("Unable to look up %s (%s)", host, gai_strerror(gai)); ++ ++ for (ai0 = ai; ai; ai = ai->ai_next) { ++ sockfd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); ++ if (sockfd < 0) ++ continue; ++ if (connect(sockfd, ai->ai_addr, ai->ai_addrlen) < 0) { ++ close(sockfd); ++ sockfd = -1; ++ continue; + } ++ break; + } + +- lookup_host(host, &addr); +- ((struct sockaddr_in *)&addr)->sin_port = htons(port); ++ freeaddrinfo(ai0); + +- sockfd = socket(PF_INET, SOCK_STREAM, IPPROTO_IP); + if (sockfd < 0) + die("unable to create socket (%s)", strerror(errno)); +- if (connect(sockfd, (void *)&addr, sizeof(addr)) < 0) +- die("unable to connect (%s)", strerror(errno)); ++ + fd[0] = sockfd; + fd[1] = sockfd; + packet_write(sockfd, "%s %s\n", prog, path); + +-- +YOSHIFUJI Hideaki @ USAGI Project +GPG-FP : 9022 65EB 1ECF 3AD1 0BDF 80D8 4807 F894 E062 0EEA + diff --git a/t/t5100/patch0005 b/t/t5100/patch0005 new file mode 100644 index 00000000000..7d24b24af83 --- /dev/null +++ b/t/t5100/patch0005 @@ -0,0 +1,69 @@ +--- + + Documentation/git-cvsimport-script.txt | 9 ++++++++- + git-cvsimport-script | 4 ++-- + 2 files changed, 10 insertions(+), 3 deletions(-) + +50452f9c0c2df1f04d83a26266ba704b13861632 +diff --git a/Documentation/git-cvsimport-script.txt b/Documentation/git-cvsimport-script.txt +--- a/Documentation/git-cvsimport-script.txt ++++ b/Documentation/git-cvsimport-script.txt +@@ -29,6 +29,10 @@ OPTIONS + currently, only the :local:, :ext: and :pserver: access methods + are supported. + ++-C :: ++ The GIT repository to import to. If the directory doesn't ++ exist, it will be created. Default is the current directory. ++ + -i:: + Import-only: don't perform a checkout after importing. This option + ensures the working directory and cache remain untouched and will +@@ -44,7 +48,7 @@ OPTIONS + + -p :: + Additional options for cvsps. +- The options '-x' and '-A' are implicit and should not be used here. ++ The options '-u' and '-A' are implicit and should not be used here. + + If you need to pass multiple options, separate them with a comma. + +@@ -57,6 +61,9 @@ OPTIONS + -h:: + Print a short usage message and exit. + ++-z :: ++ Pass the timestamp fuzz factor to cvsps. ++ + OUTPUT + ------ + If '-v' is specified, the script reports what it is doing. +diff --git a/git-cvsimport-script b/git-cvsimport-script +--- a/git-cvsimport-script ++++ b/git-cvsimport-script +@@ -190,7 +190,7 @@ sub conn { + $self->{'socketo'}->write("Root $repo\n"); + + # Trial and error says that this probably is the minimum set +- $self->{'socketo'}->write("Valid-responses ok error Valid-requests Mode M Mbinary E F Checked-in Created Updated Merged Removed\n"); ++ $self->{'socketo'}->write("Valid-responses ok error Valid-requests Mode M Mbinary E Checked-in Created Updated Merged Removed\n"); + + $self->{'socketo'}->write("valid-requests\n"); + $self->{'socketo'}->flush(); +@@ -691,7 +691,7 @@ while() { + unlink($tmpname); + my $mode = pmode($cvs->{'mode'}); + push(@new,[$mode, $sha, $fn]); # may be resurrected! +- } elsif($state == 9 and /^\s+(\S+):\d(?:\.\d+)+->(\d(?:\.\d+)+)\(DEAD\)\s*$/) { ++ } elsif($state == 9 and /^\s+(\S+):\d+(?:\.\d+)+->(\d+(?:\.\d+)+)\(DEAD\)\s*$/) { + my $fn = $1; + $fn =~ s#^/+##; + push(@old,$fn); + +-- +David Kgedal +- +To unsubscribe from this list: send the line "unsubscribe git" in +the body of a message to majordomo@vger.kernel.org +More majordomo info at http://vger.kernel.org/majordomo-info.html + diff --git a/t/t5100/sample.mbox b/t/t5100/sample.mbox new file mode 100644 index 00000000000..a76845465a4 --- /dev/null +++ b/t/t5100/sample.mbox @@ -0,0 +1,317 @@ +From nobody Mon Sep 17 00:00:00 2001 +From: A U Thor +Date: Fri, 9 Jun 2006 00:44:16 -0700 +Subject: [PATCH] a commit. + +Here is a patch from A U Thor. + +--- + foo | 2 +- + 1 files changed, 1 insertions(+), 1 deletions(-) + +diff --git a/foo b/foo +index 9123cdc..918dcf8 100644 +--- a/foo ++++ b/foo +@@ -1 +1 @@ +-Fri Jun 9 00:44:04 PDT 2006 ++Fri Jun 9 00:44:13 PDT 2006 +-- +1.4.0.g6f2b + +From nobody Mon Sep 17 00:00:00 2001 +From: A U Thor +Date: Fri, 9 Jun 2006 00:44:16 -0700 +Subject: [PATCH] another patch + +Here is a patch from A U Thor. This addresses the issue raised in the +message: + +From: Nit Picker +Subject: foo is too old +Message-Id: + +Hopefully this would fix the problem stated there. + + +I have included an extra blank line above, but it does not have to be +stripped away here, along with the +whitespaces at the end of the above line. They are expected to be squashed +when the message is made into a commit log by stripspace, +Also, there are three blank lines after this paragraph, +two truly blank and another full of spaces in between. + + + +Hope this helps. + +--- + foo | 2 +- + 1 files changed, 1 insertions(+), 1 deletions(-) + +diff --git a/foo b/foo +index 9123cdc..918dcf8 100644 +--- a/foo ++++ b/foo +@@ -1 +1 @@ +-Fri Jun 9 00:44:04 PDT 2006 ++Fri Jun 9 00:44:13 PDT 2006 +-- +1.4.0.g6f2b + +From nobody Mon Sep 17 00:00:00 2001 +From: Junio C Hamano +Date: Fri, 9 Jun 2006 00:44:16 -0700 +Subject: re: [PATCH] another patch + +From: A U Thor +Subject: [PATCH] third patch + +Here is a patch from A U Thor. This addresses the issue raised in the +message: + +From: Nit Picker +Subject: foo is too old +Message-Id: + +Hopefully this would fix the problem stated there. + +--- + foo | 2 +- + 1 files changed, 1 insertions(+), 1 deletions(-) + +diff --git a/foo b/foo +index 9123cdc..918dcf8 100644 +--- a/foo ++++ b/foo +@@ -1 +1 @@ +-Fri Jun 9 00:44:04 PDT 2006 ++Fri Jun 9 00:44:13 PDT 2006 +-- +1.4.0.g6f2b + +From nobody Sat Aug 27 23:07:49 2005 +Path: news.gmane.org!not-for-mail +Message-ID: <20050721.091036.01119516.yoshfuji@linux-ipv6.org> +From: YOSHIFUJI Hideaki / =?iso-2022-jp?B?GyRCNUhGIzFRTEAbKEI=?= + +Newsgroups: gmane.comp.version-control.git +Subject: [PATCH 1/2] GIT: Try all addresses for given remote name +Date: Thu, 21 Jul 2005 09:10:36 -0400 (EDT) +Lines: 99 +Organization: USAGI/WIDE Project +Approved: news@gmane.org +NNTP-Posting-Host: main.gmane.org +Mime-Version: 1.0 +Content-Type: Text/Plain; charset=us-ascii +Content-Transfer-Encoding: 7bit +X-Trace: sea.gmane.org 1121951434 29350 80.91.229.2 (21 Jul 2005 13:10:34 GMT) +X-Complaints-To: usenet@sea.gmane.org +NNTP-Posting-Date: Thu, 21 Jul 2005 13:10:34 +0000 (UTC) + +Hello. + +Try all addresses for given remote name until it succeeds. +Also supports IPv6. + +Signed-of-by: Hideaki YOSHIFUJI + +diff --git a/connect.c b/connect.c +--- a/connect.c ++++ b/connect.c +@@ -96,42 +96,57 @@ static enum protocol get_protocol(const + die("I don't handle protocol '%s'", name); + } + +-static void lookup_host(const char *host, struct sockaddr *in) +-{ +- struct addrinfo *res; +- int ret; +- +- ret = getaddrinfo(host, NULL, NULL, &res); +- if (ret) +- die("Unable to look up %s (%s)", host, gai_strerror(ret)); +- *in = *res->ai_addr; +- freeaddrinfo(res); +-} ++#define STR_(s) # s ++#define STR(s) STR_(s) + + static int git_tcp_connect(int fd[2], const char *prog, char *host, char *path) + { +- struct sockaddr addr; +- int port = DEFAULT_GIT_PORT, sockfd; +- char *colon; +- +- colon = strchr(host, ':'); +- if (colon) { +- char *end; +- unsigned long n = strtoul(colon+1, &end, 0); +- if (colon[1] && !*end) { +- *colon = 0; +- port = n; ++ int sockfd = -1; ++ char *colon, *end; ++ char *port = STR(DEFAULT_GIT_PORT); ++ struct addrinfo hints, *ai0, *ai; ++ int gai; ++ ++ if (host[0] == '[') { ++ end = strchr(host + 1, ']'); ++ if (end) { ++ *end = 0; ++ end++; ++ host++; ++ } else ++ end = host; ++ } else ++ end = host; ++ colon = strchr(end, ':'); ++ ++ if (colon) ++ port = colon + 1; ++ ++ memset(&hints, 0, sizeof(hints)); ++ hints.ai_socktype = SOCK_STREAM; ++ hints.ai_protocol = IPPROTO_TCP; ++ ++ gai = getaddrinfo(host, port, &hints, &ai); ++ if (gai) ++ die("Unable to look up %s (%s)", host, gai_strerror(gai)); ++ ++ for (ai0 = ai; ai; ai = ai->ai_next) { ++ sockfd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); ++ if (sockfd < 0) ++ continue; ++ if (connect(sockfd, ai->ai_addr, ai->ai_addrlen) < 0) { ++ close(sockfd); ++ sockfd = -1; ++ continue; + } ++ break; + } + +- lookup_host(host, &addr); +- ((struct sockaddr_in *)&addr)->sin_port = htons(port); ++ freeaddrinfo(ai0); + +- sockfd = socket(PF_INET, SOCK_STREAM, IPPROTO_IP); + if (sockfd < 0) + die("unable to create socket (%s)", strerror(errno)); +- if (connect(sockfd, (void *)&addr, sizeof(addr)) < 0) +- die("unable to connect (%s)", strerror(errno)); ++ + fd[0] = sockfd; + fd[1] = sockfd; + packet_write(sockfd, "%s %s\n", prog, path); + +-- +YOSHIFUJI Hideaki @ USAGI Project +GPG-FP : 9022 65EB 1ECF 3AD1 0BDF 80D8 4807 F894 E062 0EEA + +From nobody Sat Aug 27 23:07:49 2005 +Path: news.gmane.org!not-for-mail +Message-ID: +From: =?iso-8859-1?Q?David_K=E5gedal?= +Newsgroups: gmane.comp.version-control.git +Subject: [PATCH] Fixed two bugs in git-cvsimport-script. +Date: Mon, 15 Aug 2005 20:18:25 +0200 +Lines: 83 +Approved: news@gmane.org +NNTP-Posting-Host: main.gmane.org +Mime-Version: 1.0 +Content-Type: text/plain; charset=iso-8859-1 +Content-Transfer-Encoding: QUOTED-PRINTABLE +X-Trace: sea.gmane.org 1124130247 31839 80.91.229.2 (15 Aug 2005 18:24:07 GMT) +X-Complaints-To: usenet@sea.gmane.org +NNTP-Posting-Date: Mon, 15 Aug 2005 18:24:07 +0000 (UTC) +Cc: "Junio C. Hamano" +Original-X-From: git-owner@vger.kernel.org Mon Aug 15 20:24:05 2005 + +The git-cvsimport-script had a copule of small bugs that prevented me +from importing a big CVS repository. + +The first was that it didn't handle removed files with a multi-digit +primary revision number. + +The second was that it was asking the CVS server for "F" messages, +although they were not handled. + +I also updated the documentation for that script to correspond to +actual flags. + +Signed-off-by: David K=E5gedal +--- + + Documentation/git-cvsimport-script.txt | 9 ++++++++- + git-cvsimport-script | 4 ++-- + 2 files changed, 10 insertions(+), 3 deletions(-) + +50452f9c0c2df1f04d83a26266ba704b13861632 +diff --git a/Documentation/git-cvsimport-script.txt b/Documentation/git= +-cvsimport-script.txt +--- a/Documentation/git-cvsimport-script.txt ++++ b/Documentation/git-cvsimport-script.txt +@@ -29,6 +29,10 @@ OPTIONS + currently, only the :local:, :ext: and :pserver: access methods=20 + are supported. +=20 ++-C :: ++ The GIT repository to import to. If the directory doesn't ++ exist, it will be created. Default is the current directory. ++ + -i:: + Import-only: don't perform a checkout after importing. This option + ensures the working directory and cache remain untouched and will +@@ -44,7 +48,7 @@ OPTIONS +=20 + -p :: + Additional options for cvsps. +- The options '-x' and '-A' are implicit and should not be used here. ++ The options '-u' and '-A' are implicit and should not be used here. +=20 + If you need to pass multiple options, separate them with a comma. +=20 +@@ -57,6 +61,9 @@ OPTIONS + -h:: + Print a short usage message and exit. +=20 ++-z :: ++ Pass the timestamp fuzz factor to cvsps. ++ + OUTPUT + ------ + If '-v' is specified, the script reports what it is doing. +diff --git a/git-cvsimport-script b/git-cvsimport-script +--- a/git-cvsimport-script ++++ b/git-cvsimport-script +@@ -190,7 +190,7 @@ sub conn { + $self->{'socketo'}->write("Root $repo\n"); +=20 + # Trial and error says that this probably is the minimum set +- $self->{'socketo'}->write("Valid-responses ok error Valid-requests Mo= +de M Mbinary E F Checked-in Created Updated Merged Removed\n"); ++ $self->{'socketo'}->write("Valid-responses ok error Valid-requests Mo= +de M Mbinary E Checked-in Created Updated Merged Removed\n"); +=20 + $self->{'socketo'}->write("valid-requests\n"); + $self->{'socketo'}->flush(); +@@ -691,7 +691,7 @@ while() { + unlink($tmpname); + my $mode =3D pmode($cvs->{'mode'}); + push(@new,[$mode, $sha, $fn]); # may be resurrected! +- } elsif($state =3D=3D 9 and /^\s+(\S+):\d(?:\.\d+)+->(\d(?:\.\d+)+)\(= +DEAD\)\s*$/) { ++ } elsif($state =3D=3D 9 and /^\s+(\S+):\d+(?:\.\d+)+->(\d+(?:\.\d+)+)= +\(DEAD\)\s*$/) { + my $fn =3D $1; + $fn =3D~ s#^/+##; + push(@old,$fn); + +--=20 +David K=E5gedal +- +To unsubscribe from this list: send the line "unsubscribe git" in +the body of a message to majordomo@vger.kernel.org +More majordomo info at http://vger.kernel.org/majordomo-info.html + From 2662dbfa58a2d12d1ab3d240b643b9506f43523b Mon Sep 17 00:00:00 2001 From: "Eric W. Biederman" Date: Mon, 12 Jun 2006 13:48:35 -0600 Subject: [PATCH 41/43] Don't parse any headers in the real body of an email message. It was pointed out that the current behaviour might mispart a patch comment so remove this behaviour for now. [jc: this fixes "From: line in the middle" check in t5100 test.] Signed-off-by: Eric W. Biederman Signed-off-by: Junio C Hamano --- mailinfo.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mailinfo.c b/mailinfo.c index 5b6c2157ede..0ccd4900824 100644 --- a/mailinfo.c +++ b/mailinfo.c @@ -246,6 +246,8 @@ static int eatspace(char *line) /* First lines of body can have From:, Date:, and Subject: */ static void handle_inbody_header(int *seen, char *line) { + if (*seen & SEEN_PREFIX) + return; if (!memcmp(">From", line, 5) && isspace(line[5])) { if (!(*seen & SEEN_BOGUS_UNIX_FROM)) { *seen |= SEEN_BOGUS_UNIX_FROM; From ae448e3854d8b6e7e37aa88fa3917f5dd97f3210 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Sat, 17 Jun 2006 16:58:51 -0700 Subject: [PATCH 42/43] mailinfo: ignore blanks after in-body headers. [jc: this is based on Eric's patch but also fixes up the parsed subject headers]. Signed-off-by: Junio C Hamano --- mailinfo.c | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/mailinfo.c b/mailinfo.c index 0ccd4900824..d9b74f30de3 100644 --- a/mailinfo.c +++ b/mailinfo.c @@ -243,11 +243,20 @@ static int eatspace(char *line) #define SEEN_BOGUS_UNIX_FROM 010 #define SEEN_PREFIX 020 -/* First lines of body can have From:, Date:, and Subject: */ +/* First lines of body can have From:, Date:, and Subject: or empty */ static void handle_inbody_header(int *seen, char *line) { if (*seen & SEEN_PREFIX) return; + if (isspace(*line)) { + char *cp; + for (cp = line + 1; *cp; cp++) { + if (!isspace(*cp)) + break; + } + if (!*cp) + return; + } if (!memcmp(">From", line, 5) && isspace(line[5])) { if (!(*seen & SEEN_BOGUS_UNIX_FROM)) { *seen |= SEEN_BOGUS_UNIX_FROM; @@ -316,6 +325,7 @@ static char *cleanup_subject(char *subject) } break; } + eatspace(subject); return subject; } } @@ -422,9 +432,7 @@ static int read_one_header_line(char *line, int sz, FILE *in) if (fgets(line + ofs, sz - ofs, in) == NULL) break; len = eatspace(line + ofs); - if (len == 0) - break; - if (!is_rfc2822_header(line)) { + if ((len == 0) || !is_rfc2822_header(line)) { /* Re-add the newline */ line[ofs + len] = '\n'; line[ofs + len + 1] = '\0'; @@ -764,10 +772,8 @@ static void handle_body(void) { int seen = 0; - if (line[0] || fgets(line, sizeof(line), stdin) != NULL) { - handle_commit_msg(&seen); - handle_patch(); - } + handle_commit_msg(&seen); + handle_patch(); fclose(patchfile); if (!patch_lines) { fprintf(stderr, "No patch found\n"); From cd112cef999c59a7ca2a96c37b197d303a355924 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 13 Jun 2006 18:45:44 +0200 Subject: [PATCH 43/43] diff options: add --color This patch is a slightly adjusted version of Junio's patch: http://www.gelato.unsw.edu.au/archives/git/0604/19354.html However, instead of using a config variable, this patch makes it available as a diff option. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- diff.c | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++------- diff.h | 3 ++- pager.c | 2 +- 3 files changed, 73 insertions(+), 11 deletions(-) diff --git a/diff.c b/diff.c index 9e9cfc8b75b..bc32a4aa291 100644 --- a/diff.c +++ b/diff.c @@ -25,6 +25,20 @@ int git_diff_config(const char *var, const char *value) return git_default_config(var, value); } +enum color_diff { + DIFF_PLAIN = 0, + DIFF_METAINFO = 1, + DIFF_FILE_OLD = 2, + DIFF_FILE_NEW = 3, +}; + +static const char *diff_colors[] = { + "\033[0;0m", + "\033[1;35m", + "\033[1;31m", + "\033[1;34m", +}; + static char *quote_one(const char *str) { int needlen; @@ -177,23 +191,54 @@ static int fill_mmfile(mmfile_t *mf, struct diff_filespec *one) } struct emit_callback { + struct xdiff_emit_state xm; + int nparents, color_diff; const char **label_path; }; -static int fn_out(void *priv, mmbuffer_t *mb, int nbuf) +static inline void color_diff(int diff_use_color, enum color_diff ix) +{ + if (diff_use_color) + fputs(diff_colors[ix], stdout); +} + +static void fn_out_consume(void *priv, char *line, unsigned long len) { int i; struct emit_callback *ecbdata = priv; if (ecbdata->label_path[0]) { + color_diff(ecbdata->color_diff, DIFF_METAINFO); printf("--- %s\n", ecbdata->label_path[0]); + color_diff(ecbdata->color_diff, DIFF_METAINFO); printf("+++ %s\n", ecbdata->label_path[1]); ecbdata->label_path[0] = ecbdata->label_path[1] = NULL; } - for (i = 0; i < nbuf; i++) - if (!fwrite(mb[i].ptr, mb[i].size, 1, stdout)) - return -1; - return 0; + + /* This is not really necessary for now because + * this codepath only deals with two-way diffs. + */ + for (i = 0; i < len && line[i] == '@'; i++) + ; + if (2 <= i && i < len && line[i] == ' ') { + ecbdata->nparents = i - 1; + color_diff(ecbdata->color_diff, DIFF_METAINFO); + } + else if (len < ecbdata->nparents) + color_diff(ecbdata->color_diff, DIFF_PLAIN); + else { + int nparents = ecbdata->nparents; + int color = DIFF_PLAIN; + for (i = 0; i < nparents && len; i++) { + if (line[i] == '-') + color = DIFF_FILE_OLD; + else if (line[i] == '+') + color = DIFF_FILE_NEW; + } + color_diff(ecbdata->color_diff, color); + } + fwrite(line, len, 1, stdout); + color_diff(ecbdata->color_diff, DIFF_PLAIN); } static char *pprint_rename(const char *a, const char *b) @@ -549,25 +594,35 @@ static void builtin_diff(const char *name_a, b_two = quote_two("b/", name_b); lbl[0] = DIFF_FILE_VALID(one) ? a_one : "/dev/null"; lbl[1] = DIFF_FILE_VALID(two) ? b_two : "/dev/null"; + color_diff(o->color_diff, DIFF_METAINFO); printf("diff --git %s %s\n", a_one, b_two); if (lbl[0][0] == '/') { /* /dev/null */ + color_diff(o->color_diff, DIFF_METAINFO); printf("new file mode %06o\n", two->mode); - if (xfrm_msg && xfrm_msg[0]) + if (xfrm_msg && xfrm_msg[0]) { + color_diff(o->color_diff, DIFF_METAINFO); puts(xfrm_msg); + } } else if (lbl[1][0] == '/') { printf("deleted file mode %06o\n", one->mode); - if (xfrm_msg && xfrm_msg[0]) + if (xfrm_msg && xfrm_msg[0]) { + color_diff(o->color_diff, DIFF_METAINFO); puts(xfrm_msg); + } } else { if (one->mode != two->mode) { + color_diff(o->color_diff, DIFF_METAINFO); printf("old mode %06o\n", one->mode); + color_diff(o->color_diff, DIFF_METAINFO); printf("new mode %06o\n", two->mode); } - if (xfrm_msg && xfrm_msg[0]) + if (xfrm_msg && xfrm_msg[0]) { + color_diff(o->color_diff, DIFF_METAINFO); puts(xfrm_msg); + } /* * we do not run diff between different kind * of objects. @@ -575,6 +630,7 @@ static void builtin_diff(const char *name_a, if ((one->mode ^ two->mode) & S_IFMT) goto free_ab_and_return; if (complete_rewrite) { + color_diff(o->color_diff, DIFF_PLAIN); emit_rewrite_diff(name_a, name_b, one, two); goto free_ab_and_return; } @@ -602,7 +658,9 @@ static void builtin_diff(const char *name_a, xdemitcb_t ecb; struct emit_callback ecbdata; + memset(&ecbdata, 0, sizeof(ecbdata)); ecbdata.label_path = lbl; + ecbdata.color_diff = o->color_diff; xpp.flags = XDF_NEED_MINIMAL; xecfg.ctxlen = o->context; xecfg.flags = XDL_EMIT_FUNCNAMES; @@ -612,8 +670,9 @@ static void builtin_diff(const char *name_a, xecfg.ctxlen = strtoul(diffopts + 10, NULL, 10); else if (!strncmp(diffopts, "-u", 2)) xecfg.ctxlen = strtoul(diffopts + 2, NULL, 10); - ecb.outf = fn_out; + ecb.outf = xdiff_outf; ecb.priv = &ecbdata; + ecbdata.xm.consume = fn_out_consume; xdl_diff(&mf1, &mf2, &xpp, &xecfg, &ecb); } @@ -1456,6 +1515,8 @@ int diff_opt_parse(struct diff_options *options, const char **av, int ac) else if (40 < options->abbrev) options->abbrev = 40; } + else if (!strcmp(arg, "--color")) + options->color_diff = 1; else return 0; return 1; diff --git a/diff.h b/diff.h index 4fc597c5942..de9de574e78 100644 --- a/diff.h +++ b/diff.h @@ -32,7 +32,8 @@ struct diff_options { full_index:1, silent_on_remove:1, find_copies_harder:1, - summary:1; + summary:1, + color_diff:1; int context; int break_opt; int detect_rename; diff --git a/pager.c b/pager.c index 9a309390166..2d186e8bde1 100644 --- a/pager.c +++ b/pager.c @@ -46,7 +46,7 @@ void setup_pager(void) close(fd[0]); close(fd[1]); - setenv("LESS", "-S", 0); + setenv("LESS", "-RS", 0); run_pager(pager); die("unable to execute pager '%s'", pager); exit(255);