Rebasing all the time is fine if you simply want to use Git as a glorified Subversion repository mirror. Even that by itself is a great step forward: you get to work offline; you get faster log, blame, and diff operations; and you don’t annoy your coworkers who are perfectly happy using Subversion. Nobody even has to know you’re using Git.
But what if you want to do a little more than that? Maybe one of your coworkers wants to collaborate with you on a new feature using Git. Or perhaps you want to work on a few topic branches at a time and wait on committing them back to Subversion until you’re sure they’re ready. Most of all, maybe you find Subversion’s merging features tedious and you want to use Git’s much more advanced capability.
If you use git svn rebase, you can’t really do any of those things. The good news is that if you avoid using rebase, git svn will let you do it all.
There’s only one catch: your fancy, nonlinear history won’t ever be in Subversion. Your Subversion-using coworkers will see the results of your hard work in the form of an occasional squashed merge commit (see Squash Merges), but they won’t be able to see exactly how you got there.
If that’s going to be a problem, you should probably skip the rest of this chapter. But if your coworkers don’t care—most developers don’t look at others’ histories anyway—or if you want to use it to prod your coworkers to try out Git, what’s described next is a much more powerful way to use git svn.
Recall from Chapter 10 that a rebase is disruptive because it generates entirely new commits that represent the same changes. The new commits have new commit IDs, and when you merge one branch with one of the new commits into another branch that had one of the old commits, Git has no way of knowing you’re applying the same change twice. The result is duplicate entries in git log and sometimes a merge conflict.
With plain Git, preventing such situations is easy: avoid git cherry-pick and git rebase and the problems won’t occur at all. Or use the commands carefully, and issues will occur only in controlled situations.
With git svn, however, there’s one more potential source of problems, and it’s not as easy to avoid. The problem is that the Git commit objects created by your git svn are not always the same as the ones produced by other people’s git svn, and you can’t do anything about it. For example:
If you have a different version of Git than someone else, your git svn might generate different commits than your coworkers. (The Git developers try very hard to avoid this, but it can happen.)
If you use the --authors-file
option to
remap author names or apply various other git
svn options that change its behavior, all the commit IDs
will be different.
If you use a Subversion URI that’s different from someone
else working in the Subversion repository (e.g., if you access an
anonymous Subversion repository but someone else uses an
authenticated method to access the same repository), your
git-svn-id
lines will be different; this
changes the commit message, which changes the SHA1 of the commit,
which changes the commit ID.
If you fetch only a subset of Subversion revisions by using
the -r
option to git svn clone
(as in the first example in this chapter), and if someone else
fetches a different subset, the history will be different and so
the commit IDs will be different.
If you use git merge and then git svn dcommit the results, the new commit will look different to you from the same commit that other people retrieve through git svn fetch, because only your copy of git svn knows the true history of that commit. (Remember that, on its way into Subversion, the history information is lost, so even Git users retrieving from Subversion can’t get that history back again.)
With all those caveats, it might sound like trying to coordinate between git svn users is almost impossible. But there’s one simple trick you can use to avoid all these problems: make sure there’s only one Git repository, the “gatekeeper,” that ever uses git svn fetch or git svn dcommit.
Using this trick has several advantages:
Since only one repository ever interfaces with Subversion, there will never be a problem with incompatible commit IDs, because every commit is created only once.
Your Git-using coworkers will never have to learn how to use git svn.
Because all Git users are just using plain Git, they can collaborate with each other using any Git workflow, without worrying about Subversion.
It’s faster to convert a new user from Subversion to Git because a git clone operation is much faster than downloading every single revision from Subversion, one at a time.
If your entire team eventually converts to Git, you can simply unplug the Subversion server one day and nobody will know the difference.
But there’s one main disadvantage:
You end up with a bottleneck between the Git world and the Subversion world. Everything must go through a single Git repository, which is probably administered by a small number of people.
At first, compared to a completely distributed Git setup, requiring a centrally managed git svn repository may seem like a step backward. But you already have a central Subversion repository, so this doesn’t make matters any worse.
Let’s look at setting up that central gatekeeper repository.
Earlier, when you set up a personal git svn repository, the procedure cloned just a few revisions of a single branch. That’s good enough for one person who wants to do some work offline, but if an entire team is to share the same repository, you can’t make assumptions about what parts are needed and what parts are not. You want all the branches, all the tags, and all the revisions of each branch.
Because this is such a common requirement, Git has an option to perform a complete clone. Let’s clone the Subversion source code again, but this time doing all the branches:
$git svn clone --stdlayout --prefix=svn/ -r33005:33142
http://svn.collab.net/repos/svn svn-all.git
The best way to produce a gatekeeper repository is to leave out the -r
option
entirely. But if you did that here, it would take hours or even days
to complete. As of this writing, the Subversion source code contains
tens of thousands of revisions, and git svn would
have to download each one individually over the Internet. If you’re
following along with this example, keep the -r
option. But if you’re setting up a Git repository for your own
Subversion project, leave it out.
Notice the new options:
--stdlayout
tells git svn that the
repository branches are set up in the standard Subversion way, with the
/trunk
, /branches
, and
/tags
subdirectories corresponding
(respectively) to mainline development, branches, and tags. If
your repository is laid out differently, you can try the
--trunk
, --branches
, and
--tags
options instead, or edit
.git/config to set the
refspec option by hand. Type git
help svn for more information.
--prefix=svn/
creates all the remote refs with the
prefix svn/
, allowing you to refer to
individual branches as svn/trunk
and
svn/1.5.x
. Without this option, your Subversion
remote refs wouldn’t have any prefix at all, making it easy to
confuse them with local branches.
git svn should churn for a while. When it’s all over, the results look like this:
$cd svn-all.git
$git branch -a -v | cut -c1-60
* master 0502656 Merge r32790, r32796, r32798 svn/1.0.x 19e69aa Merge the 1.0.x-issue-2751 br svn/1.1.x e20a6ce Per the proposal in http://sv svn/1.2.x 70a5c8a Per the proposal in http://sv svn/1.3.x 32f8c36 * STATUS: Leave a breadcrumb svn/1.4.x 23ecb32 Per the proposal in http://sv svn/1.5.x 0502656 Merge r32790, r32796, r32798 svn/1.5.x-issue2489 2bbe257 On the 1.5.x-issue2489 branch svn/explore-wc 798f467 On the explore-wg branch: svn/file-externals 4c6e642 On the file externals branch. svn/ignore-mergeinfo e3d51f1 On the ignore-mergeinfo branc svn/ignore-prop-mods 7790729 On the ignore-prop-mods branc svn/svnpatch-diff 918b5ba On the 'svnpatch-diff' branch svn/tree-conflicts 79f44eb On the tree-conflicts branch, svn/trunk ae47f26 Remove YADFC (yet another dep
The local master
branch has automatically
been created, but it isn’t what you might expect—it’s pointing at the
same commit as the svn/1.5.x
branch, not the
svn/trunk
branch. Why? The most recent commit in
the range specified with -r
belonged to the
svn/1.5.x
branch. (But don’t count on this
behavior; it’s likely to change in a future version of git
svn.) Instead, let’s fix it up to point at the trunk:
$git reset --hard svn/trunk
HEAD is now at ae47f26 Remove YADFC (yet another deprecated function call). $git branch -a -v | cut -c1-60
* master ae47f26 Remove YADFC (yet another dep svn/1.0.x 19e69aa Merge the 1.0.x-issue-2751 br svn/1.1.x e20a6ce Per the proposal in http://sv svn/1.2.x 70a5c8a Per the proposal in http://sv svn/1.3.x 32f8c36 * STATUS: Leave a breadcrumb svn/1.4.x 23ecb32 Per the proposal in http://sv svn/1.5.x 0502656 Merge r32790, r32796, r32798 svn/1.5.x-issue2489 2bbe257 On the 1.5.x-issue2489 branch svn/explore-wc 798f467 On the explore-wg branch: svn/file-externals 4c6e642 On the file externals branch. svn/ignore-mergeinfo e3d51f1 On the ignore-mergeinfo branc svn/ignore-prop-mods 7790729 On the ignore-prop-mods branc svn/svnpatch-diff 918b5ba On the 'svnpatch-diff' branch svn/tree-conflicts 79f44eb On the tree-conflicts branch, svn/trunk ae47f26 Remove YADFC (yet another dep
After importing your complete git svn gatekeeper repository from Subversion, you need to publish it. You do that in the same way you would set up any bare repository (see Chapter 11), but with one trick: the Subversion “branches” that git svn creates are actually remote refs, not branches. The usual technique doesn’t quite work:
$cd ..
$mkdir svn-bare.git
$cd svn-bare.git
$git init --bare
Initialized empty Git repository in /tmp/svn-bare/ $cd ..
$cd svn-all.git
$git push --all ../svn-bare.git
Counting objects: 2331, done. Compressing objects: 100% (1684/1684), done. Writing objects: 100% (2331/2331), 7.05 MiB | 7536 KiB/s, done. Total 2331 (delta 827), reused 1656 (delta 616) To ../svn-bare * [new branch] master -> master
You’re almost there. With git push, you
copied the master
branch but none of the
svn/
branches. To make things work properly, modify
the git push command by telling it explicitly to
copy those branches:
$ git push ../svn-bare.git 'refs/remotes/svn/*:refs/heads/svn/*'
Counting objects: 6423, done.
Compressing objects: 100% (1559/1559), done.
Writing objects: 100% (5377/5377), 8.01 MiB, done.
Total 5377 (delta 3856), reused 5167 (delta 3697)
To ../svn-bare
* [new branch] svn/1.0.x -> svn/1.0.x
* [new branch] svn/1.1.x -> svn/1.1.x
* [new branch] svn/1.2.x -> svn/1.2.x
* [new branch] svn/1.3.x -> svn/1.3.x
* [new branch] svn/1.4.x -> svn/1.4.x
* [new branch] svn/1.5.x -> svn/1.5.x
* [new branch] svn/1.5.x-issue2489 -> svn/1.5.x-issue2489
* [new branch] svn/explore-wc -> svn/explore-wc
* [new branch] svn/file-externals -> svn/file-externals
* [new branch] svn/ignore-mergeinfo -> svn/ignore-mergeinfo
* [new branch] svn/ignore-prop-mods -> svn/ignore-prop-mods
* [new branch] svn/svnpatch-diff -> svn/svnpatch-diff
* [new branch] svn/tree-conflicts -> svn/tree-conflicts
* [new branch] svn/trunk -> svn/trunk
This takes the svn/
refs, which are
considered remote refs, from the local repository and copies them to
the remote repository, where they are considered heads (i.e., local
branches).[35]
Once the enhanced git push is done, your
repository is ready: tell your coworkers to go ahead and clone your
svn-bare.git
repository. They can then push, pull,
branch, and merge among themselves without a problem.
Eventually, you and your team will want to push changes
from Git back into Subversion. As before, you’ll do this using
git svn dcommit, but you need not rebase first. Instead, you can first
git merge or git pull the changes into a branch in the svn/
hierarchy and then commit only the single new merged commit.
For instance, suppose that your changes are in a branch called
new-feature
and that you want to
dcommit it into svn/trunk
.
Here’s what to do:
$git checkout svn/trunk
Note: moving to "svn/trunk" which isn't a local branch If you want to create a new branch from this checkout, you may do so (now or later) by using -b with the checkout command again. Example: git checkout -b <new_branch_name> HEAD is now at ae47f26... Remove YADFC (yet another deprecated function call). $git merge --no-ff new-feature
Merge made by recursive. hello.txt | 1 + 1 files changed, 1 insertions(+), 0 deletions(-) create mode 100644 hello.txt $git svn dcommit
There are three surprising things here:
Rather than checking out your local branch,
new-feature
, and merging in svn/trunk
, you
must do it the other way around. Normally,
merging works fine in either direction, but git
svn won’t work if you do it the other way.
You merge using the --no-ff
option, which ensures there will always be
a merge commit, even though sometimes a merge commit might seem
unnecessary.
You do the whole operation on a disconnected
HEAD
, which sounds dangerous.
You absolutely must do all three surprising things, or the operation won’t work reliably.
To understand why to do the dcommit in such a strange way, consider carefully how dcommit works.
First, dcommit figures out the Subversion
branch to commit to by looking at the git-svn-id
of
commits in the history.
If you’re nervous about which branch dcommit will pick, you can use git svn dcommit -n to try a harmless dry run.
If your team has been doing fancy things (which is, after all,
the point of this section), there might be merges and cherry-picked
patches on your new-feature
branch, and some of
those merges might have git-svn-id
lines from
branches other than the one to which you want to commit.
To resolve the ambiguity, git svn looks at
only the left side of every merge, in the same way that git
log --first-parent does. That’s why merging from
svn/trunk
into new-feature
doesn’t
work: svn/trunk
would end up on the right, not
the left, and git svn wouldn’t see it. Worse, it
would think your branch was based on an older version of the
Subversion branch and so would try to automatically git svn
rebase it for you, making a terrible mess.
The same reasoning explains why --no-ff
is
necessary. If you check out the new-feature
branch and git
merge svn/trunk, checkout the svn/trunk
branch and git merge new-feature without the
--no-ff
option, Git will do a fast-forward rather
than a merge. This is efficient, but again it results in
svn/trunk
being on the right side, with the same
problem as before.
Finally, after it figures all this out, git svn
dcommit needs to create one new commit in Subversion
corresponding to your merge commit. When that’s done, it must add a
git-svn-id
line to the commit message, which
means the commit ID changes, so it’s not the
same commit anymore.
The new merge commit ends up in the real
svn/trunk
branch, and the merge commit you
created earlier on the detached HEAD
is now redundant. In fact, it’s worse than redundant.
Using it for anything else eventually results in conflicts. So, just
forget about that commit. If you haven’t put it on a branch in the
first place, it’s that much easier to forget.
[35] If you think this sounds convoluted, you’re right. Eventually, git svn may offer a way to simply create local branches instead of remote refs, so that git push --all will work as expected.
3.145.9.12