From: Tyler Cipriani <tyler@tylercipriani.com>
To: git@vger.kernel.org
Cc: Srinidhi Kaushik <shrinidhi.kaushik@gmail.com>,
Stefan Haller <lists@haller-berlin.de>,
"D . Ben Knoble" <ben.knoble@gmail.com>,
Phillip Wood <phillip.wood123@gmail.com>,
Johannes Schindelin <Johannes.Schindelin@gmx.de>,
Tyler Cipriani <tyler@tylercipriani.com>
Subject: [PATCH v3 1/2] push: check pushed ref for --force-if-includes
Date: Thu, 10 Sep 2026 17:05:05 -0600 [thread overview]
Message-ID: <20260910230506.1631656-2-tyler@tylercipriani.com> (raw)
In-Reply-To: <20260910230506.1631656-1-tyler@tylercipriani.com>
"--force-if-includes" ensures, "tip of the remote-tracking ref is
reachable from one of the 'reflog' entries of the local branch."
But check_if_includes_upstream() uses the local per-branch reflog based
on the destination branch rather than the branch being pushed; using
ref->name vs. ref->peer_ref->name.
This can cause confusing rejections or unintended data loss.
Using a command like:
git push --force-if-includes --force-with-lease origin src:main
False rejections: when src is an up-to-date branch, but main is
out-of-date or nonexistent, then the includes check will fail telling
users the remote ref has been updated since the last checkout.
Data loss: when src is an orphan/out-dated branch, but main is
up-to-date, then the if-includes check will allow the push, clobbering
the remote main.
Find local reflog using ref->peer_ref. When using a refspec like
HEAD:refs/heads/main, we resolve HEAD. If HEAD is a branch, use that
branch's reflog.
But if HEAD does not resolve to a branch (i.e. a detached HEAD), then we
reject the push. HEAD's reflog is too broad to tell us if the history
being pushed includes the tip of the remote. Rejecting a detached HEAD
already happens today (if the same-named local branch lacks the remote
tip); now the detached HEAD state is explicitly rejected.
Skip deletions:
git push --force-if-includes --force-with-lease origin :main
ref->deletion is set after apply_push_cas (which triggers
check_if_includes_upstream). The ref->peer_ref name is "(delete)".
Instead check with is_null_oid to detect and allow deletion.
Reported-by: Stefan Haller <lists@haller-berlin.de>
Reported-by: D. Ben Knoble <ben.knoble@gmail.com>
Signed-off-by: Tyler Cipriani <tyler@tylercipriani.com>
---
remote.c | 24 ++++++++++++++++-
t/t5533-push-cas.sh | 65 +++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 88 insertions(+), 1 deletion(-)
diff --git a/remote.c b/remote.c
index 00723b385e..326af76eeb 100644
--- a/remote.c
+++ b/remote.c
@@ -2806,7 +2806,29 @@ static int is_reachable_in_reflog(const char *local, const struct ref *remote)
*/
static void check_if_includes_upstream(struct ref *remote)
{
- struct ref *local = get_local_ref(remote->name);
+ struct ref *local;
+ const char *name;
+ int flag;
+
+ if (!remote->peer_ref)
+ return;
+
+ /* A deletion has no local history to check against. */
+ if (is_null_oid(&remote->peer_ref->new_oid))
+ return;
+
+ name = remote->peer_ref->name;
+ if (!strcmp(name, "HEAD")) {
+ name = refs_resolve_ref_unsafe(get_main_ref_store(the_repository),
+ "HEAD", 0, NULL, &flag);
+ if (!name || !(flag & REF_ISSYMREF)) {
+ /* detached HEAD: no per-branch reflog to consult */
+ remote->unreachable = 1;
+ return;
+ }
+ }
+
+ local = get_local_ref(name);
if (!local)
return;
diff --git a/t/t5533-push-cas.sh b/t/t5533-push-cas.sh
index cba26a872d..0c02151747 100755
--- a/t/t5533-push-cas.sh
+++ b/t/t5533-push-cas.sh
@@ -396,4 +396,69 @@ test_expect_success '"--force-if-includes" should allow deletes' '
)
'
+test_expect_success '"--force-if-includes" should allow forced update when using differently named branches' '
+ setup_src_dup_dst &&
+ test_when_finished "rm -fr dst src dup" &&
+ (
+ cd src &&
+ git fetch &&
+ git switch -c newbranch origin/main &&
+ git rebase HEAD --onto HEAD^ &&
+ git push --force-if-includes --force-with-lease origin newbranch:main
+ )
+'
+test_expect_success '"--force-if-includes" should allow forced update from HEAD' '
+ setup_src_dup_dst &&
+ test_when_finished "rm -fr dst src dup" &&
+ (
+ cd src &&
+ git fetch &&
+ git switch -c newbranch origin/main &&
+ git rebase HEAD --onto HEAD^ &&
+ git push --force-if-includes --force-with-lease origin HEAD:main
+ )
+'
+
+test_expect_success '"--force-if-includes" should reject forced update from differently named branches when local lacks remote ref' '
+ setup_src_dup_dst &&
+ test_when_finished "rm -fr dst src dup" &&
+ (
+ cd src &&
+ git fetch &&
+ git switch main &&
+ git reset --hard origin/main &&
+ git switch --orphan orphan &&
+ test_commit I &&
+ test_must_fail git push --force-with-lease --force-if-includes origin orphan:main
+ )
+'
+
+test_expect_success '"--force-if-includes" should reject forced update from HEAD when it lacks remote ref' '
+ setup_src_dup_dst &&
+ test_when_finished "rm -fr dst src dup" &&
+ (
+ cd src &&
+ git fetch &&
+ git switch main &&
+ git reset --hard origin/main &&
+ git switch --orphan orphan &&
+ test_commit I &&
+ test_must_fail git push --force-with-lease --force-if-includes origin HEAD:main
+ )
+'
+
+test_expect_success '"--force-if-includes" should reject forced update from detached HEAD' '
+ setup_src_dup_dst &&
+ test_when_finished "rm -fr dst src dup" &&
+ (
+ cd src &&
+ git fetch &&
+ git switch main &&
+ git reset --hard origin/main &&
+ git switch -c newbranch origin/main &&
+ git checkout HEAD^ &&
+ test_must_fail git push --force-if-includes --force-with-lease origin HEAD:main
+ )
+'
+
test_done
--
2.47.3
next prev parent reply other threads:[~2026-09-10 23:05 UTC|newest]
Thread overview: 40+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-04 21:01 [PATCH 0/2] push: fix --force-if-includes consulting wrong ref Tyler Cipriani
2026-09-04 21:01 ` [PATCH 1/2] push: check pushed ref for --force-if-includes Tyler Cipriani
2026-09-05 18:57 ` Ben Knoble
2026-09-04 21:01 ` [PATCH 2/2] push: fix --force-if-includes detached HEAD advice Tyler Cipriani
2026-09-05 18:59 ` [PATCH 0/2] push: fix --force-if-includes consulting wrong ref Ben Knoble
2026-09-06 20:24 ` Tyler Cipriani
2026-09-08 22:20 ` [PATCH v2 " Tyler Cipriani
2026-09-09 11:59 ` D. Ben Knoble
2026-09-08 22:20 ` [PATCH v2 1/2] push: check pushed ref for --force-if-includes Tyler Cipriani
2026-09-10 18:43 ` Junio C Hamano
2026-09-10 22:08 ` Tyler Cipriani
2026-09-08 22:20 ` [PATCH v2 2/2] push: fix --force-if-includes detached HEAD advice Tyler Cipriani
2026-09-10 23:05 ` [PATCH v3 0/2] push: fix --force-if-includes consulting wrong ref Tyler Cipriani
2026-09-10 23:05 ` Tyler Cipriani [this message]
2026-09-11 6:55 ` [PATCH v3 1/2] push: check pushed ref for --force-if-includes Patrick Steinhardt
2026-09-11 22:58 ` Tyler Cipriani
2026-09-11 15:31 ` Junio C Hamano
2026-09-11 23:47 ` Tyler Cipriani
2026-09-10 23:05 ` [PATCH v3 2/2] push: fix --force-if-includes detached HEAD advice Tyler Cipriani
2026-09-11 6:55 ` Patrick Steinhardt
2026-09-11 16:03 ` Junio C Hamano
2026-09-11 15:40 ` Junio C Hamano
2026-09-14 4:00 ` [PATCH v4 0/2] push: check pushed ref for --force-if-includes Tyler Cipriani
2026-09-14 4:00 ` [PATCH v4 1/2] " Tyler Cipriani
2026-09-14 4:00 ` [PATCH v4 2/2] push: fix --force-if-includes non-branch advice Tyler Cipriani
2026-09-14 13:03 ` [PATCH v4 0/2] push: check pushed ref for --force-if-includes D. Ben Knoble
2026-09-14 19:27 ` Tyler Cipriani
2026-09-14 20:52 ` D. Ben Knoble
2026-09-15 23:33 ` [PATCH v5 0/3] " Tyler Cipriani
2026-09-15 23:33 ` [PATCH v5 1/3] " Tyler Cipriani
2026-09-15 23:33 ` [PATCH v5 2/3] push: fix --force-if-includes non-branch advice Tyler Cipriani
2026-09-15 23:33 ` [PATCH v5 3/3] push: --force-if-includes should allow fast-forward Tyler Cipriani
2026-09-16 12:29 ` D. Ben Knoble
2026-09-16 15:52 ` Tyler Cipriani
2026-09-16 17:53 ` Ben Knoble
2026-09-17 22:43 ` [PATCH v6 0/3] push: check pushed ref for --force-if-includes Tyler Cipriani
2026-09-17 22:43 ` [PATCH v6 1/3] " Tyler Cipriani
2026-09-17 22:43 ` [PATCH v6 2/3] push: fix --force-if-includes non-branch advice Tyler Cipriani
2026-09-17 22:43 ` [PATCH v6 3/3] push: --force-if-includes should allow fast-forward Tyler Cipriani
2026-10-05 21:26 ` [PATCH v6 0/3] push: check pushed ref for --force-if-includes Tyler Cipriani
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260910230506.1631656-2-tyler@tylercipriani.com \
--to=tyler@tylercipriani.com \
--cc=Johannes.Schindelin@gmx.de \
--cc=ben.knoble@gmail.com \
--cc=git@vger.kernel.org \
--cc=lists@haller-berlin.de \
--cc=phillip.wood123@gmail.com \
--cc=shrinidhi.kaushik@gmail.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox