Pushing, Pulling, Branching, and Merging with git svn

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.

Keeping Your Commit IDs Straight

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.

Cloning All the Branches

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

Warning

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

Sharing Your Repository

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.

Merging Back into Subversion

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.

How dcommit handles merges

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.

Tip

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.9.12