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
D. Ben Knoble <ben.knoble@gmail.com>
Date
Sep 14, 2026, 20:52 UTC
Message-ID
<CALnO6CAaoNjGmU267j_OnMErxK=vjH-sy9hAMO-WvUFOk9_vMA@mail.gmail.com>
In-Reply-To
<CAHLx=O=eH=7g=JUn5dOJgatv2xJVbQygK4C7zCA0Uv=BpzMkxQ@mail.gmail.com>
On Mon, Sep 14, 2026 at 3:27 PM Tyler Cipriani <tyler@tylercipriani.com> wrote:
Show 118 quoted lines
>
> On Mon, Sep 14, 2026 at 7:03 AM D. Ben Knoble <ben.knoble@gmail.com> wrote:
> > Hi Tyler,
>
> Hi Ben!
>
> > 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.
>
> > > 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.
>
> > >     ++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.
>
> > 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.
Thanks, I think this answers my questions…
Show 47 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?

…and I think this makes a good case for the change (but let's see what others think).

Show 26 quoted lines
> 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. :/

Personally, why --force at all then? ;) A bad habit to force things that don't need it.

Best,
-- 
D. Ben Knoble
Previous: Tyler CiprianiNext: Tyler Cipriani
Message 28 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.