From 5eec27e35f0a6231d2b0c50d94c726a67f14b23e Mon Sep 17 00:00:00 2001 From: Thomas Rast Date: Fri, 29 May 2009 17:09:42 +0200 Subject: [PATCH 01/10] git-svn: let 'dcommit $rev' work on $rev instead of HEAD 'git svn dcommit' takes an optional revision argument, but the meaning of it was rather scary. It completely ignored the current state of the HEAD, only looking at the revisions between SVN and $rev. If HEAD was attached to $branch, the branch lost all commits $rev..$branch in the process. Considering that 'git svn dcommit HEAD^' has the intuitive meaning "dcommit all changes on my branch except the last one", we change the meaning of the revision argument. git-svn temporarily checks out $rev for its work, meaning that * if a branch is specified, that branch (_not_ the HEAD) is rebased as part of the dcommit, * if some other revision is specified, as in the example, all work happens on a detached HEAD and no branch is affected. Signed-off-by: Thomas Rast Acked-by: Eric Wong --- Documentation/git-svn.txt | 5 +++-- git-svn.perl | 34 ++++++++++++++++++++++++++++++++-- t/t9100-git-svn-basic.sh | 19 +++++++++++++++++++ 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/Documentation/git-svn.txt b/Documentation/git-svn.txt index bb22d8e71..5027f9fbd 100644 --- a/Documentation/git-svn.txt +++ b/Documentation/git-svn.txt @@ -170,8 +170,9 @@ and have no uncommitted changes. It is recommended that you run 'git-svn' fetch and rebase (not pull or merge) your commits against the latest changes in the SVN repository. - An optional command-line argument may be specified as an - alternative to HEAD. + An optional revision or branch argument may be specified, and + causes 'git-svn' to do all work on that revision/branch + instead of HEAD. This is advantageous over 'set-tree' (below) because it produces cleaner, more linear history. + diff --git a/git-svn.perl b/git-svn.perl index 33017974d..33fe34cba 100755 --- a/git-svn.perl +++ b/git-svn.perl @@ -454,8 +454,22 @@ sub cmd_dcommit { 'Cannot dcommit with a dirty index. Commit your changes first, ' . "or stash them with `git stash'.\n"; $head ||= 'HEAD'; + + my $old_head; + if ($head ne 'HEAD') { + $old_head = eval { + command_oneline([qw/symbolic-ref -q HEAD/]) + }; + if ($old_head) { + $old_head =~ s{^refs/heads/}{}; + } else { + $old_head = eval { command_oneline(qw/rev-parse HEAD/) }; + } + command(['checkout', $head], STDERR => 0); + } + my @refs; - my ($url, $rev, $uuid, $gs) = working_head_info($head, \@refs); + my ($url, $rev, $uuid, $gs) = working_head_info('HEAD', \@refs); unless ($gs) { die "Unable to determine upstream SVN information from ", "$head history.\nPerhaps the repository is empty."; @@ -541,7 +555,7 @@ sub cmd_dcommit { if (@diff) { @refs = (); my ($url_, $rev_, $uuid_, $gs_) = - working_head_info($head, \@refs); + working_head_info('HEAD', \@refs); my ($linear_refs_, $parents_) = linearize_history($gs_, \@refs); if (scalar(@$linear_refs) != @@ -579,6 +593,22 @@ sub cmd_dcommit { } } } + + if ($old_head) { + my $new_head = command_oneline(qw/rev-parse HEAD/); + my $new_is_symbolic = eval { + command_oneline(qw/symbolic-ref -q HEAD/); + }; + if ($new_is_symbolic) { + print "dcommitted the branch ", $head, "\n"; + } else { + print "dcommitted on a detached HEAD because you gave ", + "a revision argument.\n", + "The rewritten commit is: ", $new_head, "\n"; + } + command(['checkout', $old_head], STDERR => 0); + } + unlink $gs->{index}; } diff --git a/t/t9100-git-svn-basic.sh b/t/t9100-git-svn-basic.sh index 64aa7e2f1..570e0359e 100755 --- a/t/t9100-git-svn-basic.sh +++ b/t/t9100-git-svn-basic.sh @@ -231,6 +231,25 @@ test_expect_success \ "^:refs/${remotes_git_svn}$" ' +test_expect_success 'dcommit $rev does not clobber current branch' ' + git svn fetch -i bar && + git checkout -b my-bar refs/remotes/bar && + echo 1 > foo && + git add foo && + git commit -m "change 1" && + echo 2 > foo && + git add foo && + git commit -m "change 2" && + old_head=$(git rev-parse HEAD) && + git svn dcommit -i bar HEAD^ && + test $old_head = $(git rev-parse HEAD) && + test refs/heads/my-bar = $(git symbolic-ref HEAD) && + git log refs/remotes/bar | grep "change 1" && + ! git log refs/remotes/bar | grep "change 2" && + git checkout master && + git branch -D my-bar + ' + test_expect_success 'able to dcommit to a subdirectory' " git svn fetch -i bar && git checkout -b my-bar refs/remotes/bar && From 9a8c92ac9e4698da150a413ce80f185005447f4c Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Sat, 30 May 2009 18:17:06 -0700 Subject: [PATCH 02/10] Add 'git svn help [cmd]' which works outside a repo. Previously there was no explicit 'help' command, but 'git svn help' still printed the usage message (as an invalid command), provided you got past the initialization steps that required a valid repo. Signed-off-by: Ben Jackson Acked-by: Eric Wong --- git-svn.perl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/git-svn.perl b/git-svn.perl index 33fe34cba..da1e1f64e 100755 --- a/git-svn.perl +++ b/git-svn.perl @@ -219,6 +219,9 @@ BEGIN $cmd = $ARGV[$i]; splice @ARGV, $i, 1; last; + } elsif ($ARGV[$i] eq 'help') { + $cmd = $ARGV[$i+1]; + usage(0); } }; From ca5e880ec2172f33dd0113129775f6eb65f2f678 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Wed, 3 Jun 2009 20:45:51 -0700 Subject: [PATCH 03/10] git-svn: speed up find_rev_before By limiting start revision of find_rev_before to max existing revision. This avoids a long wait if you do 'git svn reset -r 9999999'. The linear search within the contiguous revisions doesn't seem to be a problem. [ew: expanded commit message] Signed-off-by: Ben Jackson Acked-by: Eric Wong --- git-svn.perl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/git-svn.perl b/git-svn.perl index da1e1f64e..f9a672d77 100755 --- a/git-svn.perl +++ b/git-svn.perl @@ -3171,6 +3171,8 @@ sub find_rev_before { my ($self, $rev, $eq_ok, $min_rev) = @_; --$rev unless $eq_ok; $min_rev ||= 1; + my $max_rev = $self->rev_map_max; + $rev = $max_rev if ($rev > $max_rev); while ($rev >= $min_rev) { if (my $c = $self->rev_map_get($rev)) { return ($rev, $c); From 195643f2fc80b4d06a75b954b9a8ef2300976755 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Wed, 3 Jun 2009 20:45:52 -0700 Subject: [PATCH 04/10] Add 'git svn reset' to unwind 'git svn fetch' Add a command to unwind the effects of fetch by moving the rev_map and refs/remotes/git-svn back to an old SVN revision. This allows revisions to be re-fetched. Ideally SVN revs would be immutable, but permissions changes in the SVN repository or indiscriminate use of '--ignore-paths' can create situations where fetch cannot make progress. Signed-off-by: Ben Jackson Acked-by: Eric Wong --- Documentation/git-svn.txt | 59 +++++++++++++++++++++++++++++++++- git-svn.perl | 45 +++++++++++++++++++++++--- t/t9139-git-svn-reset.sh | 66 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 164 insertions(+), 6 deletions(-) create mode 100755 t/t9139-git-svn-reset.sh diff --git a/Documentation/git-svn.txt b/Documentation/git-svn.txt index 5027f9fbd..3f0fa5e6f 100644 --- a/Documentation/git-svn.txt +++ b/Documentation/git-svn.txt @@ -216,7 +216,7 @@ config key: svn.commiturl (overwrites all svn-remote..commiturl options) The following features from `svn log' are supported: + -- ---revision=[:];; +-r/--revision=[:];; is supported, non-numeric args are not: HEAD, NEXT, BASE, PREV, etc ... -v/--verbose;; @@ -314,6 +314,63 @@ Any other arguments are passed directly to 'git-log' Shows the Subversion externals. Use -r/--revision to specify a specific revision. +'reset':: + Undoes the effects of 'fetch' back to the specified revision. + This allows you to re-'fetch' an SVN revision. Normally the + contents of an SVN revision should never change and 'reset' + should not be necessary. However, if SVN permissions change, + or if you alter your --ignore-paths option, a 'fetch' may fail + with "not found in commit" (file not previously visible) or + "checksum mismatch" (missed a modification). If the problem + file cannot be ignored forever (with --ignore-paths) the only + way to repair the repo is to use 'reset'. + +Only the rev_map and refs/remotes/git-svn are changed. Follow 'reset' +with a 'fetch' and then 'git-reset' or 'git-rebase' to move local +branches onto the new tree. + +-r/--revision=;; + Specify the most recent revision to keep. All later revisions + are discarded. +-p/--parent;; + Discard the specified revision as well, keeping the nearest + parent instead. +Example:;; +Assume you have local changes in "master", but you need to refetch "r2". + +------------ + r1---r2---r3 remotes/git-svn + \ + A---B master +------------ + +Fix the ignore-paths or SVN permissions problem that caused "r2" to +be incomplete in the first place. Then: + +[verse] +git svn reset -r2 -p +git svn fetch + +------------ + r1---r2'--r3' remotes/git-svn + \ + r2---r3---A---B master +------------ + +Then fixup "master" with 'git-rebase'. +Do NOT use 'git-merge' or your history will not be compatible with a +future 'dcommit'! + +[verse] +git rebase --onto remotes/git-svn A^ master + +------------ + r1---r2'--r3' remotes/git-svn + \ + A'--B' master +------------ + + -- OPTIONS diff --git a/git-svn.perl b/git-svn.perl index f9a672d77..b1245cbab 100755 --- a/git-svn.perl +++ b/git-svn.perl @@ -211,6 +211,10 @@ BEGIN 'blame' => [ \&Git::SVN::Log::cmd_blame, "Show what revision and author last modified each line of a file", { 'git-format' => \$_git_format } ], + 'reset' => [ \&cmd_reset, + "Undo fetches back to the specified SVN revision", + { 'revision|r=s' => \$_revision, + 'parent|p' => \$_fetch_parent } ], ); my $cmd; @@ -1054,6 +1058,20 @@ sub cmd_info { print $result, "\n"; } +sub cmd_reset { + my $target = shift || $_revision or die "SVN revision required\n"; + $target = $1 if $target =~ /^r(\d+)$/; + $target =~ /^\d+$/ or die "Numeric SVN revision expected\n"; + my ($url, $rev, $uuid, $gs) = working_head_info('HEAD'); + unless ($gs) { + die "Unable to determine upstream SVN information from ". + "history\n"; + } + my ($r, $c) = $gs->find_rev_before($target, not $_fetch_parent); + $gs->rev_map_set($r, $c, 'reset', $uuid); + print "r$r = $c ($gs->{ref_id})\n"; +} + ########################### utility functions ######################### sub rebase_cmd { @@ -3023,6 +3041,14 @@ sub _rev_map_set { croak "write: $!"; } +sub _rev_map_reset { + my ($fh, $rev, $commit) = @_; + my $c = _rev_map_get($fh, $rev); + $c eq $commit or die "_rev_map_reset(@_) commit $c does not match!\n"; + my $offset = sysseek($fh, 0, SEEK_CUR) or croak "seek: $!"; + truncate $fh, $offset or croak "truncate: $!"; +} + sub mkfile { my ($path) = @_; unless (-e $path) { @@ -3039,6 +3065,7 @@ sub rev_map_set { my $db = $self->map_path($uuid); my $db_lock = "$db.lock"; my $sig; + $update_ref ||= 0; if ($update_ref) { $SIG{INT} = $SIG{HUP} = $SIG{TERM} = $SIG{ALRM} = $SIG{PIPE} = $SIG{USR1} = $SIG{USR2} = sub { $sig = $_[0] }; @@ -3062,7 +3089,8 @@ sub rev_map_set { sysopen(my $fh, $db_lock, O_RDWR | O_CREAT) or croak "Couldn't open $db_lock: $!\n"; - _rev_map_set($fh, $rev, $commit); + $update_ref eq 'reset' ? _rev_map_reset($fh, $rev, $commit) : + _rev_map_set($fh, $rev, $commit); if ($sync) { $fh->flush or die "Couldn't flush $db_lock: $!\n"; $fh->sync or die "Couldn't sync $db_lock: $!\n"; @@ -3070,7 +3098,9 @@ sub rev_map_set { close $fh or croak $!; if ($update_ref) { $_head = $self; - command_noisy('update-ref', '-m', "r$rev", + my $note = ""; + $note = " ($update_ref)" if ($update_ref !~ /^\d*$/); + command_noisy('update-ref', '-m', "r$rev$note", $self->refname, $commit); } rename $db_lock, $db or die "rev_map_set(@_): ", "Failed to rename: ", @@ -3132,12 +3162,19 @@ sub rev_map_get { return undef unless -e $map_path; sysopen(my $fh, $map_path, O_RDONLY) or croak "open: $!"; + my $c = _rev_map_get($fh, $rev); + close($fh) or croak "close: $!"; + $c +} + +sub _rev_map_get { + my ($fh, $rev) = @_; + binmode $fh or croak "binmode: $!"; my $size = (stat($fh))[7]; ($size % 24) == 0 or croak "inconsistent size: $size"; if ($size == 0) { - close $fh or croak "close: $fh"; return undef; } @@ -3155,11 +3192,9 @@ sub rev_map_get { } elsif ($r > $rev) { $u = $i - 24; } else { # $r == $rev - close($fh) or croak "close: $!"; return $c eq ('0' x 40) ? undef : $c; } } - close($fh) or croak "close: $!"; undef; } diff --git a/t/t9139-git-svn-reset.sh b/t/t9139-git-svn-reset.sh new file mode 100755 index 000000000..0735526d4 --- /dev/null +++ b/t/t9139-git-svn-reset.sh @@ -0,0 +1,66 @@ +#!/bin/sh +# +# Copyright (c) 2009 Ben Jackson +# + +test_description='git svn reset' +. ./lib-git-svn.sh + +test_expect_success 'setup test repository' ' + svn_cmd co "$svnrepo" s && + ( + cd s && + mkdir vis && + echo always visible > vis/vis.txt && + svn_cmd add vis && + svn_cmd commit -m "create visible files" && + mkdir hid && + echo initially hidden > hid/hid.txt && + svn_cmd add hid && + svn_cmd commit -m "create initially hidden files" && + svn_cmd up && + echo mod >> vis/vis.txt && + svn_cmd commit -m "modify vis" && + svn_cmd up + ) +' + +test_expect_success 'clone SVN repository with hidden directory' ' + git svn init "$svnrepo" g && + ( cd g && git svn fetch --ignore-paths="^hid" ) +' + +test_expect_success 'modify hidden file in SVN repo' ' + ( cd s && + echo mod hidden >> hid/hid.txt && + svn_cmd commit -m "modify hid" && + svn_cmd up + ) +' + +test_expect_success 'fetch fails on modified hidden file' ' + ( cd g && + git svn find-rev refs/remotes/git-svn > ../expect && + ! git svn fetch 2> ../errors && + git svn find-rev refs/remotes/git-svn > ../expect2 ) && + fgrep "not found in commit" errors && + test_cmp expect expect2 +' + +test_expect_success 'reset unwinds back to r1' ' + ( cd g && + git svn reset -r1 && + git svn find-rev refs/remotes/git-svn > ../expect2 ) && + echo 1 >expect && + test_cmp expect expect2 +' + +test_expect_success 'refetch succeeds not ignoring any files' ' + ( cd g && + git svn fetch && + git svn rebase && + fgrep "mod hidden" hid/hid.txt + ) +' + +test_done From 6224406914c3d3fecec73e32ed271dbdc0c8736a Mon Sep 17 00:00:00 2001 From: Marc Branchaud Date: Tue, 23 Jun 2009 13:02:08 -0400 Subject: [PATCH 05/10] git svn: Support multiple branch and tag paths in the svn repository. This enables git-svn.perl to read multiple 'branches' and 'tags' entries in svn-remote config sections. The init and clone subcommands also support multiple --branches and --tags arguments. The branch (and tag) subcommand gets a new argument: --destination (or -d). This argument is required if there are multiple branches (or tags) entries configured for the remote Subversion repository. The argument's value specifies which branch (or tag) path to use to create the branch (or tag). The specified value must match the left side (without wildcards) of one of the branches (or tags) refspecs in the svn-remote's config. [ew: avoided explicit loop when combining globs with "push"] Signed-off-by: Marc Branchaud Acked-by: Eric Wong --- git-svn.perl | 78 ++++++++++++------ t/t9138-git-svn-multiple-branches.sh | 114 +++++++++++++++++++++++++++ 2 files changed, 170 insertions(+), 22 deletions(-) create mode 100755 t/t9138-git-svn-multiple-branches.sh diff --git a/git-svn.perl b/git-svn.perl index b1245cbab..48e8aad00 100755 --- a/git-svn.perl +++ b/git-svn.perl @@ -63,7 +63,7 @@ BEGIN $sha1 = qr/[a-f\d]{40}/; $sha1_short = qr/[a-f\d]{4,40}/; my ($_stdin, $_help, $_edit, - $_message, $_file, + $_message, $_file, $_branch_dest, $_template, $_shared, $_version, $_fetch_all, $_no_rebase, $_fetch_parent, $_merge, $_strategy, $_dry_run, $_local, @@ -92,11 +92,11 @@ BEGIN 'localtime' => \$Git::SVN::_localtime, %remote_opts ); -my ($_trunk, $_tags, $_branches, $_stdlayout); +my ($_trunk, @_tags, @_branches, $_stdlayout); my %icv; my %init_opts = ( 'template=s' => \$_template, 'shared:s' => \$_shared, - 'trunk|T=s' => \$_trunk, 'tags|t=s' => \$_tags, - 'branches|b=s' => \$_branches, 'prefix=s' => \$_prefix, + 'trunk|T=s' => \$_trunk, 'tags|t=s@' => \@_tags, + 'branches|b=s@' => \@_branches, 'prefix=s' => \$_prefix, 'stdlayout|s' => \$_stdlayout, 'minimize-url|m' => \$Git::SVN::_minimize_url, 'no-metadata' => sub { $icv{noMetadata} = 1 }, @@ -141,11 +141,13 @@ BEGIN branch => [ \&cmd_branch, 'Create a branch in the SVN repository', { 'message|m=s' => \$_message, + 'destination|d=s' => \$_branch_dest, 'dry-run|n' => \$_dry_run, 'tag|t' => \$_tag } ], tag => [ sub { $_tag = 1; cmd_branch(@_) }, 'Create a tag in the SVN repository', { 'message|m=s' => \$_message, + 'destination|d=s' => \$_branch_dest, 'dry-run|n' => \$_dry_run } ], 'set-tree' => [ \&cmd_set_tree, "Set an SVN repository to a git tree-ish", @@ -365,7 +367,7 @@ sub init_subdir { sub cmd_clone { my ($url, $path) = @_; if (!defined $path && - (defined $_trunk || defined $_branches || defined $_tags || + (defined $_trunk || @_branches || @_tags || defined $_stdlayout) && $url !~ m#^[a-z\+]+://#) { $path = $url; @@ -379,10 +381,10 @@ sub cmd_clone { sub cmd_init { if (defined $_stdlayout) { $_trunk = 'trunk' if (!defined $_trunk); - $_tags = 'tags' if (!defined $_tags); - $_branches = 'branches' if (!defined $_branches); + @_tags = 'tags' if (! @_tags); + @_branches = 'branches' if (! @_branches); } - if (defined $_trunk || defined $_branches || defined $_tags) { + if (defined $_trunk || @_branches || @_tags) { return cmd_multi_init(@_); } my $url = shift or die "SVN repository location required ", @@ -630,7 +632,31 @@ sub cmd_branch { my ($src, $rev, undef, $gs) = working_head_info($head); my $remote = Git::SVN::read_all_remotes()->{$gs->{repo_id}}; - my $glob = $remote->{ $_tag ? 'tags' : 'branches' }; + my $allglobs = $remote->{ $_tag ? 'tags' : 'branches' }; + my $glob; + if ($#{$allglobs} == 0) { + $glob = $allglobs->[0]; + } else { + unless(defined $_branch_dest) { + die "Multiple ", + $_tag ? "tag" : "branch", + " paths defined for Subversion repository.\n", + "You must specify where you want to create the ", + $_tag ? "tag" : "branch", + " with the --destination argument.\n"; + } + foreach my $g (@{$allglobs}) { + if ($_branch_dest eq $g->{path}->{left}) { + $glob = $g; + last; + } + } + unless (defined $glob) { + die "Unknown ", + $_tag ? "tag" : "branch", + " destination $_branch_dest\n"; + } + } my ($lft, $rgt) = @{ $glob->{path} }{qw/left right/}; my $dst = join '/', $remote->{url}, $lft, $branch_name, ($rgt || ()); @@ -837,7 +863,7 @@ sub cmd_proplist { sub cmd_multi_init { my $url = shift; - unless (defined $_trunk || defined $_branches || defined $_tags) { + unless (defined $_trunk || @_branches || @_tags) { usage(1); } @@ -862,10 +888,14 @@ sub cmd_multi_init { undef, $trunk_ref); } } - return unless defined $_branches || defined $_tags; + return unless @_branches || @_tags; my $ra = $url ? Git::SVN::Ra->new($url) : undef; - complete_url_ls_init($ra, $_branches, '--branches/-b', $_prefix); - complete_url_ls_init($ra, $_tags, '--tags/-t', $_prefix . 'tags/'); + foreach my $path (@_branches) { + complete_url_ls_init($ra, $path, '--branches/-b', $_prefix); + } + foreach my $path (@_tags) { + complete_url_ls_init($ra, $path, '--tags/-t', $_prefix.'tags/'); + } } sub cmd_multi_fetch { @@ -1150,6 +1180,7 @@ sub complete_url_ls_init { die "--prefix='$pfx' must have a trailing slash '/'\n"; } command_noisy('config', + '--add', "svn-remote.$gs->{repo_id}.$n", "$remote_path:refs/remotes/$pfx*" . ('/*' x (($remote_path =~ tr/*/*/) - 1)) ); @@ -1616,7 +1647,8 @@ sub fetch_all { # read the max revs for wildcard expansion (branches/*, tags/*) foreach my $t (qw/branches tags/) { defined $remote->{$t} or next; - push @globs, $remote->{$t}; + push @globs, @{$remote->{$t}}; + my $max_rev = eval { tmp_config(qw/--int --get/, "svn-remote.$repo_id.${t}-maxRev") }; if (defined $max_rev && ($max_rev < $base)) { @@ -1663,15 +1695,16 @@ sub read_all_remotes { } elsif (m!^(.+)\.(branches|tags)= (.*):refs/remotes/(.+)\s*$/!x) { my ($p, $g) = ($3, $4); - my $rs = $r->{$1}->{$2} = { - t => $2, - remote => $1, - path => Git::SVN::GlobSpec->new($p), - ref => Git::SVN::GlobSpec->new($g) }; + my $rs = { + t => $2, + remote => $1, + path => Git::SVN::GlobSpec->new($p), + ref => Git::SVN::GlobSpec->new($g) }; if (length($rs->{ref}->{right}) != 0) { die "The '*' glob character must be the last ", "character of '$g'\n"; } + push @{ $r->{$1}->{$2} }, $rs; } } @@ -1811,9 +1844,10 @@ sub find_by_url { # repos_root and, path are optional next if defined $repos_root && $repos_root ne $u; my $fetch = $remotes->{$repo_id}->{fetch} || {}; - foreach (qw/branches tags/) { - resolve_local_globs($u, $fetch, - $remotes->{$repo_id}->{$_}); + foreach my $t (qw/branches tags/) { + foreach my $globspec (@{$remotes->{$repo_id}->{$t}}) { + resolve_local_globs($u, $fetch, $globspec); + } } my $p = $path; my $rwr = rewrite_root({repo_id => $repo_id}); diff --git a/t/t9138-git-svn-multiple-branches.sh b/t/t9138-git-svn-multiple-branches.sh new file mode 100755 index 000000000..9725ccf9d --- /dev/null +++ b/t/t9138-git-svn-multiple-branches.sh @@ -0,0 +1,114 @@ +#!/bin/sh +# +# Copyright (c) 2009 Marc Branchaud +# + +test_description='git svn multiple branch and tag paths in the svn repo' +. ./lib-git-svn.sh + +test_expect_success 'setup svnrepo' ' + mkdir project \ + project/trunk \ + project/b_one \ + project/b_two \ + project/tags_A \ + project/tags_B && + echo 1 > project/trunk/a.file && + svn import -m "$test_description" project "$svnrepo/project" && + rm -rf project && + svn cp -m "Branch 1" "$svnrepo/project/trunk" \ + "$svnrepo/project/b_one/first" && + svn cp -m "Tag 1" "$svnrepo/project/trunk" \ + "$svnrepo/project/tags_A/1.0" && + svn co "$svnrepo/project" svn_project && + cd svn_project && + . + echo 2 > trunk/a.file && + svn ci -m "Change 1" trunk/a.file && + svn cp -m "Branch 2" "$svnrepo/project/trunk" \ + "$svnrepo/project/b_one/second" && + svn cp -m "Tag 2" "$svnrepo/project/trunk" \ + "$svnrepo/project/tags_A/2.0" && + echo 3 > trunk/a.file && + svn ci -m "Change 2" trunk/a.file && + svn cp -m "Branch 3" "$svnrepo/project/trunk" \ + "$svnrepo/project/b_two/1" && + svn cp -m "Tag 3" "$svnrepo/project/trunk" \ + "$svnrepo/project/tags_A/3.0" && + echo 4 > trunk/a.file && + svn ci -m "Change 3" trunk/a.file && + svn cp -m "Branch 4" "$svnrepo/project/trunk" \ + "$svnrepo/project/b_two/2" && + svn cp -m "Tag 4" "$svnrepo/project/trunk" \ + "$svnrepo/project/tags_A/4.0" && + svn up && + echo 5 > b_one/first/a.file && + svn ci -m "Change 4" b_one/first/a.file && + svn cp -m "Tag 5" "$svnrepo/project/b_one/first" \ + "$svnrepo/project/tags_B/v5" && + echo 6 > b_one/second/a.file && + svn ci -m "Change 5" b_one/second/a.file && + svn cp -m "Tag 6" "$svnrepo/project/b_one/second" \ + "$svnrepo/project/tags_B/v6" && + echo 7 > b_two/1/a.file && + svn ci -m "Change 6" b_two/1/a.file && + svn cp -m "Tag 7" "$svnrepo/project/b_two/1" \ + "$svnrepo/project/tags_B/v7" && + echo 8 > b_two/2/a.file && + svn ci -m "Change 7" b_two/2/a.file && + svn cp -m "Tag 8" "$svnrepo/project/b_two/2" \ + "$svnrepo/project/tags_B/v8" && + cd .. + ' + +test_expect_success 'clone multiple branch and tag paths' ' + git svn clone -T trunk \ + -b b_one/* --branches b_two/* \ + -t tags_A/* --tags tags_B \ + "$svnrepo/project" git_project && + cd git_project && + git rev-parse refs/remotes/first && + git rev-parse refs/remotes/second && + git rev-parse refs/remotes/1 && + git rev-parse refs/remotes/2 && + git rev-parse refs/remotes/tags/1.0 && + git rev-parse refs/remotes/tags/2.0 && + git rev-parse refs/remotes/tags/3.0 && + git rev-parse refs/remotes/tags/4.0 && + git rev-parse refs/remotes/tags/v5 && + git rev-parse refs/remotes/tags/v6 && + git rev-parse refs/remotes/tags/v7 && + git rev-parse refs/remotes/tags/v8 && + cd .. + ' + +test_expect_success 'Multiple branch or tag paths require -d' ' + cd git_project && + test_must_fail git svn branch -m "No new branch" Nope && + test_must_fail git svn tag -m "No new tag" Tagless && + test_must_fail git rev-parse refs/remotes/Nope && + test_must_fail git rev-parse refs/remotes/tags/Tagless && + cd ../svn_project && + svn up && + test_must_fail test -d b_one/Nope && + test_must_fail test -d b_two/Nope && + test_must_fail test -d tags_A/Tagless && + test_must_fail test -d tags_B/Tagless && + cd .. + ' + +test_expect_success 'create new branches and tags' ' + ( cd git_project && git svn branch -m "New branch 1" -d project/b_one New1 ) && + ( cd svn_project && svn up && test -e b_one/New1/a.file ) && + + ( cd git_project && git svn branch -m "New branch 2" -d project/b_two New2 ) && + ( cd svn_project && svn up && test -e b_two/New2/a.file ) && + + ( cd git_project && git svn branch -t -m "New tag 1" -d project/tags_A Tag1 ) && + ( cd svn_project && svn up && test -e tags_A/Tag1/a.file ) + + ( cd git_project && git svn tag -m "New tag 2" -d project/tags_B Tag2 ) && + ( cd svn_project && svn up && test -e tags_B/Tag2/a.file ) + ' + +test_done From f7050599310c18bd67b35b8d59486116b30ff1f6 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Thu, 25 Jun 2009 02:28:15 -0700 Subject: [PATCH 06/10] git-svn: convert globs to regexps for branch destinations Marc Branchaud wrote: > I'm fairly happy with this, except for the way the branch > subcommand matches refspecs. The patch does a simple string > comparison, but it'd be better to do an actual glob. I just > couldn't track down the right function for that, so I left it as > a strcmp and hope that a gitizen can tell me how to glob here. Signed-off-by: Eric Wong --- git-svn.perl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/git-svn.perl b/git-svn.perl index 48e8aad00..6c42e2afc 100755 --- a/git-svn.perl +++ b/git-svn.perl @@ -646,7 +646,9 @@ sub cmd_branch { " with the --destination argument.\n"; } foreach my $g (@{$allglobs}) { - if ($_branch_dest eq $g->{path}->{left}) { + # SVN::Git::Editor could probably be moved to Git.pm.. + my $re = SVN::Git::Editor::glob2pat($g->{path}->{left}); + if ($_branch_dest =~ /$re/) { $glob = $g; last; } From 2317d289fefbe04fa57379fa6d115717bb25ba14 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Thu, 25 Jun 2009 16:09:59 -0700 Subject: [PATCH 07/10] t9138: remove stray dot in test which broke bash The stray dot broke bash and probably some other shells, but worked fine with dash in my limited testing. Signed-off-by: Eric Wong --- t/t9138-git-svn-multiple-branches.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/t/t9138-git-svn-multiple-branches.sh b/t/t9138-git-svn-multiple-branches.sh index 9725ccf9d..37ecdb08b 100755 --- a/t/t9138-git-svn-multiple-branches.sh +++ b/t/t9138-git-svn-multiple-branches.sh @@ -22,7 +22,6 @@ test_expect_success 'setup svnrepo' ' "$svnrepo/project/tags_A/1.0" && svn co "$svnrepo/project" svn_project && cd svn_project && - . echo 2 > trunk/a.file && svn ci -m "Change 1" trunk/a.file && svn cp -m "Branch 2" "$svnrepo/project/trunk" \ From 50ff23667020768fa18dcffd154406fc41ebaa84 Mon Sep 17 00:00:00 2001 From: Ulrich Dangel Date: Fri, 26 Jun 2009 16:52:09 +0200 Subject: [PATCH 08/10] git-svn: Canonicalize svn urls to prevent libsvn assertion Cloning/initializing svn repositories with an uncanonicalize url does not work as libsvn throws an assertion. This patch canonicalize svn uris for the clone and init command from git-svn. [ew: fixed trailing whitespace] Signed-off-by: Ulrich Dangel Acked-by: Eric Wong --- git-svn.perl | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/git-svn.perl b/git-svn.perl index 6c42e2afc..d1af1a3d2 100755 --- a/git-svn.perl +++ b/git-svn.perl @@ -389,6 +389,7 @@ sub cmd_init { } my $url = shift or die "SVN repository location required ", "as a command-line argument\n"; + $url = canonicalize_url($url); init_subdir(@_); do_git_init_db(); @@ -806,6 +807,12 @@ sub canonicalize_path { return $path; } +sub canonicalize_url { + my ($url) = @_; + $url =~ s#^([^:]+://[^/]*/)(.*)$#$1 . canonicalize_path($2)#e; + return $url; +} + # get_svnprops(PATH) # ------------------ # Helper for cmd_propget and cmd_proplist below. @@ -875,7 +882,7 @@ sub cmd_multi_init { $_prefix = '' unless defined $_prefix; if (defined $url) { - $url =~ s#/+$##; + $url = canonicalize_url($url); init_subdir(@_); } do_git_init_db(); From b5c9b38bc3f7a1151dc6b1af1f3fabe9d1c4aefa Mon Sep 17 00:00:00 2001 From: Marc Branchaud Date: Fri, 26 Jun 2009 17:08:19 -0400 Subject: [PATCH 09/10] git svn: cleanup t9138-multiple-branches Using the "svn_cmd" wrapper instead of "svn" alone allows tests to run consistently for users with customized ~/.subversion/configs. Additionally, using subshells via "(cd ...)" allow cleaner and less error-prone tests to be written. [ew: expanded commit message] Signed-off-by: Marc Branchaud Acked-by: Eric Wong --- t/t9138-git-svn-multiple-branches.sh | 123 ++++++++++++++------------- 1 file changed, 66 insertions(+), 57 deletions(-) diff --git a/t/t9138-git-svn-multiple-branches.sh b/t/t9138-git-svn-multiple-branches.sh index 37ecdb08b..cb9a6d229 100755 --- a/t/t9138-git-svn-multiple-branches.sh +++ b/t/t9138-git-svn-multiple-branches.sh @@ -14,58 +14,58 @@ test_expect_success 'setup svnrepo' ' project/tags_A \ project/tags_B && echo 1 > project/trunk/a.file && - svn import -m "$test_description" project "$svnrepo/project" && + svn_cmd import -m "$test_description" project "$svnrepo/project" && rm -rf project && - svn cp -m "Branch 1" "$svnrepo/project/trunk" \ - "$svnrepo/project/b_one/first" && - svn cp -m "Tag 1" "$svnrepo/project/trunk" \ - "$svnrepo/project/tags_A/1.0" && - svn co "$svnrepo/project" svn_project && - cd svn_project && + svn_cmd cp -m "Branch 1" "$svnrepo/project/trunk" \ + "$svnrepo/project/b_one/first" && + svn_cmd cp -m "Tag 1" "$svnrepo/project/trunk" \ + "$svnrepo/project/tags_A/1.0" && + svn_cmd co "$svnrepo/project" svn_project && + ( cd svn_project && echo 2 > trunk/a.file && - svn ci -m "Change 1" trunk/a.file && - svn cp -m "Branch 2" "$svnrepo/project/trunk" \ - "$svnrepo/project/b_one/second" && - svn cp -m "Tag 2" "$svnrepo/project/trunk" \ - "$svnrepo/project/tags_A/2.0" && + svn_cmd ci -m "Change 1" trunk/a.file && + svn_cmd cp -m "Branch 2" "$svnrepo/project/trunk" \ + "$svnrepo/project/b_one/second" && + svn_cmd cp -m "Tag 2" "$svnrepo/project/trunk" \ + "$svnrepo/project/tags_A/2.0" && echo 3 > trunk/a.file && - svn ci -m "Change 2" trunk/a.file && - svn cp -m "Branch 3" "$svnrepo/project/trunk" \ - "$svnrepo/project/b_two/1" && - svn cp -m "Tag 3" "$svnrepo/project/trunk" \ - "$svnrepo/project/tags_A/3.0" && + svn_cmd ci -m "Change 2" trunk/a.file && + svn_cmd cp -m "Branch 3" "$svnrepo/project/trunk" \ + "$svnrepo/project/b_two/1" && + svn_cmd cp -m "Tag 3" "$svnrepo/project/trunk" \ + "$svnrepo/project/tags_A/3.0" && echo 4 > trunk/a.file && - svn ci -m "Change 3" trunk/a.file && - svn cp -m "Branch 4" "$svnrepo/project/trunk" \ - "$svnrepo/project/b_two/2" && - svn cp -m "Tag 4" "$svnrepo/project/trunk" \ - "$svnrepo/project/tags_A/4.0" && - svn up && + svn_cmd ci -m "Change 3" trunk/a.file && + svn_cmd cp -m "Branch 4" "$svnrepo/project/trunk" \ + "$svnrepo/project/b_two/2" && + svn_cmd cp -m "Tag 4" "$svnrepo/project/trunk" \ + "$svnrepo/project/tags_A/4.0" && + svn_cmd up && echo 5 > b_one/first/a.file && - svn ci -m "Change 4" b_one/first/a.file && - svn cp -m "Tag 5" "$svnrepo/project/b_one/first" \ - "$svnrepo/project/tags_B/v5" && + svn_cmd ci -m "Change 4" b_one/first/a.file && + svn_cmd cp -m "Tag 5" "$svnrepo/project/b_one/first" \ + "$svnrepo/project/tags_B/v5" && echo 6 > b_one/second/a.file && - svn ci -m "Change 5" b_one/second/a.file && - svn cp -m "Tag 6" "$svnrepo/project/b_one/second" \ - "$svnrepo/project/tags_B/v6" && + svn_cmd ci -m "Change 5" b_one/second/a.file && + svn_cmd cp -m "Tag 6" "$svnrepo/project/b_one/second" \ + "$svnrepo/project/tags_B/v6" && echo 7 > b_two/1/a.file && - svn ci -m "Change 6" b_two/1/a.file && - svn cp -m "Tag 7" "$svnrepo/project/b_two/1" \ - "$svnrepo/project/tags_B/v7" && + svn_cmd ci -m "Change 6" b_two/1/a.file && + svn_cmd cp -m "Tag 7" "$svnrepo/project/b_two/1" \ + "$svnrepo/project/tags_B/v7" && echo 8 > b_two/2/a.file && - svn ci -m "Change 7" b_two/2/a.file && - svn cp -m "Tag 8" "$svnrepo/project/b_two/2" \ - "$svnrepo/project/tags_B/v8" && - cd .. - ' + svn_cmd ci -m "Change 7" b_two/2/a.file && + svn_cmd cp -m "Tag 8" "$svnrepo/project/b_two/2" \ + "$svnrepo/project/tags_B/v8" + ) +' test_expect_success 'clone multiple branch and tag paths' ' git svn clone -T trunk \ -b b_one/* --branches b_two/* \ -t tags_A/* --tags tags_B \ "$svnrepo/project" git_project && - cd git_project && + ( cd git_project && git rev-parse refs/remotes/first && git rev-parse refs/remotes/second && git rev-parse refs/remotes/1 && @@ -77,37 +77,46 @@ test_expect_success 'clone multiple branch and tag paths' ' git rev-parse refs/remotes/tags/v5 && git rev-parse refs/remotes/tags/v6 && git rev-parse refs/remotes/tags/v7 && - git rev-parse refs/remotes/tags/v8 && - cd .. - ' + git rev-parse refs/remotes/tags/v8 + ) +' test_expect_success 'Multiple branch or tag paths require -d' ' - cd git_project && + ( cd git_project && test_must_fail git svn branch -m "No new branch" Nope && test_must_fail git svn tag -m "No new tag" Tagless && test_must_fail git rev-parse refs/remotes/Nope && - test_must_fail git rev-parse refs/remotes/tags/Tagless && - cd ../svn_project && - svn up && + test_must_fail git rev-parse refs/remotes/tags/Tagless + ) && + ( cd svn_project && + svn_cmd up && test_must_fail test -d b_one/Nope && test_must_fail test -d b_two/Nope && test_must_fail test -d tags_A/Tagless && - test_must_fail test -d tags_B/Tagless && - cd .. - ' + test_must_fail test -d tags_B/Tagless + ) +' test_expect_success 'create new branches and tags' ' - ( cd git_project && git svn branch -m "New branch 1" -d project/b_one New1 ) && - ( cd svn_project && svn up && test -e b_one/New1/a.file ) && + ( cd git_project && + git svn branch -m "New branch 1" -d project/b_one New1 ) && + ( cd svn_project && + svn_cmd up && test -e b_one/New1/a.file ) && - ( cd git_project && git svn branch -m "New branch 2" -d project/b_two New2 ) && - ( cd svn_project && svn up && test -e b_two/New2/a.file ) && + ( cd git_project && + git svn branch -m "New branch 2" -d project/b_two New2 ) && + ( cd svn_project && + svn_cmd up && test -e b_two/New2/a.file ) && - ( cd git_project && git svn branch -t -m "New tag 1" -d project/tags_A Tag1 ) && - ( cd svn_project && svn up && test -e tags_A/Tag1/a.file ) + ( cd git_project && + git svn branch -t -m "New tag 1" -d project/tags_A Tag1 ) && + ( cd svn_project && + svn_cmd up && test -e tags_A/Tag1/a.file ) && - ( cd git_project && git svn tag -m "New tag 2" -d project/tags_B Tag2 ) && - ( cd svn_project && svn up && test -e tags_B/Tag2/a.file ) - ' + ( cd git_project && + git svn tag -m "New tag 2" -d project/tags_B Tag2 ) && + ( cd svn_project && + svn_cmd up && test -e tags_B/Tag2/a.file ) +' test_done From ab81a3643ba3a68aae8a60d2c84f164b2c301b86 Mon Sep 17 00:00:00 2001 From: Marc Branchaud Date: Fri, 26 Jun 2009 16:49:19 -0400 Subject: [PATCH 10/10] git svn: Doc update for multiple branch and tag paths Signed-off-by: Marc Branchaud Acked-by: Eric Wong --- Documentation/git-svn.txt | 44 +++++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/Documentation/git-svn.txt b/Documentation/git-svn.txt index 3f0fa5e6f..7e9b9a042 100644 --- a/Documentation/git-svn.txt +++ b/Documentation/git-svn.txt @@ -3,7 +3,7 @@ git-svn(1) NAME ---- -git-svn - Bidirectional operation between a single Subversion branch and git +git-svn - Bidirectional operation between a Subversion repository and git SYNOPSIS -------- @@ -15,13 +15,12 @@ DESCRIPTION It provides a bidirectional flow of changes between a Subversion and a git repository. -'git-svn' can track a single Subversion branch simply by using a -URL to the branch, follow branches laid out in the Subversion recommended -method (trunk, branches, tags directories) with the --stdlayout option, or -follow branches in any layout with the -T/-t/-b options (see options to -'init' below, and also the 'clone' command). +'git-svn' can track a standard Subversion repository, +following the common "trunk/branches/tags" layout, with the --stdlayout option. +It can also follow branches and tags in any layout with the -T/-t/-b options +(see options to 'init' below, and also the 'clone' command). -Once tracking a Subversion branch (with any of the above methods), the git +Once tracking a Subversion repository (with any of the above methods), the git repository can be updated from Subversion by the 'fetch' command and Subversion updated from git by the 'dcommit' command. @@ -48,8 +47,11 @@ COMMANDS --stdlayout;; These are optional command-line options for init. Each of these flags can point to a relative repository path - (--tags=project/tags') or a full url - (--tags=https://foo.org/project/tags). The option --stdlayout is + (--tags=project/tags) or a full url + (--tags=https://foo.org/project/tags). + You can specify more than one --tags and/or --branches options, in case + your Subversion repository places tags or branches under multiple paths. + The option --stdlayout is a shorthand way of setting trunk,tags,branches as the relative paths, which is the Subversion default. If any of the other options are given as well, they take precedence. @@ -205,6 +207,20 @@ config key: svn.commiturl (overwrites all svn-remote..commiturl options) Create a tag by using the tags_subdir instead of the branches_subdir specified during git svn init. +-d;; +--destination;; + If more than one --branches (or --tags) option was given to the 'init' + or 'clone' command, you must provide the location of the branch (or + tag) you wish to create in the SVN repository. The value of this + option must match one of the paths specified by a --branches (or + --tags) option. You can see these paths with the commands ++ + git config --get-all svn-remote..branches + git config --get-all svn-remote..tags ++ +where is the name of the SVN repository as specified by the -R option to +'init' (or "svn" by default). + 'tag':: Create a tag in the SVN repository. This is a shorthand for 'branch -t'. @@ -727,6 +743,16 @@ already dcommitted. It is considered bad practice to --amend commits you've already pushed to a remote repository for other users, and dcommit with SVN is analogous to that. +When using multiple --branches or --tags, 'git-svn' does not automatically +handle name collisions (for example, if two branches from different paths have +the same name, or if a branch and a tag have the same name). In these cases, +use 'init' to set up your git repository then, before your first 'fetch', edit +the .git/config file so that the branches and tags are associated with +different name spaces. For example: + + branches = stable/*:refs/remotes/svn/stable/* + branches = debug/*:refs/remotes/svn/debug/* + BUGS ----