As mentioned in the README, there are a couple of different backend methods one can use to drive a Git/Hg bridge, and then some additional choices one can make about packaging those backends for day-to-day use. (In git-speak, what "porcelain" to install on top of them.)
Before we sort out our frontend choices, though, we've got to settle which backend engines work best. Here are the options.
-
git fast-import
is a component in the standard Git distribution that imports text-serialized repositories. There is another componentgit fast-export
that serializes Git repositories to the needed format; but what we need is a way to serialize Mercurial repositories into that format. Someone has in fact made that; it's available as thehg-fast-export
scripts (hg-fast-export.py
,hg-fast-export.sh
, andhg2git.py
) that are in the fast-export project. (That project also has somehg-reset
scripts that I don't yet understand; and also some scripts for interacting with Subversion.)So these
hg-fast-export
scripts provide a way to serialize a Mercurial repository, and we can pipe the result intogit fast-import
. That's one way to go from hg->git. It's what thegit-hg
tool uses. -
How about the reverse direction, from git->hg? One way to do this is with the convert extension for Mercurial (see also here). This again is what the
git-hg
tool uses. Theconvert
extension already comes with the standard Mercurial distribution, though it's not enabled by default. No problem, though: whatever frontend porcelain we build can just explicitly enable it for the Mercurial repos we use.This extension is able to keep track of conversions that it has already made into a Mercurial repository, so that later conversions can be incremental. However, as we'll see below, there are severe limits to how well this works when we combine it with hg->git movement between the same repositories.
-
A different Mercurial extension is hg-git. This isn't part of the standard Mercurial distribution, but needs to be installed separately. As with the
convert
extension, our frontend porcelain can take care of enabling this extension in the repositories where we're going to use it, so the user doesn't need to make it enabled globally. She just needs to have it installed.The documentation for the
hg-git
extension describes three different modes of operation. The first involves cloning a Mercurial repository from an existing upstream Git repository. That doesn't fit our needs; it's instead what someone has to do who wants to use Mercurial to interact with Git-based projects. We're interested in the reverse. The second mode described involves starting with a Mercurial repository, spawning a Git repository off of it, and then interacting with that Git repository just as in the first method. This is a method we could use. We will call it our method 3. It would permit going in both directions: both pulling from Mercurial into Git and pushing back.The documentation for
hg-git
says that this method can't be used with local Git repositories because the Dulwich Python Git library they rely on "doesn't support local repositories yet". They say you have to speak to the Git repository over a network (though it could simply be a network connection to localhost). They wrote that on 6 April 2010. But on my machine, I'm not encountering any such limitations. It looks like in recent versions of Dulwich, using a local Git repository, specified by a plain old pathname, works fine. It's not easy to tell when this started working; the Dulwich changelog doesn't obviously refer to it. From their git log, I'm guessing they started to add it around 2 April 2010. -
I said that the documentation for
hg-git
describes three modes of operation. The last of these provides a different way we could use the extension. This involves, notpush
ing andpull
ing to a Git repository that may be (but needn't be) remote, but rathergexport
ing andgimport
ing to a local Git repository. The default behavior is togexport
andgimport
to a bare Git repository hidden in the.hg
folder, but in fact we can arrange for that repository to be located anywhere and it needn't be left bare. One of the existing frontends has this Git repository sharing its working directory with the local Mercurial repository.
There are details to work out about which parts of the Mercurial repository we're going to synch to Git: the tip of the default branch for sure, but what about other named branches? what about bookmarks from upstream? what about tags? And what parts of Git are we going to synch to Mercurial: which branches and how will they be identified in the Hg repository? and again, what about tags?
But as we muddle over those questions, let's start exploring what constraints these different backends impose. That may affect the decisions we make about what gets synched with what.
I created a test Mercurial repository that you can clone from https://code.google.com/p/yagh-test/. This is a small repository displaying a variety of features: it has several named branches, one of them "inactive" and another with multiple heads. There are also some bookmarks already in the upstream repository, and some committed tags. Here is how it looks on my machine, you may see different revision numbers. (I did myself, after cloning it a few times.)
+-- r17 "latest" <= tip
all this is /
branch3 ==> +- r13 <- r14 <-- r16 mark3
/ \
"inactive" branch2 => r12 +-- r15 "head1"
ends here /
r11 mark2
/
r0 <- r1 <- r2 <- r3 <- r4 <- r5 <- r6 <- r7 <- r8 <= default branch
tag1 tag2 \ mark1
r9
\
r10 <= branch1
The tags, bookmarks, and branches are as labeled. The "latest" and "head1" labels aren't any of those; they're just what the commit messages on r15 and r17 say.
Okay, let's see how well method 1 works.
~/repo $ hg clone https://code.google.com/p/yagh-test/ yagh-test1
requesting all changes
adding changesets
adding manifests
adding file changes
added 18 changesets with 18 changes to 2 files (+4 heads)
updating to branch default
2 files updated, 0 files merged, 0 files removed, 0 files unresolved
~/repo $ git init --bare test1.git && cd test1.git && rm hooks/*.sample
Initialized empty Git repository in /usr/home/jim/repo/test1.git/
Those sample hooks would just distract us later.
~/repo/test1.git $ /usr/local/libexec/yagh/hg-fast-export.sh -r ~/repo/yagh-test1
Error: repository has at least one unnamed head: hg r16
git-fast-import statistics:
---------------------------------------------------------------------
Alloc'd objects: 5000
...[output pruned]...
---------------------------------------------------------------------
~/repo/test1.git $ find .
.
./refs
./refs/heads
./refs/tags
./hg2git-heads
./hg2git-marks
./info
./info/exclude
./branches
./objects
./objects/info
./objects/pack
./description
./config
./HEAD
./hooks
Uh-oh, looks like nothing got imported. That's because the hg-fast-export
scripts didn't like the unnamed-and-untagged heads in our repository. (It identified r16 as one such, but r15 is as well. The "latest" head on branch3 is tagged, as "tip".) The only way to proceed here is to give these scripts the --force
flag. Then things work ok, but we'll be missing out on some sanity checks. If you know your upstream repository will almost never have multiple heads on a single branch, then you won't need to --force
things. Note that it's not enough that the heads be bookmarked: here the script complained about r16 even though that is bookmarked as "mark3".
$ cd ~/repo/yagh-test1 && hg log -r16
changeset: 16:f409556ae260
branch: branch3
bookmark: mark3
parent: 12:c740be107432
user: Jim Pryor <dubiousjim@gmail.com>
date: Fri Jun 29 19:46:19 2012 -0400
summary: to be marked3
Ok, let's supply the --force
and keep going. I'll supply two other flags to the hg-fast-export
scripts as well, which may be useful to know:
$ cd ~/repo/test1.git && /usr/local/libexec/yagh/hg-fast-export.sh --force -o origin2 -M master2 -r ~/repo/yagh-test1
The -M master2
flag lets you specify what branch on the Git-side the Mercurial "default" branch should be exported to. This defaults to "master", but depending on your Git habits, it might be cognitively more natural to put it someplace else you'll be less likely to try to directly modify. The -o origin2
flag permits you to specify a prefix for all the Git-side branches these scripts export to (including any branch specified with -M
). We'll see how that works below. Here's the output I got. Notice there are errors for the two unnamed-and-untagged heads on branch3, but the export continues anyway.
Error: repository has at least one unnamed head: hg r16
Error: repository has at least one unnamed head: hg r15
origin2/master2: Exporting full revision 1/18 with 1/0/0 added/changed/removed files
origin2/master2: Exporting simple delta revision 2/18 with 0/1/0 added/changed/removed files
origin2/master2: Exporting simple delta revision 3/18 with 1/0/0 added/changed/removed files
Skip .hgtags
...[output pruned]...
Exporting tag [tag1] at [hg r1] [git :2]
Exporting tag [tag2] at [hg r4] [git :5]
Issued 20 commands
git-fast-import statistics:
---------------------------------------------------------------------
Alloc'd objects: 5000
...[output pruned]...
---------------------------------------------------------------------
And here's what our Git repository currently looks like:
~/repo/test1.git $ find .
.
./hg2git-mapping
./HEAD
./branches
./hg2git-marks
./hg2git-heads
./description
./objects
./objects/info
./objects/pack
./objects/pack/pack-f6ddcc01c237af229c6d99ed746822e4976d3b67.idx
./objects/pack/pack-f6ddcc01c237af229c6d99ed746822e4976d3b67.pack
./info
./info/exclude
./config
./hg2git-state
./refs
./refs/tags
./refs/tags/tag1
./refs/tags/tag2
./refs/heads
./refs/heads/origin2
./refs/heads/origin2/branch2
./refs/heads/origin2/master2
./refs/heads/origin2/branch1
./refs/heads/origin2/branch3
./hooks
See how it created the branches as origin2/master2
and so on? If we left off the -o origin2
flag they'd just be master2
and so on. Now, even though these look like remote-tracking branches, they're not:
~/repo/test1.git $ git branch -r
~/repo/test1.git $ git branch
origin2/branch1
origin2/branch2
origin2/branch3
origin2/master2
But we can interact with them as if they were. Notice we have no master
branch, so let's create one. We'll set it to track origin2/master2
:
~/repo/test1.git $ git branch --track master origin2/master2
Branch master set up to track local branch origin2/master2.
I'll point out two nice things about how this all worked. One is that our tags were imported from Mercurial: you can see them in the above file listing, or you can ask Git directly:
~/repo/test1.git $ git tag
tag1
tag2
The other nice thing is that although we got error messages about the unnamed heads, they were still imported into the Git database. Here's a handy Git alias I have in my ~/.gitconfig
:
[alias]
lost-commits = !git fsck --unreachable | sed -ne 's/^unreachable commit //p' | xargs git log --no-walk
Using that, we can ask:
~/repo/test1.git $ git lost-commits --oneline
f39fb92 to be marked3
0a861da head1
And there are our two unnamed heads from the Mercurial repository. Git might GC them after a while, though I expect they'd then be reimported again the next time we import records from Mercurial.
Let's try adding another changeset on the Mercurial side and doing the hg->git import again.
$ cd ../yagh-test1 && hg checkout tip
2 files updated, 0 files merged, 0 files removed, 0 files unresolved
~/repo/yagh-test1 $ hg summary
parent: 17:aad9ea416809 tip
latest
branch: branch3
commit: (clean)
update: 2 new changesets, 3 branch heads (merge)
~/repo/yagh-test1 $ vim data && hg commit -m "post-latest"
~/repo/yagh-test1 $ hg log -l2
changeset: 18:218b4ab1e286
branch: branch3
tag: tip
user: Jim Pryor <dubiousjim@gmail.com>
date: Fri Jun 29 21:10:38 2012 -0400
summary: post-latest
changeset: 17:aad9ea416809
branch: branch3
parent: 12:c740be107432
user: Jim Pryor <dubiousjim@gmail.com>
date: Fri Jun 29 19:47:22 2012 -0400
summary: latest
$ cd ~/repo/test1.git
~/repo/test1.git $ /usr/local/libexec/yagh/hg-fast-export.sh --force -M master2 -o origin2
Using last hg repository "/usr/home/jim/repo/yagh-test1"
Error: repository has at least one unnamed head: hg r16
Error: repository has at least one unnamed head: hg r15
origin2/branch3: Exporting simple delta revision 19/19 with 0/1/0 added/changed/removed files
Exporting tag [tag1] at [hg r1] [git c9afde4116c377eef91f6bcad0236449e9a93c4e]
Exporting tag [tag2] at [hg r4] [git 5cd422ec2b24b25aa1a390466325f2495b1e8e89]
Issued 3 commands
git-fast-import statistics:
---------------------------------------------------------------------
Alloc'd objects: 5000
...[output pruned]...
---------------------------------------------------------------------
~/repo/test1.git $ git log --oneline -2 origin2/branch3
df0cd1a post-latest
bcd39c5 latest
Cool, so new commits on the Mercurial side get imported in the way we'd expect.
Besides the need to --force
if a Mercurial named branch---or the default branch---has multiple heads, the only other downside I see to this importing method is that it ignores bookmarks in the Mercurial repository. Depending on how the upstream repositories you want to interact with operate, that may or may not be a problem. The folks behind the hg-git
extension say that Mercurial bookmarks are conceptually closer to Git branches than Mercurial named branches are. (References to the latter, but not to either of the former, are hard-written into a commit and are difficult to expunge or modify. Also, commits can only belong to one Mercurial named branch at a time.) So they encourage Mercurial/Git integration based on Mercurial bookmarks rather than named branches.
However, if your upstream Mercurial repositories are structured in ways that work well with this import method, then it may suit your needs. I haven't done any performance comparisons versus the hg-git
methods, but many users do report satisfaction using the git-hg frontend tool, which as I said is built around this import method.
The fast-export project does note some limitations on the hg-fast-export
scripts:
hg-fast-export supports multiple branches but only named branches with
exaclty one head each...
As we saw, their scripts complain when a branch has multiple heads. However, --force
ing the export seemed to work ok. The notes go on:
Otherwise commits to the tip of these heads
within branch will get flattened into merge commits.
I didn't observe this behavior in my testing. Maybe the documentation is outdated? Or maybe I didn't understand where to look. The notes continue:
As each git-fast-import run creates a new pack file, it may be
required to repack the repository quite often for incremental imports
(especially when importing a small number of changesets per
incremental import).
So keep that in mind: if you use this backend method to pull often, you should also arrange for frequent git-repack
ings.
These things noted, though, this method might still work well for some users who want to pull from Mercurial into Git.
However if you also need to push from Git back to Mercurial, then things aren't so rosy. The natural way to do this is to combine the hg-fast-export
scripts with the Mercurial convert
extension. (That's what the git-hg
frontend tool does. If one were going to install the hg-git
extension, one could just use that to go in both directions.)
Let's make a change on the Git side and try to push it back to Mercurial. The test1.git
repository we created is a bare repository, so we can't modify it directly. Let's just clone it and push a change from the clone back to its origin.
$ cd ~/repo && git clone test1.git test2 && cd test2
Cloning into 'test2'...
done.
~/repo/test2 $ git status
# On branch master
nothing to commit (working directory clean)
~/repo/test2 $ vim data && git commit -m "change from git" data
[master 47eab84] change from git
1 files changed, 1 insertions(+), 0 deletions(-)
~/repo/test2 $ git log --oneline -2
47eab84 change from git
ae0b1c1 post marked1
~/repo/test2 $ git branch -a
* master
remotes/origin/HEAD -> origin/master
remotes/origin/master
remotes/origin/origin2/branch1
remotes/origin/origin2/branch2
remotes/origin/origin2/branch3
remotes/origin/origin2/master2
~/repo/test2 $ git push origin
Total 0 (delta 0), reused 0 (delta 0)
To /usr/home/jim/repo/test1.git
ae0b1c1..47eab84 master -> master
~/repo/test2 $ git push origin master:origin2/master2
Counting objects: 5, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 285 bytes, done.
Total 3 (delta 1), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
To /usr/home/jim/repo/test1.git
ae0b1c1..47eab84 master -> origin2/master2
~/repo/test2 $ cd ~/repo/test1.git/
~/repo/test1.git $ git log -2 --oneline master
47eab84 change from git
ae0b1c1 post marked1
~/repo/test1.git $ git log -2 --oneline origin2/master2
47eab84 change from git
ae0b1c1 post marked1
~/repo/test1.git $ git show-branch --more=1 master origin2/master2
* [master] change from git
! [origin2/master2] change from git
--
*+ [master] change from git
*+ [master^] post marked1
Ok, so we've made a change and pushed it both to master
and to our "hg-tracking" branch origin2/master2
. Ideally we'd have some frontend porcelain that made that more elegant, but this is what we'd expect the end result to be. Now we want to push that change from the "hg-tracking" branch back to our clone of the Mercurial repository. If that succeeds, then our frontend porcelain could arrange for the change to be hg push
ed back upstream.
The documentation for convert
describes a "branchmap" file that lets you explicitly specify which Git branches being imported should go to which Mercurial named branches. So we'll try that out:
$ cd ~/repo/yagh-test1
~/repo/yagh-test1 $ vim branchmap && cat branchmap
origin2/master2 default
origin2/branch1 branch1
origin2/branch2 branch2
origin2/branch3 branch3
~/repo/yagh-test1 $ hg convert --branchmap branchmap ~/repo/test1.git .
scanning source...
sorting...
converting...
17 initial commit
16 to be tagged1
15 Added tag tag1 for changeset d03e4e8a14b0
14 post tag1
13 to be marked2
12 post marked2
11 branch3 continues
10 fork here
9 latest
8 post-latest
7 to be tagged2
6 Added tag tag2 for changeset 54a73ff52ed4
5 post tag2
4 to be marked1
3 post marked1
2 change from git
1 side branch1
0 more on branch1
updating bookmarks
That looks promising. It imported a lot of revisions, but that's because we had never run it before. The next time we run it, it would start again from the place where it had previously finished converting. Let's see how things look:
$ $ hg log --style=compact -l 4
36[tip][origin2/branch1] 7d904ec96fcc 2012-06-29 19:42 -0400 dubiousjim
more on branch1
35:31 b172949a7241 2012-06-29 19:42 -0400 dubiousjim
side branch1
34[master,origin2/master2] 37b41efc0a9a 2012-06-29 21:36 -0400 dubiousjim
change from git
33 acc0091ce7f6 2012-06-29 19:41 -0400 dubiousjim
post marked1
Yeah, the "change from git" showed up alright, as r34 (umm...r34? ...yeah, hold that thought for a moment). There are the funny remarks about "origin2/branch1" and "master" and so on, and sure enough, we see that the convert
extension created bookmarks for all of these Git branches:
$ hg bookmarks
mark1 10:1d8979712420
mark2 5:e2833a193f93
mark3 16:f409556ae260
master 34:37b41efc0a9a
origin2/branch1 36:7d904ec96fcc
origin2/branch2 24:3c59d8452fea
origin2/branch3 28:8a3194ee63b3
origin2/master2 34:37b41efc0a9a
There are those high revision numbers again. What's going on with that? We started off with a repository with 19 changesets (the 0..17 we cloned plus the "post-latest" one we added). Then we added one on the Git side. So we should now have 20 changesets. Instead we have 37. It looks like all of the reachable old Git commits (all the ancestors of the named heads in Mercurial) have been duplicated in the Mercurial repository. This is unacceptable.
Maybe there's some way to clean this all up and get it to work. I don't know. But on the face of it, this method just looks terribly unsuited to play the role of an ongoing Git/Hg bridge. Of course, that's not its intended purpose: it's meant for importing Git commits that the destination Mercurial repository had never seen, for example, if we were converting the Git repository into an empty Mercurial repository. But it initially looked like it might do what we need for an ongoing back-and-forth bridge. That's in fact how the git-hg
tool uses it. But it doesn't work! We can't be polluting our Mercurial repository with all these duplicate commits everytime we push. Not to mention that our Git-created commits are descended in the history from the new copies of the old commits, rather than from the originals, as we intended.
The convert
extension has a configuration setting convert.hg.usebranchnames
that defaults to true
. So far as I can tell, turning that off has no effect if you continue to specify a --branchmap
file. I tried repeating everything we did before without the -M
and -o
flags to hg-fast-export
, and without the branchmap file, but still end up getting the same duplicated commits when we convert
the Git repository back into Mercurial.
The convert
extension comes with a flag --rev
that permits you to specify what revisions to import up until, but we want the opposite: to be able to specify what revisions not to import before. The extension's documentation discusses that, but says you need to do this:
The second way would be to use the splice map and say "The first commit I'm
interested in should have 0000000000000000000000000000000000000000 as its
parent." After the repository conversion, you can then clean the history and
remove unwanted branches. [This will] still require downloading all
changes, though.
Perhaps we could do something like that (only using an existing Mercurial changeset as the parent we splice onto, rather than 0000000000000000000000000000000000000000). But wow, that's a lot more work than we were expecting. And no doubt our initial efforts to get that working will be brittle and break in corner cases that didn't occur to us. Perhaps if there were no alternatives, this is what we'd have to do. But given how difficult this all looks, let's turn instead to the hg-git
methods.
Lesson learned: the backend method the git-hg
tool uses to push from Git to Mercurial is not usable. That tool does declare this functionality "experimental", but in its current implementation it's just broken. It will double your Mercurial repository every time you pull-then-push, and your new Git commits will not be descendents of any commit already in the database. As I hope the above discussion makes clear, this is not something one can fix with tweaks to the git-hg
frontend; it's a limitation in the backend implementation that git-hg
is relying on. So don't use git-hg
to push to Mercurial; if you're going to use it, use it for pulling only.
We want next to try out the other methods of pulling and pushing from Mercurial, using the hg-git
Mercurial extension.
A first difficulty I faced is that the documentation for this extension is scattered across several sites; and each of the sites adds useful information. So I'll begin by pulling together the pieces we might use.
One version of the documentation says:
The Hg-Git plugin can convert commits/changesets losslessly from one system
to another, so you can push via a Mercurial repository and another Mercurial
client can pull it. In theory, the changeset IDs should not change, although
this may not hold true for complex histories.
That sounds good. We don't want duplicated commits with different changeset IDs, like we saw when using the convert
extension.
Some of the documentation discusses cloning an existing Git repository into Mercurial. That's not what we'll be doing; but some of what they say may be useful anyway (I've fixed some clear mistakes):
You can clone a Git repository from Hg by running hg clone url. For example, if you were to run
$ hg clone git://github.com/schacon/hg-git.git
hg-git would clone the repository down into the directory 'hg-git', then convert it to an Hg repository for you.
If you want to clone a github repository for later pushing (or any other
repository you access via ssh), you need to convert the ssh url to a format
with an explicit protocol prefix. For example, the ssh url with push access
git@github.com:schacon/hg-git.git
would read
git+ssh://git@github.com/schacon/hg-git.git
(Mind the switch from colon to slash after the host!)
Your clone command would thus look like this:
$ hg clone git+ssh://git@github.com/schacon/hg-git.git
One version of the documentation says that when we clone a Git repository into Mercurial:
This will also create a bookmark for each git branch, and add local tags
default/<branch name> for each branch. This is similar to origin/<branch name>
in git.
When you pull from a git repository in the [paths] section of hgrc, it will
"fast-forward" the bookmarks if the branches haven't diverged. It will also
create local tags as above.
and that:
When pushing to git, the following is done to determine what's pushed :
* if there are no bookmarks and the remote repository is empty, the tip is pushed as the master branch.
* for each branch in the remote repository, if there is a bookmark or a
tag with the same name that points to a descendent of the head, then
push it.
* if there are bookmarks with no remote branch, a new branch is created.
The bookmarks extension is not necessary, one can work using solely local tags, but it's more convenient to use it.
The hg-git
documentation refers several times to "the bookmarks extension" and discuss how to enable it. But bookmarks have been merged into the core of Mercurial since v1.8, released 1 March 2011.
Other parts of the documentation discuss spawning a new Git repository off of an existing Mercurial one. This sounds useful for our purposes:
If you are starting from an existing Hg repository, you have to setup a Git
repository somewhere that you have push access to, add it as default path or
default-push path in your .hg/hgrc and then run hg push from within your
project. For example:
$ cd hg-git # (an Hg repository)
$ # edit .hg/hgrc and add the target git url in the paths section
$ hg push
This will convert all your Hg data into Git objects and push them up to the Git server.
A different version of the documentation presents the process like this:
$ cd hg-git # (a Mercurial repository)
$ hg bookmark -r default master # make a bookmark of master for default, so a ref gets created
$ hg push git+ssh://git@github.com/schacon/hg-git.git
$ hg push
This will convert all our Mercurial data into Git objects and push them up
to the Git server. You can also put that path in the [paths] section of
.hg/hgrc and then push to it by name.
The version of the documentation we were following first continues:
Now that you have an Hg repository that can push/pull to/from a Git repository, you can fetch updates with hg pull.
$ hg pull
That will pull down any commits that have been pushed to the server in the
meantime and give you a new head that you can merge in.
Hg-Git can also be used to convert a Mercurial repository to Git. As
Dulwich doesn't support local repositories yet...
We discussed this comment earlier; it seems to be out-of-date. The docs continue:
...the easiest way is to setup up a local SSH server. Then use the
following commands to convert the repository (it assumes your running this in
$HOME).
$ mkdir git-repo; cd git-repo; git init; cd ..
$ cd hg-repo
$ hg bookmarks hg
$ hg push git+ssh://localhost:git-repo
The hg bookmark is necessary to prevent problems as otherwise hg-git pushes
to the currently checked out branch confusing Git. This will create a branch
named hg in the Git repository. To get the changes in master use the following
command (only necessary in the first run, later just use git merge or rebase).
$ cd git-repo
$ git checkout -b master hg
To import new changesets into the Git repository just rerun the hg push
command and then use git merge or git rebase in your Git repository.
Finally, other parts of the documentation add these details:
hg-git keeps a git repository clone for reading and updating. By default,
the git clone is the subdirectory git in your local Mercurial repository. If
you would like this git clone to be at the same level of your Mercurial
repository instead (named .git), add the following to your hgrc:
[git]
intree = True
And:
hg-git does not convert between Mercurial named branches and git branches
as the two are conceptually different; instead, it uses Mercurial bookmarks to
represent the concept of a git branch. Therefore, when translating an hg repo
over to git, you typically need to create bookmarks to mirror all the named
branches that you'd like to see transferred over to git. The major caveat with
this is that you can't use the same name for your bookmark as that of the named
branch, and furthermore there's no feasible way to rename a branch in
Mercurial. For the use case where one would like to transfer an hg repo over to
git, and maintain the same named branches as are present on the hg side, the
branch_bookmark_suffix might be all that's needed. This presents a string
"suffix" that will be recognized on each bookmark name, and stripped off as the
bookmark is translated to a git branch:
[git]
branch_bookmark_suffix=_bookmark
Above, if an hg repo had a named branch called release_6_maintenance, you
could then link it to a bookmark called release_6_maintenance_bookmark. hg-git
will then strip off the _bookmark suffix from this bookmark name, and create a
git branch called release_6_maintenance. When pulling back from git to hg, the
_bookmark suffix is then applied back, if and only if an hg named branch of
that name exists. E.g., when changes to the release_6_maintenance branch are
checked into git, these will be placed into the release_6_maintenance_bookmark
bookmark on hg. But if a new branch called release_7_maintenance were pulled
over to hg, and there was not a release_7_maintenance named branch already, the
bookmark will be named release_7_maintenance with no usage of the suffix.
The branch_bookmark_suffix option is ... intended for migrating legacy hg
named branches. Going forward, an hg repo that is to be linked with a git repo
should only use bookmarks for named branching.
There is one more relevant part of the documentation, discussing gimport
and gexport
; but we'll reserve that for later.
Other parts of the documentation discuss installation; we discuss that in the installation instructions in the README.
Here are some tidbits I extract out of the preceding:
- pulling from Git will add local Mercurial tags
default/branchname
for each Git branch - cloning from Git will create a bookmark for each Git branch
- pulling from Git will "fast-forward" those bookmarks if the Hg and Git branches haven't diverged
We'll have to check and see whether pulling also creates new bookmarks for Git branches that have no Mercurial correlate. The documentation seems to imply it won't.
For the time being, we'll ignore worries about Mercurial bookmarks and named branches possibly having the same names.
With regard to pushing:
- pushing to a Git repo with branches will push Mercurial bookmarks and tags to Git branches with the same name, so long as these are "fast-forward" pushes
Is it only local Mercurial tags that are so handled? That's what the documentation seems to imply. Are global Mercurial tags exported to Git at all? (We'll see below: yes, they are.)
- new Git branches will be created for each Mercurial bookmark that doesn't yet have a matching Git branch
Finally:
- when there are no Mercurial bookmarks, the Mercurial
tip
is pushed into Gitmaster
The documentation implies this only happens when the Git repository is empty. But what if we then push a second time: is tip
no longer pushed? And when there are Mercurial bookmarks, is tip
just ignored? We'll have to check and see.
Also, one part of the documentation seems to contradict the last item: it says that when there are no Mercurial bookmarks, hg-git
will push "to the currently checked out branch" in Git, which need not be master
.
Let's try this all out. We'll start over with a new clone of the upstream repository:
$ cd ~/repo && hg clone https://code.google.com/p/yagh-test/ yagh-test3
requesting all changes
adding changesets
adding manifests
adding file changes
added 18 changesets with 18 changes to 2 files (+4 heads)
updating to branch default
2 files updated, 0 files merged, 0 files removed, 0 files unresolved
~/repo $ git init test3 && vim yagh-test3/.hg/hgrc && cat yagh-test3/.hg/hgrc
Initialized empty Git repository in /usr/home/jim/repo/test3/.git/
[paths]
default = /home/jim/repo/test3
As before, the *.sample
hooks in the git repository are just going to distract us later, so let's delete them:
~/repo $ rm test3/.git/hooks/*.sample
We're going to want some additional copies of this as we proceed, so let's make them now:
~/repo $ hg clone yagh-test3 yagh-test4
updating to branch default
2 files updated, 0 files merged, 0 files removed, 0 files unresolved
~/repo $ cp -a test{3,4} && vim yagh-test4/.hg/hgrc && cat yagh-test4/.hg/hgrc
[paths]
default = /home/jim/repo/test4
~/repo $ cd yagh-test4 && hg bookmarks
mark1 7:1d8979712420
mark2 11:e2833a193f93
mark3 16:f409556ae260
Let's delete bookmark mark3
and replace it with a local tag:
~/repo/yagh-test4 $ hg bookmark --delete mark3 && hg tag --local -r16 local3
~/repo/yagh-test4 $ hg tags -v
tip 17:aad9ea416809
local3 16:f409556ae260 local
tag2 4:54a73ff52ed4
tag1 1:d03e4e8a14b0
Let's make a filesystem copy of this Mercurial repository, instead of a clone, to carry over the local tag:
$ cd ~/repo && cp -a yagh-test{4,5}
~/repo $ cp -a test{3,5} && vim yagh-test5/.hg/hgrc && cat yagh-test5/.hg/hgrc
[paths]
default = /home/jim/repo/test5
~/repo $ cd yagh-test5 && hg bookmarks
mark1 7:1d8979712420
mark2 11:e2833a193f93
In this version we won't have any bookmarks:
~/repo/yagh-test5 $ hg bookmark --delete mark1
~/repo/yagh-test5 $ hg bookmark --delete mark2
But we still have local3
as a local tag:
~/repo/yagh-test5 $ hg tags -v
tip 17:aad9ea416809
local3 16:f409556ae260 local
tag2 4:54a73ff52ed4
tag1 1:d03e4e8a14b0
We'll make another copy for later:
$ cd ~/repo && cp -a yagh-test{5,6}
OK, now let's try pushing these to our fresh Git repositories and see what happens. We'll start by pushing yagh-test5 into test5:
$ cd ~/repo/yagh-test5 && hg push
pushing to /usr/home/jim/repo/test5
exporting hg objects to git
creating and sending data
Unpacking objects: 100% (30/30), done.
abort: git remote error: refs/heads/master failed to update
Whoops. What happened?
$ cd ~/repo/test5 && find .
.
./.git
./.git/description
./.git/info
./.git/info/exclude
./.git/config
./.git/refs
./.git/refs/tags
./.git/refs/tags/tag1
./.git/refs/tags/tag2
./.git/refs/heads
./.git/objects
./.git/objects/info
./.git/objects/6f
./.git/objects/6f/94cdc38665d4de4de49104a18a8ac921689b75
...[output pruned]...
./.git/HEAD
./.git/hooks
./.git/branches
It looks like a bunch of Mercurial commits were copied over, but when hg-git
tried to "fast-forward" the master
branch, it failed. That's because this was a fresh Git repository and the master
ref hadn't yet been created. Let's start over with that reference in place:
$ cd ~/repo && rm -rf test5 && cp -a test{3,5} && cd test5
~repo/test5 $ git commit --allow-empty -m "initial git commit"
[master (root-commit) 260e7dc] initial git commit
~repo/test5 $ ls .git/refs/heads/
master
$ cd ~/repo/yagh-test5 && hg push
pushing to /usr/home/jim/repo/test5
creating and sending data
abort: refs/heads/master changed on the server, please pull and merge before pushing
Uh-oh. It looks like hg-git
is confused because refs/heads/master in our new destination repository isn't where it expected it to be. Let's start over with our fresh copy of yagh-test5
as well.
$ cd ~/repo/yagh-test6 && hg push
pushing to /usr/home/jim/repo/test5
exporting hg objects to git
creating and sending data
Unpacking objects: 100% (15/15), done.
default::refs/tags/tag2 => GIT:2b9279f9
default::refs/tags/tag1 => GIT:c9afde41
default::refs/heads/master => GIT:260e7dc
Ok, that seemed to work. So it looks like, if we want to push from a Mercurial repository without any bookmarks, we should make sure that the master
ref has been created in the destination Git repository, before we do any pushing. Else the push will fail and hg-git
will get confused about the state of the Git repository.
Let's see how it worked out:
$ cd ~/repo/test5 && git show-branch -a
[master] initial git commit
Uh-oh, that doesn't look good. Where's all the stuff we imported in?
~/repo/test5 $ git log --oneline
260e7dc initial git commit
~/repo/test5 $ git branch -a
* master
And yet the tags are there:
~/repo/test5 $ git tag -n
tag1 to be tagged1
tag2 to be tagged2
~/repo/test5 $ git log --oneline tag1
c9afde4 to be tagged1
0293f63 initial commit
And in fact all of our Mercurial commits have been imported:
~/repo/test5 $ git log --oneline --date-order $(git rev-list --all)
260e7dc initial git commit
2b9279f to be tagged2
d94575d post tag1
129b7eb Added tag tag1 for changeset d03e4e8a14b0
c9afde4 to be tagged1
0293f63 initial commit
We just can't see them because the master
ref we created doesn't have any common ancestors with them.
Well, we could fix this by forcing the master ref to point to the latest of the Mercurial commits:
~/repo/test5 $ git reset 2b9279f
HEAD is now at 2b9279f to be tagged2
Then we can leave the "initial git commit" to be GC'd. In hindsight, maybe we could have just cleaned up after the failed hg push
from yagh-test5 in the same way. And in fact that does bring us to roughly this same point; except in that case, we have a master
bookmark created on the Mercurial side pointing to the Mercurial tip
. And that could mess up later pushes, since the tip
needn't be on the default branch, which is the only branch hg-git
is pushing over.
Let's go back to the Mercurial repository and see how things stand:
$ cd ~/repo/yagh-test6 && hg push
pushing to /usr/home/jim/repo/test5
creating and sending data
default::refs/tags/tag2 => GIT:2b9279f9
default::refs/tags/tag1 => GIT:c9afde41
default::refs/heads/master => GIT:260e7dc
Pushing a second time works fine. However, how much of our Mercurial tree was pushed?
$ cd ~/repo/test5 && git log --oneline --date-order
2b9279f to be tagged2
d94575d post tag1
129b7eb Added tag tag1 for changeset d03e4e8a14b0
c9afde4 to be tagged1
0293f63 initial commit
$ cd ~/repo/yagh-test6 && hg log -r 'sort(branch(default),-date)' --template 'r{rev} {node|short} {tags}: {desc}\n'
r8 9f5b5d581bed : post marked1
r7 1d8979712420 : to be marked1
r6 51ace10a7ac3 : post tag2
r5 0d91ce7d9df4 : Added tag tag2 for changeset 54a73ff52ed4
r4 54a73ff52ed4 tag2: to be tagged2
r3 664d889398ea : post tag1
r2 867f59552a9a : Added tag tag1 for changeset d03e4e8a14b0
r1 d03e4e8a14b0 tag1: to be tagged1
r0 faa28ab8d90e : initial commit
It looks like everything past tag2
wasn't pushed. However, once we we start adding revisions to default
branch, then its head will be tagged (with tip
), and perhaps we'd then get the rest of it pushed over to Git. Even if that works, though, it's looking like more and more trouble to use hg-git
without Mercurial bookmarks.
Let's create a new commit on the Mercurial side and push it. We have to be sure we do this on the default branch, since as I said, that's the only branch that hg-git
is pushing (in this mode of operation).
~/repo/yagh-test6 $ hg checkout -r8
0 files updated, 0 files merged, 0 files removed, 0 files unresolved
~/repo/yagh-test6 $ vim data && hg commit -m "latest on default"
~/repo/yagh-test6 $ hg parents
changeset: 18:e1c4d4ffeba8
tag: tip
parent: 8:9f5b5d581bed
user: Jim Pryor <dubiousjim@gmail.com>
date: Sat Jun 30 12:23:20 2012 -0400
summary: latest on default
~/repo/yagh-test6 $ hg push
pushing to /usr/home/jim/repo/test5
exporting hg objects to git
creating and sending data
default::refs/tags/tag2 => GIT:2b9279f9
default::refs/tags/tag1 => GIT:c9afde41
default::refs/heads/master => GIT:2b9279f9
$ cd ~/repo/test5 && git log --oneline --date-order $(git rev-list --all)
2b9279f to be tagged2
d94575d post tag1
129b7eb Added tag tag1 for changeset d03e4e8a14b0
c9afde4 to be tagged1
0293f63 initial commit
That doesn't look good.
$ cd ~/repo/yagh-test6 && hg push -rtip
pushing to /usr/home/jim/repo/test5
creating and sending data
abort: revision e1c4d4ffeba8 cannot be pushed since it doesn't have a ref
~/repo/yagh-test6 $ hg tags -v
tip 18:e1c4d4ffeba8
local3 16:f409556ae260 local
tag2 4:54a73ff52ed4
default/master 4:54a73ff52ed4
tag1 1:d03e4e8a14b0
~/repo/yagh-test6 $ hg tag --local -rtip default/master
abort: tag 'default/master' already exists (use -f to force)
~/repo/yagh-test6 $ hg tag -f --local -rtip default/master
~/repo/yagh-test6 $ hg tags -v
tip 18:e1c4d4ffeba8
local3 16:f409556ae260 local
tag2 4:54a73ff52ed4
default/master 4:54a73ff52ed4 local
tag1 1:d03e4e8a14b0
Hey, it didn't update. It's still at r4. That's because the default/master
tag is managed by the hg-git
extension, not by the ordinary tags mechanism. Well, let's clean up our .hg/localtags
file and try something else:
~/repo/yagh-test6 $ vim .hg/localtags && cat .hg/localtags
f409556ae260823778413762e93f7aa6e6d16ad5 local3
~/repo/yagh-test6 $ hg tag --local -rtip master && hg tag --local -rtip newbranch && hg tags -v
tip 18:e1c4d4ffeba8
master 18:e1c4d4ffeba8 local
newbranch 18:e1c4d4ffeba8 local
local3 16:f409556ae260 local
tag2 4:54a73ff52ed4
default/master 4:54a73ff52ed4
tag1 1:d03e4e8a14b0
Now let's see if this works:
~/repo/yagh-test6 $ $ hg push
pushing to /usr/home/jim/repo/test5
creating and sending data
default::refs/tags/tag2 => GIT:2b9279f9
default::refs/tags/tag1 => GIT:c9afde41
default::refs/heads/master => GIT:2b9279f9
$ cd ~/repo/test5 && git rev-list --all | wc -l
5
Still no joy. Lesson learned: trying to use hg-git
without bookmarks is a pain in the rear.
OK, let's go back to yagh-test4, then. To remind ourselves, this has both some bookmarks and some local and global tags.
$ cd ~/repo/yagh-test4 && hg bookmarks
mark1 7:1d8979712420
mark2 11:e2833a193f93
~/repo/yagh-test4 $ hg tags -v
tip 17:aad9ea416809
local3 16:f409556ae260 local
tag2 4:54a73ff52ed4
tag1 1:d03e4e8a14b0
Let's see if pushing works:
~/repo/yagh-test4 $ hg push
pushing to /usr/home/jim/repo/test4
exporting hg objects to git
creating and sending data
Unpacking objects: 100% (27/27), done.
default::refs/heads/mark2 => GIT:c071dc28
default::refs/tags/tag2 => GIT:2b9279f9
default::refs/tags/tag1 => GIT:c9afde41
default::refs/heads/mark1 => GIT:9a989e35
$ cd ~/repo/test4 && git log --oneline --date-order $(git rev-list --all)
c071dc2 to be marked2
9a989e3 to be marked1
9e12910 post tag2
9ee1d72 Added tag tag2 for changeset 54a73ff52ed4
2b9279f to be tagged2
d94575d post tag1
129b7eb Added tag tag1 for changeset d03e4e8a14b0
c9afde4 to be tagged1
0293f63 initial commit
Everything got transferred over except revisions 8-10, and revisions 12-17 in our Mercurial repository:
+-- r17 "latest" <= tip
/
branch3 ==> +- r13 <- r14 <-- r16 local3
/ \
branch2 => r12 +-- r15 "head1"
/
. mark2
/
. <- . <- . <- . <- . <- . <- . <- . <- r8 <= default branch
tag1 tag2 \ mark1
r9
\
r10 <= branch1
And those are just the revisions that are ahead of any bookmark. If you'll recall, the hg-git
documentation said that it would also transfer over local tags, but we observe here that our tag local3
, and the chain leading up to it, were not transferred.
Hence it looks like you've really got to rely on bookmarks to control what gets pushed.
In general, we shouldn't assume that the upstream Mercurial repositories will cooperate though. They may or may not use bookmarks themselves. If they do, we'll want to download them and track them in our Git clones. The upstream repositories may or may not use named branches. If they do, we'll want to download those and track them too. So we'll make use of that git.branch_bookmark_suffix
setting mentioned in the hg-git
documentation. We'll have our frontend porcelain make sure that every Mercurial named branch has a correlated bookmark that tracks its tipmost head, so that these all get pushed to Git. (We won't do anything about nodes on the branch that aren't ancestors of the branch's tipmost head. But in principle, one could could check for multiple heads and maintain a set of bookmarks that track them, too.)
OK, let's apply this to our original clone of the upstream Mercurial repository:
$ cd ~/repo/yagh-test3 && hg bookmarks
mark1 7:1d8979712420
mark2 11:e2833a193f93
mark3 16:f409556ae260
~/repo/yagh-test3 $ hg tags -v
tip 17:aad9ea416809
tag2 4:54a73ff52ed4
tag1 1:d03e4e8a14b0
~/repo/yagh-test3 $ hg branches --active
branch3 17:aad9ea416809
branch1 10:d80cc7a33529
default 8:9f5b5d581bed
We'll create bookmarks to track branch1 and branch3:
~/repo/yagh-test3 $ hg bookmark -rbranch1 branch1_bookmark
~/repo/yagh-test3 $ hg bookmark -rbranch3 branch3_bookmark
~/repo/yagh-test3 $ vim .hg/hgrc && cat .hg/hgrc
[paths]
default = /home/jim/repo/test3
[git]
branch_bookmark_suffix = _bookmark
Now let's see if pushing works:
~/repo/yagh-test3 $ hg push
pushing to /home/jim/repo/test3
exporting hg objects to git
creating and sending data
Unpacking objects: 100% (48/48), done.
default::refs/heads/mark3 => GIT:bb0b2c00
default::refs/heads/mark2 => GIT:c071dc28
default::refs/tags/tag2 => GIT:2b9279f9
default::refs/tags/tag1 => GIT:c9afde41
default::refs/heads/mark1 => GIT:9a989e35
default::refs/heads/branch1 => GIT:b4e42711
default::refs/heads/branch3 => GIT:911326b6
$ cd ~/repo/test3 && git log --oneline --date-order $(git rev-list --all)
911326b latest
bb0b2c0 to be marked3
acf74ed fork here
57a866b branch3 continues
2c67b58 post marked2
c071dc2 to be marked2
b4e4271 more on branch1
fcd19dd side branch1
9a989e3 to be marked1
9e12910 post tag2
9ee1d72 Added tag tag2 for changeset 54a73ff52ed4
2b9279f to be tagged2
d94575d post tag1
129b7eb Added tag tag1 for changeset d03e4e8a14b0
c9afde4 to be tagged1
0293f63 initial commit
This pushed everything except revisions 8 and 15.
+-- . "latest" <= tip, also branch3_bookmark
/
branch3 ==> +- . <- . <-- . mark3
/ \
branch2 => . +-- r15 "head1"
/
. mark2
/
. <- . <- . <- . <- . <- . <- . <- . <- r8 <= default branch
tag1 tag2 \ mark1
.
\
. <= branch1, also branch1_bookmark
And that makes sense, because r8 and r15 are exactly the revisions beyond any of our bookmarks. (We haven't marked the tip of the default branch with a bookmark, I suppose we should do that, too.)
What branches exist on the git side?
~/repo/test3 $ git branch -r
~/repo/test3 $ git branch
branch1
branch3
mark1
mark2
mark3
Notice there's no master
branch. Let's make one.
~/repo/test3 $ git branch master mark1
~/repo/test3 $ git branch
branch1
branch3
mark1
mark2
mark3
* master
Let's try adding some more commits in Mercurial and seeing if they get pushed over properly:
$ cd ~/repo/yagh-test3 && hg checkout -C -rbranch1_bookmark
0 files updated, 0 files merged, 0 files removed, 0 files unresolved
~/repo/yagh-test3 $ vim data && hg commit -m "latest on branch1"
~/repo/yagh-test3 $ hg parents
changeset: 18:f4ae4285230c
branch: branch1
bookmark: branch1_bookmark
tag: tip
parent: 10:d80cc7a33529
user: Jim Pryor <dubiousjim@gmail.com>
date: Sat Jun 30 14:57:51 2012 -0400
summary: latest on branch1
Since we checked out the branch1_bookmark
directly, it advanced when we committed to the new head on branch1. However we can't rely on that always happening: if we had checked out -rbranch1
or -r10
, for instance, the branch1_bookmark
wouldn't have been updated---even though these were all the same node. So in general, our frontend porcelain will have to make sure that the branch-tracking bookmarks are pointing to the branch heads whenever we push.
~/repo/yagh-test3 $ hg checkout -r8
~/repo/yagh-test3 $ vim data && hg commit -m "latest on default"
~/repo/yagh-test3 $ hg parents
changeset: 19:75101cc9c3ef
tag: tip
parent: 8:9f5b5d581bed
user: Jim Pryor <dubiousjim@gmail.com>
date: Sat Jun 30 14:58:35 2012 -0400
summary: latest on default
~/repo/yagh-test3 $ hg push
pushing to /home/jim/repo/test3
exporting hg objects to git
creating and sending data
Unpacking objects: 100% (4/4), done.
default::refs/heads/branch1_bookmark => GIT:71a50a04
default::refs/heads/mark3 => GIT:bb0b2c00
default::refs/heads/mark2 => GIT:c071dc28
default::refs/tags/tag2 => GIT:2b9279f9
default::refs/tags/tag1 => GIT:c9afde41
default::refs/heads/mark1 => GIT:9a989e35
default::refs/heads/branch3_bookmark => GIT:911326b6
default::refs/heads/master => GIT:9a989e35
$ cd ~/repo/test3 && git log -2 --oneline --date-order $(git rev-list --all)
71a50a0 latest on branch1
911326b latest
~/repo/test3 $ git rev-list --all | wc -l
17
Our r18 got pushed to Git, because it was at or behind a bookmark, but r19 didn't, even though it was the tip
of the Mercurial repository and we had created a master
branch in the Git repository.
Notice that hg-git
creates some special Mercurial tags for us to track the state of the Git repository:
$ cd ~/repo/yagh-test3 && hg tags -v
tip 19:75101cc9c3ef
default/branch1_bookmark 18:f4ae4285230c
default/branch3_bookmark 17:aad9ea416809
default/mark3 16:f409556ae260
default/mark2 11:e2833a193f93
default/master 7:1d8979712420
default/mark1 7:1d8979712420
tag2 4:54a73ff52ed4
tag1 1:d03e4e8a14b0
These default/*
tags aren't stored in either .hgtags
or in .hg/localtags
, but in a special hg-git
metadata file (specifically, .hg/git-remote-refs
).
This is all working pretty well. Let's try adding a commit from the Git side, and pulling it back into Mercurial.
$ cd ~/test3 && git checkout master && git reset --hard
Already on 'master'
HEAD is now at 9a989e3 to be marked1
~/test3 $ vim data && git commit -m "added in git" data
[master 9284712] added in git
1 files changed, 1 insertions(+), 0 deletions(-)
~/test3 $ git show-branch master mark1
* [master] added in git
! [mark1] to be marked1
--
* [master] added in git
*+ [mark1] to be marked1
~/test3 $ git checkout mark1
Switched to branch 'mark1'
~/test3 $ git merge master
Updating 9a989e3..9284712
Fast-forward
data | 1 +
1 files changed, 1 insertions(+), 0 deletions(-)
~/test3 $ git checkout -b newbranch master
Switched to a new branch 'newbranch'
~/test3 $ vim data && git commit -m "added to newbranch" data
[newbranch d56a467] added to newbranch
1 files changed, 1 insertions(+), 0 deletions(-)
~/test3 $ git checkout master
Switched to branch 'master'
~/test3 $ vim data && git commit -m "added more to master" data
[master bf55acc] added more to master
1 files changed, 1 insertions(+), 0 deletions(-)
OK, so now we've advanced the mark1
and master
branches, and added a new branch besides. Let's pull this into Mercurial and see what happens:
$ cd ../yagh-test3 && hg pull
pulling from /home/jim/repo/test3
importing git objects into hg
(run 'hg heads' to see heads, 'hg merge' to merge)
~/repo/yagh-test3 $ hg log -l4 --template 'r{rev} {node|short} {tags}: {desc}\n'
r22 5acd3b5efe61 default/master tip: added more to master
r21 351851ece6e1 default/newbranch: added to newbranch
r20 6bdb2775b948 default/mark1: added in git
r19 75101cc9c3ef : latest on default
All three of the commits from Git got pulled in. Moreover our bookmarks were updated, and a new bookmark was created for the new Git branch:
~/repo/yagh-test3 $ hg bookmarks
master 22:5acd3b5efe61
newbranch 21:351851ece6e1
mark1 20:6bdb2775b948
...[output pruned]...
The hg-git
-managed tags were updated as well:
~/repo/yagh-test3 $ hg tags
tip 22:5acd3b5efe61
default/master 22:5acd3b5efe61
default/newbranch 21:351851ece6e1
default/mark1 20:6bdb2775b948
...[output pruned]...
This all seems to be working as expected. The only awkwardness I see is that the bookmarks and hg-git
-managed tags overlap some in functionality. Also, all of our Git repository is getting pulled in; there's no division between working and hg-tracking branches. But these are things a frontend porcelain can take care of.
The hg-git
seems to dislike pushing to the Git master
branch, even if we update the master
bookmark on the Mercurial side:
~/repo/yagh-test3 $ hg checkout -C master
0 files updated, 0 files merged, 0 files removed, 0 files unresolved
~/repo/yagh-test3 $ vim data && hg commit -m "advance master"
~/repo/yagh-test3 $ hg parents
changeset: 23:21c8a3d179f7
bookmark: master
tag: tip
user: Jim Pryor <dubiousjim@gmail.com>
date: Sat Jun 30 15:20:05 2012 -0400
summary: advance master
~/repo/yagh-test3 $ hg push
pushing to /home/jim/repo/test3
exporting hg objects to git
creating and sending data
Unpacking objects: 100% (4/4), done.
abort: git remote error: refs/heads/master failed to update
but that's just because we had the master
branch checked out. If we instead check out some other branch, we can push to master just fine:
$ cd ~/test3 && git checkout mark1 && cd ~/repo/yagh-test3 && hg push
Switched to branch 'mark1'
pushing to /home/jim/repo/test3
creating and sending data
Unpacking objects: 100% (4/4), done.
default::refs/heads/branch1_bookmark => GIT:71a50a04
default::refs/heads/mark3 => GIT:bb0b2c00
default::refs/heads/newbranch => GIT:d56a467e
default::refs/heads/mark2 => GIT:c071dc28
default::refs/tags/tag2 => GIT:2b9279f9
default::refs/tags/tag1 => GIT:c9afde41
default::refs/heads/mark1 => GIT:92847126
default::refs/heads/branch3_bookmark => GIT:911326b6
default::refs/heads/master => GIT:83feef0e
$ cd ~/test2 && $ git log -2 --oneline --date-order $(git rev-list --all)
83feef0 advance master
bf55acc added more to master
Again, this is something our frontend porcelain can guard against.
The hg-git
documentation describes a different mode of operation for the extension, using the commands hg gexport
and hg gimport
instead of hg push
and hg pull
. I suspect there's no good reason to use these, now that it's possible to hg push
and hg pull
to git repositories stored locally. But for completeness, let's check out how these work too.
Here is the relevant part of the hg-git
documentation:
Using hg-git to interact with a hg repository with git
TODO: this section is outdated (references to master branch and exportbranch config)
You can create a local .git repository like this:
Editr the .hg/hgrc (or your ~/.hgrc if you want to make this the default):
[git]
intree=1
Then do the following from in the hg repository:
hg gexport
This will create a .git repository in the working directory (alongside the
.hg directory) that you can interact with like any regular git repository. If
you have made commits in the git repository and want to convert them to hg
commits, first make sure the changes you want are on the master branch, then
do:
hg gimport
This will put your changes on top of the current hg tip.
Optionally you can change your hgrc to include an exportbranch statement:
[git]
intree=1
exportbranch=refs/heads/from-hg
This will cause 'hg gexport' to update the 'from-hg' branch, instead of the
master branch, so that your changes will not be lost even if you work on the
master branch.
I went through all of the steps from the preceding section with yagh-test5, yagh-test4, and so on, substituting hg gexport
for hg push
and hg gimport
for hg pull
. Here is what the .hg/hgrc
file I used:
[git]
intree=1
exportbranch=refs/heads/from-hg
branch_bookmark_suffix=_bookmark
Things seemed to act pretty much the same, except that hg-git
didn't create all those default/*
tags. It seems to do that only when pushing and pulling.
Also, the from-hg
branch seems to be ignored, even if we manually create it. The Mercurial tip
is never automatically copied, but only when some bookmark happens to point to it. (If any bookmark is "active" then it will track the tip
as you commit to it. See the Mercurial documentation.)
I'm guessing that the git.exportbranch
setting is obsolete and is no longer honored. And indeed, checking their git log we see an entry on 23 June 2009:
Drop importbranch/exportbranch options (exportbranch was really broken)
Plus "exportbranch" does not appear anywhere in the hg-git
source. I wish these guys would keep their documentation more up-to-date.
In the course of all this, I noticed that new branches on the Git side only seem to get corresponding bookmarks created for them on the Mercurial side if they have new commits, not already in the Mercurial database. I expect this holds for hg pull
ing, too, not just for hg import
ing.
OK, so the hg-git
extension seems to work pretty well. We just have to make sure we have bookmarks tracking all of the heads on the Mercurial side that we want to push or export to Git. At this point in time, there doesn't seem to be much difference in functionality between hg push
ing and hg gexport
ing.
For more discussion about how to best fit a nice frontend porcelain onto the hg-git
extension, see the accompanying Frontends page.