git-cinnabar icon indicating copy to clipboard operation
git-cinnabar copied to clipboard

Is it possible to avoid pushing new heads?

Open tinloaf opened this issue 1 year ago • 3 comments

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

tinloaf avatar Mar 25 '24 09:03 tinloaf

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.

glandium avatar Apr 22 '24 20:04 glandium

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).

tinloaf avatar Apr 23 '24 12:04 tinloaf

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. :)

winterqt avatar Jun 14 '25 18:06 winterqt