From 80871f356e88d23cc32cd852fd4a4548e861f47c Mon Sep 17 00:00:00 2001 From: Runxi Yu Date: Mon, 30 Mar 2026 13:18:20 +0200 Subject: [PATCH 1/3] t5516: test updateInstead with worktree and unborn bare HEAD This is a regression test which should presently fail, to demonstrate the behavior I encountered that looks like a bug. When a bare repository has a worktree checked out on a separate branch, receive.denyCurrentBranch=updateInstead should allow a push to that branch and update the linked worktree, as long as the linked worktree is clean. But, if the bare repository's own HEAD is repointed to an unborn branch, the push is rejected with "Working directory has staged changes", even though the linked worktree itself is clean. This test is essentially a minimal working example of what I encountered while actually using Git; it might not be the optimal way to demonstrate the underlying bug. I suspect builtin/receive-pack.c is using the bare repository's HEAD even when comparing it to the worktree's index. Signed-off-by: Runxi Yu Signed-off-by: Junio C Hamano --- t/t5516-fetch-push.sh | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh index 29e2f17608..f44250c38f 100755 --- a/t/t5516-fetch-push.sh +++ b/t/t5516-fetch-push.sh @@ -1816,6 +1816,24 @@ test_expect_success 'denyCurrentBranch and bare repository worktrees' ' test_must_fail git push --delete bare.git wt ' +# NEEDSWORK: updateInstead unexpectedly fails when bare HEAD points to unborn +# branch (or probably any ref that differs from the target worktree) despite +# the target worktree being clean. This seems to be because receive-pack.c +# diffs the target worktree index against the bare repository HEAD. +test_expect_failure 'updateInstead with bare repository worktree and unborn bare HEAD' ' + test_when_finished "rm -fr bare.git cloned" && + git clone --bare . bare.git && + git -C bare.git worktree add wt && + git -C bare.git config receive.denyCurrentBranch updateInstead && + git -C bare.git symbolic-ref HEAD refs/heads/unborn && + test_must_fail git -C bare.git rev-parse -q --verify HEAD^{commit} && + git clone . cloned && + test_commit -C cloned mozzarella && + git -C cloned push ../bare.git HEAD:wt && + test_path_exists bare.git/wt/mozzarella.t && + test "$(git -C cloned rev-parse HEAD)" = "$(git -C bare.git/wt rev-parse HEAD)" +' + test_expect_success 'refuse fetch to current branch of worktree' ' test_when_finished "git worktree remove --force wt && git branch -D wt" && git worktree add wt && From b310755ecaf4459eddd4f602b3cb02e793c01177 Mon Sep 17 00:00:00 2001 From: Pablo Sabater Date: Mon, 30 Mar 2026 13:18:21 +0200 Subject: [PATCH 2/3] t5516: clean up cloned and new-wt in denyCurrentBranch and worktrees test The 'denyCurrentBranch and worktrees' test creates a 'cloned' and a 'new-wt' but it doesn't clean them after the test. This makes other tests that use the same name after this one to fail. Add test_when_finished to clean them at the end. Signed-off-by: Pablo Sabater Signed-off-by: Junio C Hamano --- t/t5516-fetch-push.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh index f44250c38f..c40f2790d8 100755 --- a/t/t5516-fetch-push.sh +++ b/t/t5516-fetch-push.sh @@ -1792,6 +1792,7 @@ test_expect_success 'updateInstead with push-to-checkout hook' ' ' test_expect_success 'denyCurrentBranch and worktrees' ' + test_when_finished "rm -fr cloned && git worktree remove --force new-wt" && git worktree add new-wt && git clone . cloned && test_commit -C cloned first && From 8151f4fe7e4bf36f2656ae849a4ffaf386708178 Mon Sep 17 00:00:00 2001 From: Pablo Sabater Date: Mon, 30 Mar 2026 13:18:22 +0200 Subject: [PATCH 3/3] receive-pack: use worktree HEAD for updateInstead When a bare repo has linked worktrees, and its HEAD points to an unborn branch, pushing to a wt branch with updateInstead fails and rejects the push, even if the wt is clean. This happens because HEAD is checked only for the bare repo context, instead of the wt. Remove head_has_history and check for worktree->head_oid which does have the correct HEAD of the wt. Update the test added by Runxi's patch to expect success. Signed-off-by: Pablo Sabater Signed-off-by: Junio C Hamano --- builtin/receive-pack.c | 39 +++++++++++++++------------------------ t/t5516-fetch-push.sh | 6 +----- 2 files changed, 16 insertions(+), 29 deletions(-) diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c index e34edff406..26a3a0bcd3 100644 --- a/builtin/receive-pack.c +++ b/builtin/receive-pack.c @@ -1380,32 +1380,16 @@ static int update_shallow_ref(struct command *cmd, struct shallow_info *si) return 0; } -/* - * NEEDSWORK: we should consolidate various implementations of "are we - * on an unborn branch?" test into one, and make the unified one more - * robust. !get_sha1() based check used here and elsewhere would not - * allow us to tell an unborn branch from corrupt ref, for example. - * For the purpose of fixing "deploy-to-update does not work when - * pushing into an empty repository" issue, this should suffice for - * now. - */ -static int head_has_history(void) -{ - struct object_id oid; - - return !repo_get_oid(the_repository, "HEAD", &oid); -} - static const char *push_to_deploy(unsigned char *sha1, struct strvec *env, - const char *work_tree) + const struct worktree *worktree) { struct child_process child = CHILD_PROCESS_INIT; strvec_pushl(&child.args, "update-index", "-q", "--ignore-submodules", "--refresh", NULL); strvec_pushv(&child.env, env->v); - child.dir = work_tree; + child.dir = worktree->path; child.no_stdin = 1; child.stdout_to_stderr = 1; child.git_cmd = 1; @@ -1417,7 +1401,7 @@ static const char *push_to_deploy(unsigned char *sha1, strvec_pushl(&child.args, "diff-files", "--quiet", "--ignore-submodules", "--", NULL); strvec_pushv(&child.env, env->v); - child.dir = work_tree; + child.dir = worktree->path; child.no_stdin = 1; child.stdout_to_stderr = 1; child.git_cmd = 1; @@ -1427,9 +1411,16 @@ static const char *push_to_deploy(unsigned char *sha1, child_process_init(&child); strvec_pushl(&child.args, "diff-index", "--quiet", "--cached", "--ignore-submodules", - /* diff-index with either HEAD or an empty tree */ - head_has_history() ? "HEAD" : empty_tree_oid_hex(the_repository->hash_algo), - "--", NULL); + /* + * diff-index with either HEAD or an empty tree + * + * NEEDSWORK: is_null_oid() cannot know whether it's an + * unborn HEAD or a corrupt ref. It works for now because + * it's only needed to know if we are comparing HEAD or an + * empty tree. + */ + !is_null_oid(&worktree->head_oid) ? "HEAD" : + empty_tree_oid_hex(the_repository->hash_algo), "--", NULL); strvec_pushv(&child.env, env->v); child.no_stdin = 1; child.no_stdout = 1; @@ -1442,7 +1433,7 @@ static const char *push_to_deploy(unsigned char *sha1, strvec_pushl(&child.args, "read-tree", "-u", "-m", hash_to_hex(sha1), NULL); strvec_pushv(&child.env, env->v); - child.dir = work_tree; + child.dir = worktree->path; child.no_stdin = 1; child.no_stdout = 1; child.stdout_to_stderr = 0; @@ -1490,7 +1481,7 @@ static const char *update_worktree(unsigned char *sha1, const struct worktree *w retval = push_to_checkout(sha1, &invoked_hook, &env, worktree->path); if (!invoked_hook) - retval = push_to_deploy(sha1, &env, worktree->path); + retval = push_to_deploy(sha1, &env, worktree); strvec_clear(&env); free(git_dir); diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh index c40f2790d8..117cfa051f 100755 --- a/t/t5516-fetch-push.sh +++ b/t/t5516-fetch-push.sh @@ -1817,11 +1817,7 @@ test_expect_success 'denyCurrentBranch and bare repository worktrees' ' test_must_fail git push --delete bare.git wt ' -# NEEDSWORK: updateInstead unexpectedly fails when bare HEAD points to unborn -# branch (or probably any ref that differs from the target worktree) despite -# the target worktree being clean. This seems to be because receive-pack.c -# diffs the target worktree index against the bare repository HEAD. -test_expect_failure 'updateInstead with bare repository worktree and unborn bare HEAD' ' +test_expect_success 'updateInstead with bare repository worktree and unborn bare HEAD' ' test_when_finished "rm -fr bare.git cloned" && git clone --bare . bare.git && git -C bare.git worktree add wt &&