Is it possible to avoid pushing new heads?
Hi,
First: thanks for providing git-cinnabar! I now see a realistic chance to avoid working with Mercurial at my job, that would be great.
The Problem
However, I have one problem that I cannot work around currently: Under certain (not so exotic, see below) circumstances, git-cinnabar does push new heads into the Mercurial repo without confirmation. Mercurial itself does not do this, a hg push that would create a new head in the current branch fails with an error "abort: push creates new remote head …"
With git-cinnabar, the behavior seems to depend on whether git already knows that it will push a new head at the time of push, i.e.:
1.) Situation A Say you have a git-cinnabar repository, and you are currently on a remote-tracking branch. You do git reset --hard 'HEAD^'. Now, your next commit would be a new head in Mercurial terms (or a non-fast-forward in git terms…). If you now make a modification, commit and push, git rejects the push with the usual 'this is not a fast-forward' warning. This is exactly the behavior I want.
2.) Situation B You have the same repository, but you do not do git reset --hard 'HEAD^'. So in your repository, it looks like your new commit neatly sits on top of the branch and should be a fast-forward. However, some other person has pushed to the branch since your last git pull, and thus your new commit actually is not a fast-forward, but it would require a pull-and-rebase. If you now run git push, git-cinnabar just seems to go ahead and push your commit as a new head without any confirmation or --force. (See full reproduction below).
Question
Is it possible to get git-cinnabar to also reject pushes in "Situation B" above, i.e., if the fact that a second head is being pushed is only discovered during push?
Full Reproduction
This is a minimal example to reproduce the problem locally.
Step 1: Create new hg repo and a commit
> mkdir hgrepo
> cd hgrepo
> hg init .
> echo "test" > hgtestfile
> hg add hgtestfile
> hg commit -m "Initial commit on hg"
changeset: 0:b9ca7780ad67
user: Lukas Barth
date: Fri Mar 22 10:48:35 2024 +0100
summary: Initial commit on hg
Step 2: Clone a git-cinnabar repository from it
> cd ..
> git clone hg::file://$(pwd)/hgrepo gitrepo # absolute path is necessary here
Cloning into 'gitrepo'...
Reading 1 changesets
Reading and importing 1 manifests
Reading and importing 1 revisions of 1 files
Importing 1 changesets
Updating metadata...
Unpacking objects: 100% (15/15), 1.53 KiB | 781.00 KiB/s, done.
It is recommended that you set "remote.origin.prune" or "fetch.prune" to "true".
git config remote.origin.prune true
or
git config fetch.prune true
> cd gitrepo
> git config remote.origin.prune true
> git --no-pager log
commit 50507633e44aba4d61a235a2a7f84acedd4b823c (HEAD -> branches/default/tip, origin/branches/default/tip, origin/HEAD)
Author: Lukas Barth
Date: Fri Mar 22 10:48:35 2024 +0100
Initial commit on hg
> git status
On branch branches/default/tip
Your branch is up to date with 'origin/branches/default/tip'.
nothing to commit, working tree clean
Step 3: Add another commit to the hg repo
> cd ../hgrepo
> echo "second hg commit" > secondfile
> hg add secondfile
> hg commit -m "Second HG commit"
Step 4: Without pulling, create a commit in the git repo
> cd ../gitrepo
> echo "git change" > gitfile
> git add gitfile
> git commit -m "git change"
> git --no-pager log
commit f8af826004bd40e39d32c0f227ec50b655597c93 (HEAD -> branches/default/tip)
Author: Lukas Barth
Date: Fri Mar 22 10:54:31 2024 +0100
git change
commit 50507633e44aba4d61a235a2a7f84acedd4b823c (origin/branches/default/tip, origin/HEAD)
Author: Lukas Barth
Date: Fri Mar 22 10:48:35 2024 +0100
Initial commit on hg
Step 5: Push in the git repo
We're now in 'Situation B' from above. Let's push:
> git push
Bundling 1 changesets
Bundling 1 manifests
Bundling 1 revisions of 1 files
remote: adding changesets
remote: adding manifests
remote: adding file changes
remote: added 1 changesets with 1 changes to 1 files (+1 heads)
Updating metadata...To hg::hgrepo
* [new branch] branches/default/tip -> branches/default/tip
Unpacking objects: 100% (11/11), 1.55 KiB | 1.55 MiB/s, done.
The (+1 heads) in the log above already gives away what has happened: git-cinnabar has pushed a new head.
Aftermath
This is how the repo looks like from git's perspective. Note that the second mercurial commit is still not visible in git:
> git --no-pager log
commit f8af826004bd40e39d32c0f227ec50b655597c93 (HEAD -> branches/default/tip, origin/branches/default/tip, origin/HEAD)
Author: Lukas Barth
Date: Fri Mar 22 10:54:31 2024 +0100
git change
commit 50507633e44aba4d61a235a2a7f84acedd4b823c
Author: Lukas Barth
Date: Fri Mar 22 10:48:35 2024 +0100
Initial commit on hg
And even after pulling, the new commit does not become visible (because from the git-cinnabar perspective, which tracks each Mercurial-head in a git-branch, it is part of a different branch):
> git pull
Reading 1 changesets
Reading and importing 1 manifests
Reading and importing 1 revisions of 1 files
Importing 1 changesets
Updating metadata...
Unpacking objects: 100% (14/14), 2.09 KiB | 1.04 MiB/s, done.
Checking 1 imported file root and head revisions
From hg::hgrepo
* [new branch] branches/default/9f2f25ebf99890d6282b33cc588ba17758d73cb8 -> origin/branches/default/9f2f25ebf99890d6282b33cc588ba17758d73cb8
Already up to date.
> git --no-pager log
commit f8af826004bd40e39d32c0f227ec50b655597c93 (HEAD -> branches/default/tip, origin/branches/default/tip, origin/HEAD)
Author: Lukas Barth
Date: Fri Mar 22 10:54:31 2024 +0100
git change
commit 50507633e44aba4d61a235a2a7f84acedd4b823c
Author: Lukas Barth
Date: Fri Mar 22 10:48:35 2024 +0100
Initial commit on hg
The hg repo now has a branch with two heads:
> cd ../hgrepo
> hg log --pager=off
changeset: 2:339433b7588b
tag: tip
parent: 0:b9ca7780ad67
user: Lukas Barth
date: Fri Mar 22 10:54:31 2024 +0100
summary: git change
changeset: 1:9f2f25ebf998
user: Lukas Barth
date: Fri Mar 22 10:53:33 2024 +0100
summary: Second HG commit
changeset: 0:b9ca7780ad67
user: Lukas Barth
date: Fri Mar 22 10:48:35 2024 +0100
summary: Initial commit on hg
> hg heads
changeset: 2:339433b7588b
tag: tip
parent: 0:b9ca7780ad67
user: Lukas Barth
date: Fri Mar 22 10:54:31 2024 +0100
summary: git change
changeset: 1:9f2f25ebf998
user: Lukas Barth
date: Fri Mar 22 10:53:33 2024 +0100
summary: Second HG commit
To some extent, it's a problem with git. But it's one we can do something about.
We do detect this situation already, but only when the remote is fully up-to-date before pushing. So if you fetch before pushing, you won't have this problem (as a workaround for now).
Here's a patch to the test to make the problem happen in the test suite.
diff --git a/tests/push-refs.t b/tests/push-refs.t
index fa3094f5..d3ea9de1 100755
--- a/tests/push-refs.t
+++ b/tests/push-refs.t
@@ -98,13 +98,21 @@ Pushing `a` and `c` to the default branch.
To hg::.*/push-refs.t/repo-from-git (re)
* [new branch] 7688446e0a5d5b6108443632be74c9bca72d31b1 -> branches/default/tip
- $ git -C repo-git2 push origin 7688446e0a5d5b6108443632be74c9bca72d31b1:refs/heads/branches/default/tip
+ $ git -C repo-git2 push origin 8b86a58578d5270969543e287634e3a2f122a338:refs/heads/branches/default/tip
remote: adding changesets
remote: adding manifests
remote: adding file changes
- remote: added 2 changesets with 2 changes to 2 files
+ remote: added 1 changesets with 1 changes to 1 files
To hg::.*/push-refs.t/repo-from-git2 (re)
- * [new branch] 7688446e0a5d5b6108443632be74c9bca72d31b1 -> branches/default/tip
+ * [new branch] 8b86a58578d5270969543e287634e3a2f122a338 -> branches/default/tip
+
+ $ git -c cinnabar.data=never -C repo-git2 push origin 7688446e0a5d5b6108443632be74c9bca72d31b1:refs/heads/branches/default/tip
+ remote: adding changesets
+ remote: adding manifests
+ remote: adding file changes
+ remote: added 1 changesets with 1 changes to 1 files
+ To hg::.*/push-refs.t/repo-from-git2 (re)
+ 8b86a58..7688446 7688446e0a5d5b6108443632be74c9bca72d31b1 -> branches/default/tip
$ git -C repo-git ls-remote hg::$REPO-from-hg
7688446e0a5d5b6108443632be74c9bca72d31b1 HEAD
@@ -117,8 +125,8 @@ Pushing `a` and `c` to the default branch.
$ git -C repo-git2 ls-remote
From hg::.*/push-refs.t/repo-from-git2 (re)
- 7688446e0a5d5b6108443632be74c9bca72d31b1 HEAD
- 7688446e0a5d5b6108443632be74c9bca72d31b1 refs/heads/branches/default/tip
+ 0000000000000000000000000000000000000000 HEAD
+ 0000000000000000000000000000000000000000 refs/heads/branches/default/tip
Pushing `b` fails because it would add a new head to the branch.
I'm not sure when I'll get to actually fixing the issue.
Thanks for having a look at this! The problem is compounded by the fact that our HG server rejects pushing new heads, and I'm not sure whether git-cinnabar handles rejected pushes correctly (whatever that would be…)
However, I did look at the hg source, and I'm not sure anymore whether hg itself is actually able to detect the problem 100% of the time. It I read my notes correctly, they also only do that check on the client side (in discovery.py:checkheads()) after refreshing the data from the repository (i.e., a sort-of-fetch). So, if just the correct race condition appears, I think the check in hg also fails to find this condition. I'm not sure if there is even a way of doing this server-side during the push (without having the server just reject the push as ours does).
I’ve run into a semi-related issue(?): that the logs of a push may say that cinnabar added a new head, when it actually didn’t. I can get more details on Monday, but is this something that’s expected?
I also would like to see this feature in cinnabar, and as such I’ll probably take a look at implementing it myself soon. But I want to figure out ^ first, hence this comment. :)