git/list[1] front-page[2] threads[3] people[4] search[5] about
 

Re: [PATCH v4 0/2] push: check pushed ref for --force-if-includes

From
Tyler Cipriani <tyler@tylercipriani.com>
Date
Sep 14, 2026, 19:27 UTC
Message-ID
<CAHLx=O=eH=7g=JUn5dOJgatv2xJVbQygK4C7zCA0Uv=BpzMkxQ@mail.gmail.com>
In-Reply-To
<CALnO6CDz8QBcBojmhjgwgWzi4oUbs+V4KVQ1h0+JgN7k0v-SYQ@mail.gmail.com>
On Mon, Sep 14, 2026 at 7:03 AM D. Ben Knoble <ben.knoble@gmail.com> wrote:
> Hi Tyler,
Hi Ben!
Show 10 quoted lines
> On Mon, Sep 14, 2026 at 12:00 AM Tyler Cipriani <tyler@tylercipriani.com> wrote:
> >
> > Changes since v3:
> >
> > - check_if_includes_upstream unconditionally resolves peer_ref with
> >   RESOLVE_REF_READING, now all non-branch ref pushes will be rejected
> >   when using --force-if-includes
> > - add test for --force-if-includes tag push 1/2
>
> This is intriguing and seems like a significant behavior change, let's read on…

It's definitely true that this is a behavior change and it'll add some friction to your process. And it's also true that the current behavior is failing to provide the guarantees it claims.

Show 18 quoted lines
> > Range-diff against v3:
> > 1:  da27c421ed ! 1:  e7912c3fd0 push: check pushed ref for --force-if-includes
> >     @@ Commit message
> >     -    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.
> >     +    Instead, use ref->peer_ref to locate a branch with a reflog. But if ref
> >     +    does not resolve to a branch (e.g., a detached HEAD, a tag, an oid),
> >     +    then we reject the push. The alternative would be to use HEAD's reflog,
> >     +    which is too broad to tell us if the history being pushed includes the
> >     +    tip of the remote. We need a per-branch reflog, which means that pushes
> >     +    of a ref that do not resolve to a branch are rejected. Rejecting the
> >     +    push of a ref like a detached HEAD already happens today (if the
> >     +    same-named local branch lacks the remote tip); now the detached HEAD and
> >     +    other non-branch pushes are explicitly rejected.
>
> So, we would now reject a force-push whose source is anything but a branch (with
> force-if-includes, and that presumably includes push.useForceIfIncludes)?

I should clarify, reject the force-push of any source not ultimately resolvable to a branch; e.g., HEAD will work if resolves to a branch.

Show 36 quoted lines
> >     ++test_expect_success '"--force-if-includes" should reject forced update from tag' '
> >     ++  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^ &&
> >     ++          git tag stable &&
> >     ++          test_must_fail git push --force-if-includes --force-with-lease origin stable:main
> >     ++  )
> >     ++'
>
> Which is what I think this test says.
>
> I think this would break a common thing I do at work (although this is soon to
> be deprecated, so take my anecdote with appropriate salt; I can't claim that no
> one else relies on it, of course):
>
> As I think I described in the message you linked, I have an alias "pf = push
> --force-with-lease" and push.useForceIfIncludes=true in config. Our team has a
> "main" release branch and a "hotfix" release branch for emergencies. When
> hotfixing, we first reset the hotfix branch to the last tag to go out to our
> production environment, which I typically do like this:
>
>     # validate that we won't lose any interesting commits (no regressions) with
>     # something like
>     git log --oneline --graph --boundary --cherry-mark --left-right
> origin/hotfix...<TAG>
>     # push
>     git pf origin <TAG>:hotfix
>
> (On a second pass before sending, I can't recall if this works as-is when I
> don't have a local hotfix branch tracking origin/hotfix.)

Yes, this workflow will break. And it will not work today without a local branch named "hotfix". It's broken today, insofar as this is a false pass since push.useForceIfIncludes is unable to say anything about whether you've integrated origin's hotfix branch into the <TAG>, you're pushing so it only incidentally works.

Today, git pf is actually checking that your refs/heads/hotfix's reflog has the tip of origin's refs/heads/hotfix. But it makes no promises about <TAG>. That is, you could:

    git checkout hotfix && git pull # This line is what makes it work today
    git checkout --orphan junk
    git commit -m --allow-empty 'Totally unrelated empty commit'
    git tag <TAG>
    git pf origin <TAG>:hotfix

And pf will allow that to happen since origin/hotfix's tip has been integrated with your local refs/heads/hotfix, which is what it's checking today.

Show 11 quoted lines
> If I'm reading this version right, I would now have to say
>
>     git pf --no-force-if-includes origin <TAG>:hotfix
>
> or perhaps better
>
>     git pf --no-force-if-includes --force-with-lease=hotfix[:origin/hotfix] …
>
> probably after seeing a (hopefully improved?) message after the original
> command. (Do I need to disable force-if-includes in the more-specific lease
> command?)
    git pf --force-with-lease=hotfix:origin/hotfix origin <TAG>:hotfix

Should be sufficient and as I understand your process, that's what you're after. The explicit --force-with-lease argument makes --force-if-includes a no-op, so --no-force-if-includes should be unnecessary.

Show 28 quoted lines
> Now, on the one hand, enshrining existing behavior is good for backwards
> compatibility but has earned us a bit of a reputation for not innovating in
> useful ways ;) On the other, I wonder if the description of force-if-includes
> allows some latitude to break with existing behavior here.
>
> The relevant docs say
>
>        --force-if-includes, --no-force-if-includes
>            Force an update only if the tip of the remote-tracking ref has been
>            integrated locally.
>
>            This option enables a check that verifies if the tip of the
>            remote-tracking ref is reachable from one of the "reflog" entries of
>            the local branch based in it for a rewrite. The check ensures that
>            any updates from the remote have been incorporated locally by
>            rejecting the forced update if that is not the case.
>
> It is unclear to me what "one of the 'reflog' entries of the local branch based
> in it" means! Ignoring that, the surrounding text only talks about whether the
> remote-tracking ref's tip (or "updates from the remote") have been "integrated
> locally."
>
> So I think we *could* say that, in this case, we don't have enough information
> from "<TAG>:hotfix" to check whether "origin/hotfix" has been integrated locally
> or not, and we should tighten the meaning of the check. (Perhaps when
> "--force-with-lease=hotfix" is given, though, we now have more information
> available to check---but that could be outside the scope of this series if we
> don't mind breaking backwards compatibility now.)

From my perspective, this is similar to the detached HEAD discussion from 2020[0] where "[the reflog of HEAD not attached to a branch] _does_ answer a different question from what we actually asked."

[0]: <https://lore.kernel.org/git/nycvar.QRO.7.76.6.2009161214030.56@tvgsbejvaqbjf.bet/>

I opted for a direction requiring explicit arguments to express intent, since that's the only way to ensure --force-if-includes aligns with (how I read) the documentation and the previous discussions.

Specifically, with tags:
- tags may have a reflog, but it answers a different question vs. "has
this tag integrated changes from an upstream" it answers what oid/ref
does this tag point to
- tags may incidentally point at oids referenced by branches with
reflogs, but there may also be several branches pointed to the same
oid, so which would we choose?

BUT I just realized there is existing, more fundamental breakage with --force-if-includes here that I'm making worse.

There is one case where we do have enough information to say whether <TAG> has integrated the tip of the remote-ref locally: fast-forward push. And that's actually broken today, too :)

    git --version
    git version 2.47.3
    git clone repo.git repo && cd repo
    git commit --allow-empty -m 'Normal, no-force-needed fast forward commit'
    git reflog expire --expire=all --all
    # Regular fast-forward push fails, even though it does not require
--force to begin with
    git push --force-with-lease --force-if-includes origin main
    ! [rejected]        main -> main (remote ref updated since checkout)

Checking for fast-forward happens after --force-if-includes checks the reflog. So that will need a fix…

My change makes an existing problem more acute, and probably requires a fix before other fixes can merge. Otherwise, --force-if-includes will always fail when pushing tags and detached heads, even when they're fast forward changes, adding needless friction to otherwise safe pushes (e.g., for tags that fast-forward a branch). So v5 will require a third change that touches other functions in remote.c. :/

> Thanks,
> D. Ben Knoble
Thank you for all the review and thoughts!
Previous: D. Ben KnobleNext: D. Ben Knoble
Message 27 of 40 in “push: fix --force-if-includes consulting wrong ref”
  1. 0/2 push: fix --force-if-includes consulting wrong refTyler Cipriani, Sep 4, 2026
  2. 1/2 push: check pushed ref for --force-if-includesTyler Cipriani, Sep 4, 2026
  3. Ben KnobleSep 5, 2026
  4. 2/2 push: fix --force-if-includes detached HEAD adviceTyler Cipriani, Sep 4, 2026
  5. Ben KnobleSep 5, 2026
  6. Tyler CiprianiSep 6, 2026
  7. 0/2 push: fix --force-if-includes consulting wrong refTyler Cipriani, Sep 8, 2026
  8. D. Ben KnobleSep 9, 2026
  9. 1/2 push: check pushed ref for --force-if-includesTyler Cipriani, Sep 8, 2026
  10. Junio C HamanoSep 10, 2026
  11. Tyler CiprianiSep 10, 2026
  12. 2/2 push: fix --force-if-includes detached HEAD adviceTyler Cipriani, Sep 8, 2026
  13. 0/2 push: fix --force-if-includes consulting wrong refTyler Cipriani, Sep 10, 2026
  14. 1/2 push: check pushed ref for --force-if-includesTyler Cipriani, Sep 10, 2026
  15. Patrick SteinhardtSep 11, 2026
  16. Tyler CiprianiSep 11, 2026
  17. Junio C HamanoSep 11, 2026
  18. Tyler CiprianiSep 11, 2026
  19. 2/2 push: fix --force-if-includes detached HEAD adviceTyler Cipriani, Sep 10, 2026
  20. Patrick SteinhardtSep 11, 2026
  21. Junio C HamanoSep 11, 2026
  22. Junio C HamanoSep 11, 2026
  23. 0/2 push: check pushed ref for --force-if-includesTyler Cipriani, Sep 14, 2026
  24. 1/2 push: check pushed ref for --force-if-includesTyler Cipriani, Sep 14, 2026
  25. 2/2 push: fix --force-if-includes non-branch adviceTyler Cipriani, Sep 14, 2026
  26. D. Ben KnobleSep 14, 2026
  27. Tyler CiprianiSep 14, 2026
  28. D. Ben KnobleSep 14, 2026
  29. 0/3 push: check pushed ref for --force-if-includesTyler Cipriani, Sep 15, 2026
  30. 1/3 push: check pushed ref for --force-if-includesTyler Cipriani, Sep 15, 2026
  31. 2/3 push: fix --force-if-includes non-branch adviceTyler Cipriani, Sep 15, 2026
  32. 3/3 push: --force-if-includes should allow fast-forwardTyler Cipriani, Sep 15, 2026
  33. D. Ben KnobleSep 16, 2026
  34. Tyler CiprianiSep 16, 2026
  35. Ben KnobleSep 16, 2026
  36. 0/3 push: check pushed ref for --force-if-includesTyler Cipriani, Sep 17, 2026
  37. 1/3 push: check pushed ref for --force-if-includesTyler Cipriani, Sep 17, 2026
  38. 2/3 push: fix --force-if-includes non-branch adviceTyler Cipriani, Sep 17, 2026
  39. 3/3 push: --force-if-includes should allow fast-forwardTyler Cipriani, Sep 17, 2026
  40. Tyler CiprianiOct 5, 2026

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.