push: --force-if-includes should allow fast-forward

In set_ref_status_for_push, we apply --force-if-includes's reflog
reachability checks before fast-forward rules. As a result, valid
fast-forward pushes may be rejected when a force push is unneeded; like
when the reflog is expired:

    git clone repo.git repo
    git commit --allow-empty -m 1
    git reflog expire --expire=all --all
    git push --force-with-lease --force-if-includes origin main
    ! [rejected]    main -> main (remote ref updated since checkout)

Rejecting fast-forwards is a mismatch with the --force-if-includes
documentation "Force an update only if the tip of the remote-tracking
ref has been integrated locally."

Instead, defer reflog rejection for --force-if-includes until after
determining if a force push is needed.

Remember the reflog rejection reason as needs_force_reject_reason. If
the fast-forward rules reject the push for a ref, show the reflog
rejection reason to preserve existing behavior. But if fast-forward
rules allow a push (a fast-forward, deletion, or new ref), then a force
push is unneeded, the reflog rejection reason is discarded, and the push
proceeds.

Signed-off-by: Tyler Cipriani <tyler@tylercipriani.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
seen
Tyler Cipriani 2026-09-17 16:43:51 -06:00 committed by Junio C Hamano
parent 1af0f4f9ea
commit 795ce211df
2 changed files with 40 additions and 3 deletions

View File

@ -1669,6 +1669,7 @@ void set_ref_status_for_push(struct ref *remote_refs, int send_mirror,
for (ref = remote_refs; ref; ref = ref->next) {
int force_ref_update = ref->force || force_update;
int reject_reason = 0;
int needs_force_reject_reason = 0;

if (ref->peer_ref)
oidcpy(&ref->new_oid, &ref->peer_ref->new_oid);
@ -1693,16 +1694,17 @@ void set_ref_status_for_push(struct ref *remote_refs, int send_mirror,
*
* If the tip of the remote-tracking ref is unreachable
* from any reflog entry of its local ref indicating a
* possible update since checkout; reject the push.
* possible update since checkout, then remember the
* rejection in case force push is needed.
*/
if (ref->expect_old_sha1) {
if (!oideq(&ref->old_oid, &ref->old_oid_expect))
reject_reason = REF_STATUS_REJECT_STALE;
else if (ref->check_reachable && ref->unreachable)
reject_reason =
needs_force_reject_reason =
REF_STATUS_REJECT_REMOTE_UPDATED;
else if (ref->check_reachable && ref->unverifiable)
reject_reason =
needs_force_reject_reason =
REF_STATUS_REJECT_UNVERIFIABLE;
else
/*
@ -1746,6 +1748,14 @@ void set_ref_status_for_push(struct ref *remote_refs, int send_mirror,
reject_reason = REF_STATUS_REJECT_NONFASTFORWARD;
}

/*
* If fast-forward rules rejected the push and we were
* asked to verify the reflog but were unable to, then
* reflog verification is the right reject_reason.
*/
if (needs_force_reject_reason && reject_reason)
reject_reason = needs_force_reject_reason;

/*
* "--force" will defeat any rejection implemented
* by the rules above.

View File

@ -421,6 +421,33 @@ test_expect_success '"--force-if-includes" should allow forced update from HEAD'
)
'

test_expect_success '"--force-if-includes" should allow fast-forward push without local reflog' '
setup_src_dup_dst &&
test_when_finished "rm -fr dst src dup" &&
(
cd src &&
git fetch &&
git switch main &&
git reset --hard origin/main &&
test_commit I &&
git reflog expire --expire=all --all &&
git push --force-with-lease --force-if-includes origin main
)
'

test_expect_success '"--force-if-includes" should allow fast-forward push from tag' '
setup_src_dup_dst &&
test_when_finished "rm -fr dst src dup" &&
(
cd src &&
git fetch &&
git switch -c newbranch origin/main &&
test_commit I &&
git tag T &&
git push --force-with-lease --force-if-includes origin T: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" &&