Skip to content

branch: let --delete-merged find squash merged branches - #2425

Open
HaraldNordgren wants to merge 3 commits into
git:masterfrom
HaraldNordgren:branch-delete-squashed
Open

HaraldNordgren wants to merge 3 commits into
git:masterfrom
HaraldNordgren:branch-delete-squashed

Conversation

@HaraldNordgren

@HaraldNordgren HaraldNordgren commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Branches merged on GitHub with "Squash and merge" or "Rebase and merge" are never deleted by git branch --delete-merged, because their tips are not reachable from the upstream. This treats such a branch as merged when some upstream commit contains all of its changes, and names that commit in the output:

Deleted branch topic (was 1a2b3c4, landed as 9f8e7d6).

Changes in v3:

  • Find rebase merged branches by pairing their commits the way git range-diff does, in a commit of its own. Add a prep commit so range-diff's commit pairing can be reused.
  • Make squash detection opt-in with --squashed, replacing --no-squashed.

Changes in v2:

  • Diff each upstream commit once and share the result between all branches, faster than v1.
  • Fix a branch merged with a merge commit being reported as "landed as" the merge when another branch was searched first, and add a test for it. Show the topic commit itself in "landed as" for work that arrived through a merge.
  • Use git merge --squash as the example in the commit message and mention GitHub's squash and rebase buttons.
  • Add --no-squashed to skip the squash search.

cc: "Kristoffer Haugsbakk" kristofferhaugsbakk@fastmail.com
cc: "D. Ben Knoble" ben.knoble@gmail.com
cc: Phillip Wood phillip.wood123@gmail.com

@HaraldNordgren

Copy link
Copy Markdown
Contributor Author

/submit

@gitgitgadget-git

Copy link
Copy Markdown

Submitted as pull.2425.git.git.1790667030497.gitgitgadget@gmail.com

To fetch this version into FETCH_HEAD:

git fetch https://github.com/gitgitgadget/git/ pr-git-2425/HaraldNordgren/branch-delete-squashed-v1

To fetch this version to local tag pr-git-2425/HaraldNordgren/branch-delete-squashed-v1:

git fetch --no-tags https://github.com/gitgitgadget/git/ tag pr-git-2425/HaraldNordgren/branch-delete-squashed-v1

@gitgitgadget-git

Copy link
Copy Markdown

"Kristoffer Haugsbakk" wrote on the Git mailing list (how to reply to this email):

On Tue, Sep 29, 2026, at 09:30, Harald Nordgren via GitGitGadget wrote:
> From: Harald Nordgren <haraldnordgren@gmail.com>
>
> Branches merged on GitHub with "Squash and merge" or "Rebase and
> merge" are never deleted by "git branch --delete-merged". The upstream
> holds a rewritten copy of their work, so their tips are not reachable
> from it and they look unmerged forever.

An example closer to git(1)’s home:

    git merge --squash
    git commit

>[snip]

@gitgitgadget-git

Copy link
Copy Markdown

User "Kristoffer Haugsbakk" <kristofferhaugsbakk@fastmail.com> has been added to the cc: list.

@gitgitgadget-git

Copy link
Copy Markdown

Harald Nordgren wrote on the Git mailing list (how to reply to this email):

On Tue, Sep 29, 2026 at 9:47 AM Kristoffer Haugsbakk
<kristofferhaugsbakk@fastmail.com> wrote:
>
> On Tue, Sep 29, 2026, at 09:30, Harald Nordgren via GitGitGadget wrote:
> > From: Harald Nordgren <haraldnordgren@gmail.com>
> >
> > Branches merged on GitHub with "Squash and merge" or "Rebase and
> > merge" are never deleted by "git branch --delete-merged". The upstream
> > holds a rewritten copy of their work, so their tips are not reachable
> > from it and they look unmerged forever.
>
> An example closer to git(1)’s home:
>
>     git merge --squash
>     git commit

True. But likely it opens up the question of _why_ would anyone on
upstream be doing such destructive actions? Well, then the answer is
of course that millions of users (including) me do that via GitHub all
the time.

Maybe I should include both examples in my text.


Harald

@gitgitgadget-git

Copy link
Copy Markdown

"Kristoffer Haugsbakk" wrote on the Git mailing list (how to reply to this email):

On Tue, Sep 29, 2026, at 09:52, Harald Nordgren wrote:
> On Tue, Sep 29, 2026 at 9:47 AM Kristoffer Haugsbakk
> <kristofferhaugsbakk@fastmail.com> wrote:
>>
>> On Tue, Sep 29, 2026, at 09:30, Harald Nordgren via GitGitGadget wrote:
>> > From: Harald Nordgren <haraldnordgren@gmail.com>
>> >
>> > Branches merged on GitHub with "Squash and merge" or "Rebase and
>> > merge" are never deleted by "git branch --delete-merged". The upstream
>> > holds a rewritten copy of their work, so their tips are not reachable
>> > from it and they look unmerged forever.
>>
>> An example closer to git(1)’s home:
>>
>>     git merge --squash
>>     git commit
>
> True. But likely it opens up the question of _why_ would anyone on
> upstream be doing such destructive actions? Well, then the answer is
> of course that millions of users (including) me do that via GitHub all
> the time.
>
> Maybe I should include both examples in my text.

My *guess* is that `git merge --squash` inspired the forge squashes.
But the forges popularized it.

The apparent `git merge --squash` approach of using `git log` for
concatenating the commit messages isn’t that nice in my opinion. So I
wonder how much it is used.

@gitgitgadget-git

Copy link
Copy Markdown

"D. Ben Knoble" wrote on the Git mailing list (how to reply to this email):

On Tue, Sep 29, 2026 at 4:17 AM Kristoffer Haugsbakk
<kristofferhaugsbakk@fastmail.com> wrote:
>
> On Tue, Sep 29, 2026, at 09:52, Harald Nordgren wrote:
> > On Tue, Sep 29, 2026 at 9:47 AM Kristoffer Haugsbakk
> > <kristofferhaugsbakk@fastmail.com> wrote:
> >>
> >> On Tue, Sep 29, 2026, at 09:30, Harald Nordgren via GitGitGadget wrote:
> >> > From: Harald Nordgren <haraldnordgren@gmail.com>
> >> >
> >> > Branches merged on GitHub with "Squash and merge" or "Rebase and
> >> > merge" are never deleted by "git branch --delete-merged". The upstream
> >> > holds a rewritten copy of their work, so their tips are not reachable
> >> > from it and they look unmerged forever.
> >>
> >> An example closer to git(1)’s home:
> >>
> >>     git merge --squash
> >>     git commit
> >
> > True. But likely it opens up the question of _why_ would anyone on
> > upstream be doing such destructive actions? Well, then the answer is
> > of course that millions of users (including) me do that via GitHub all
> > the time.
> >
> > Maybe I should include both examples in my text.
>
> My *guess* is that `git merge --squash` inspired the forge squashes.
> But the forges popularized it.
>
> The apparent `git merge --squash` approach of using `git log` for
> concatenating the commit messages isn’t that nice in my opinion. So I
> wonder how much it is used.

The same behavior is present in GitHub's default squash merge message,
and almost no one I work with bothers to edit it.

It's really sad to lose the opportunity to have good commit messages
when using squash-and-merge on a forge---not because we *cannot*, but
because the defaults do not *encourage* it (and we all know how
defaults affect user behavior!).

Anyway, see https://benknoble.github.io/blog/2024/08/02/github-squash/
for a distillation of my thoughts from working around folks that
squash carelessly.

So, anecdotally: it is used widely due to defaults. Blech.

-- 
D. Ben Knoble

@gitgitgadget-git

Copy link
Copy Markdown

User "D. Ben Knoble" <ben.knoble@gmail.com> has been added to the cc: list.

@gitgitgadget-git

Copy link
Copy Markdown

"D. Ben Knoble" wrote on the Git mailing list (how to reply to this email):

Without looking too much further…

On Tue, Sep 29, 2026 at 3:33 AM Harald Nordgren via GitGitGadget
<gitgitgadget@gmail.com> wrote:
>
> From: Harald Nordgren <haraldnordgren@gmail.com>
>
> Branches merged on GitHub with "Squash and merge" or "Rebase and
> merge" are never deleted by "git branch --delete-merged". The upstream
> holds a rewritten copy of their work, so their tips are not reachable
> from it and they look unmerged forever.
>
> Treat such a branch as merged when some upstream commit since the fork
> point contains all of its changes, so that merging the branch into
> that commit would change nothing. Name that commit in the output so
> the user can see where the work went:
>
>     Deleted branch topic (was 1a2b3c4, landed as 9f8e7d6).
>
> The first upstream commit that contains the changes is used, so the
> branch is deleted even if upstream later reverted or reworked them.
> Nothing is lost, since that commit keeps them in the upstream history.
> A branch whose changes only partly landed is kept.
>
> Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>

…in the rebase case, I would expect something like git-log's
--cherry-mark option (or really the algorithm behind it, git-cherry,
and git-range-diff) to be useful for identifying rebased branches. But
of course even rebase-merged branches can end up with minor
differences (say, a commit was made upstream before that branch was
rebased with an identical change; no conflict occurs, but the new
commit differs from the old by not having that change).

In the squash case, I suppose the best we can do is check that all our
changes were applied at some point between the merge-base and the tip.
There probably won't be any tree-same commits, though maybe a
(premature?) optimization can return early if the trees match exactly.

It looked like you don't distinguish the 2 cases in the code, and I
think that's reasonable: we wouldn't know a priori whether to check
for a rebased series or a squashed commit, so we'd have to run both
checks, and the latter presumably subsumes the former.

Anyway, I can see how this would all be fairly expensive---on one repo
I work in, git-range-diff can be somewhat slow depending on how many
commits are in the range, I think. I don't know if it's worth trying
to state that for folks, though? If we ever make improvements to
performance, we'd have to remember to remove the "this may be slow"
text.

> After the release of 2.56, I saw people liking the --delete-merged
>    feature, but asking for this. A lot of people, me included prefer
>    squash-merge and it currently doesn't work with --delete-merged.

Btw, I wonder if you can share where you saw this? 2.56 was released
so recently I'm (pleasantly) surprised there's already feedback on
this!

-- 
D. Ben Knoble

@gitgitgadget-git

Copy link
Copy Markdown

"Kristoffer Haugsbakk" wrote on the Git mailing list (how to reply to this email):

On Tue, Sep 29, 2026, at 13:18, D. Ben Knoble wrote:
> On Tue, Sep 29, 2026 at 4:17 AM Kristoffer Haugsbakk
> <kristofferhaugsbakk@fastmail.com> wrote:
>>
>> On Tue, Sep 29, 2026, at 09:52, Harald Nordgren wrote:
>> > [snip]
>> >
>> > Maybe I should include both examples in my text.
>>
>> My *guess* is that `git merge --squash` inspired the forge squashes.
>> But the forges popularized it.
>>
>> The apparent `git merge --squash` approach of using `git log` for
>> concatenating the commit messages isn’t that nice in my opinion. So I
>> wonder how much it is used.
>
> The same behavior is present in GitHub's default squash merge message,
> and almost no one I work with bothers to edit it.

The asterisk bullet points on GitHub are better than `git merge
--squash`:

    Squashed commit of the following:

    [just `git log` of the commits in the range]

> It's really sad to lose the opportunity to have good commit messages
> when using squash-and-merge on a forge---not because we *cannot*, but
> because the defaults do not *encourage* it (and we all know how
> defaults affect user behavior!).

In my opinion squash merges cannot be implemented in a good way, in a
way that leads to good commits. Fundamentally not. It’s the button to
both squash “oops” and the incremental, valuable commits, resulting in a
blob where even a manually written commit message cannot document all
the changes properly. And the reasons why are laid out in your article,
I think...

>
> Anyway, see https://benknoble.github.io/blog/2024/08/02/github-squash/
> for a distillation of my thoughts from working around folks that
> squash carelessly.
>
> So, anecdotally: it is used widely due to defaults. Blech.

Which is excellent. Thanks for writing it.

@gitgitgadget-git

Copy link
Copy Markdown

Harald Nordgren wrote on the Git mailing list (how to reply to this email):

> > After the release of 2.56, I saw people liking the --delete-merged
> >    feature, but asking for this. A lot of people, me included prefer
> >    squash-merge and it currently doesn't work with --delete-merged.
>
> Btw, I wonder if you can share where you saw this? 2.56 was released
> so recently I'm (pleasantly) surprised there's already feedback on
> this!

Reddit thread: https://www.reddit.com/r/git/comments/1wsnrl9/comment/pcncf5t

There was only one person asking to clean up squashed branches. But I
also started thinking about it the other day, when 2.56 drew closer,
and I realized that friends that work in companies using squash merge
won't get any benefit from this.

Blog posts that is drawing attention to this new feature (not
necessary feedbacking on it):

- https://github.blog/open-source/git/highlights-from-git-2-56/
- https://about.gitlab.com/blog/whats-new-in-git-2-56-0/
- https://9to5linux.com/git-2-56-adds-new-options-for-cleaning-up-branches-and-resolving-conflicts
- https://linuxiac.com/git-2-56-released-with-safer-conflict-resolution-and-performance-gains/

Btw, I love squash merge!


Harald

@gitgitgadget-git

Copy link
Copy Markdown

Harald Nordgren wrote on the Git mailing list (how to reply to this email):

> Anyway, I can see how this would all be fairly expensive---on one repo
> I work in, git-range-diff can be somewhat slow depending on how many
> commits are in the range, I think. I don't know if it's worth trying
> to state that for folks, though? If we ever make improvements to
> performance, we'd have to remember to remove the "this may be slow"
> text.

It was much slower in my first iterations, so running this on my local
Git repo now does not feel painfully slow. Although slower than
without this feature.

We might hide it behind a feature flag?


Harald

@gitgitgadget-git

Copy link
Copy Markdown

"D. Ben Knoble" wrote on the Git mailing list (how to reply to this email):

On Tue, Sep 29, 2026 at 9:53 AM Harald Nordgren
<haraldnordgren@gmail.com> wrote:
>
> > Anyway, I can see how this would all be fairly expensive---on one repo
> > I work in, git-range-diff can be somewhat slow depending on how many
> > commits are in the range, I think. I don't know if it's worth trying
> > to state that for folks, though? If we ever make improvements to
> > performance, we'd have to remember to remove the "this may be slow"
> > text.
>
> It was much slower in my first iterations, so running this on my local
> Git repo now does not feel painfully slow. Although slower than
> without this feature.
>
> We might hide it behind a feature flag?

I could imagine wanting a CSV-style value for the option, like
"--delete-merged=squashed,rebased" vs. "--delete-merged=merged"
(current default), or something. But I'd have to think about whether
I'm suggesting that only to work around performance or to also support
actual use cases. Mostly I think people just want to go "gah, delete
merged branches" and not think about it further… hm.

-- 
D. Ben Knoble

@gitgitgadget-git

Copy link
Copy Markdown

Harald Nordgren wrote on the Git mailing list (how to reply to this email):

> > We might hide it behind a feature flag?
>
> I could imagine wanting a CSV-style value for the option, like
> "--delete-merged=squashed,rebased" vs. "--delete-merged=merged"
> (current default), or something. But I'd have to think about whether
> I'm suggesting that only to work around performance or to also support
> actual use cases. Mostly I think people just want to go "gah, delete
> merged branches" and not think about it further… hm.

For me, the only reason to hide this would be for performance.

Yes, I think most people (me included) would rather have the simplest
possible command. I'd rather not have to specify the pattern so that
these are to the same:

    git branch --delete-merged
    git branch --delete-merged '*/*'


Harald

@gitgitgadget-git

Copy link
Copy Markdown

Phillip Wood wrote on the Git mailing list (how to reply to this email):

On 29/09/2026 12:26, D. Ben Knoble wrote:
> On Tue, Sep 29, 2026 at 3:33 AM Harald Nordgren via GitGitGadget
> <gitgitgadget@gmail.com> wrote:
> > …in the rebase case, I would expect something like git-log's
> --cherry-mark option (or really the algorithm behind it, git-cherry,
> and git-range-diff) That's what I was expecting as well. It would be worth carefully studying the implementation of git-cherry. "git cherry A...B" precalculates the patch-ids from the side of the merge base that has the fewest commits and then walks the other side to compare them. While it is walking the other side I think it also looks at which paths were changed to avoid calculating the patch-id for commits that cannot match. It also batches fetches the blobs it needs in partial clones.

As far as I can see the implementation here makes a separate upstream revision walk for each branch, and recalculates the upstream diffs each time which seems less efficient than it could be.
> to be useful for identifying rebased branches. But
> of course even rebase-merged branches can end up with minor
> differences (say, a commit was made upstream before that branch was
> rebased with an identical change; no conflict occurs, but the new
> commit differs from the old by not having that change).

Yes if a branch has been rebased before it is merged it may be altered such that we cannot detect it.
> In the squash case, I suppose the best we can do is check that all our
> changes were applied at some point between the merge-base and the tip.
> There probably won't be any tree-same commits, though maybe a
> (premature?) optimization can return early if the trees match exactly.

If we have

(topic)  D - C - B - A
                      \
 (main)    M - Q - P - O -
            \          /
              - - S - -

where M is a squashed merge of topic I think we have

    M^2^{tree} == topic^{tree}
    Merge-base(M^1, M^2) == Merge-base(topic, topic@{upstream})
    $(git rev-list --count --right-only M^1...M^2) == 1

If you know your repository only has squash merges that were not rebased it would be a lot more efficient to just look at the trees and merge-bases, especially in a blobless clone. Having an option to turn off the patch-id based detection would probably be useful in that case.

I think detecting branches that have been squashed and/or rebased is a useful improvement, but it needs careful implementation to be efficient enough that it is practical in large repositories and I'm unlikely to have time to closely review it.

Thanks

Phillip

> It looked like you don't distinguish the 2 cases in the code, and I
> think that's reasonable: we wouldn't know a priori whether to check
> for a rebased series or a squashed commit, so we'd have to run both
> checks, and the latter presumably subsumes the former.
> > Anyway, I can see how this would all be fairly expensive---on one repo
> I work in, git-range-diff can be somewhat slow depending on how many
> commits are in the range, I think. I don't know if it's worth trying
> to state that for folks, though? If we ever make improvements to
> performance, we'd have to remember to remove the "this may be slow"
> text.
> >> After the release of 2.56, I saw people liking the --delete-merged
>>     feature, but asking for this. A lot of people, me included prefer
>>     squash-merge and it currently doesn't work with --delete-merged.
> > Btw, I wonder if you can share where you saw this? 2.56 was released
> so recently I'm (pleasantly) surprised there's already feedback on
> this!
> 

@gitgitgadget-git

Copy link
Copy Markdown

User Phillip Wood <phillip.wood123@gmail.com> has been added to the cc: list.

@HaraldNordgren
HaraldNordgren force-pushed the branch-delete-squashed branch from 5698216 to 8839e59 Compare October 4, 2026 15:05
@gitgitgadget-git

Copy link
Copy Markdown

Harald Nordgren wrote on the Git mailing list (how to reply to this email):

> As far as I can see the implementation here makes a separate upstream
> revision walk for each branch, and recalculates the upstream diffs each
> time which seems less efficient than it could be.

Yes, that can be improved!

> > In the squash case, I suppose the best we can do is check that all our
> > changes were applied at some point between the merge-base and the tip.
> > There probably won't be any tree-same commits, though maybe a
> > (premature?) optimization can return early if the trees match exactly.
...
> If you know your repository only has squash merges that were not rebased
> it would be a lot more efficient to just look at the trees and
> merge-bases, especially in a blobless clone. Having an option to turn
> off the patch-id based detection would probably be useful in that case.

Maybe yes, but for users I imagine they want the interface to be as
simple as possible.

I'm iterating on the code on my side (sharing logic between branches,
etc) and it became fast on my local Git repo. If there are no
performance concerns then would we still want the option to turn it
off?


Harald

@gitgitgadget-git

Copy link
Copy Markdown

Phillip Wood wrote on the Git mailing list (how to reply to this email):

Hi Harald

On 04/10/2026 23:28, Harald Nordgren wrote:
> ...
>> If you know your repository only has squash merges that were not rebased
>> it would be a lot more efficient to just look at the trees and
>> merge-bases, especially in a blobless clone. Having an option to turn
>> off the patch-id based detection would probably be useful in that case.
> > Maybe yes, but for users I imagine they want the interface to be as
> simple as possible.

Agreed, they also don't want to wait to find squashed merges they know they do not have.

> I'm iterating on the code on my side (sharing logic between branches,
> etc) and it became fast on my local Git repo. If there are no
> performance concerns then would we still want the option to turn it
> off?

Even an efficient implementation is going to be a lot slower when it is trying to find branches that have been squashed, so I think we probably do want a way to turn it off. That's especially true in partial clones where we'll have to download a bunch of blobs to do the squash detection. So long as it isn't diabolically slow enabling it by default is probably fine.

Thanks

Phillip

@gitgitgadget-git

Copy link
Copy Markdown

"D. Ben Knoble" wrote on the Git mailing list (how to reply to this email):

On Sun, Oct 4, 2026 at 5:54 AM Phillip Wood <phillip.wood123@gmail.com> wrote:
>
> On 29/09/2026 12:26, D. Ben Knoble wrote:
> > On Tue, Sep 29, 2026 at 3:33 AM Harald Nordgren via GitGitGadget
> > <gitgitgadget@gmail.com> wrote:
> >
> > …in the rebase case, I would expect something like git-log's
> > --cherry-mark option (or really the algorithm behind it, git-cherry,
> > and git-range-diff)
>
> That's what I was expecting as well. It would be worth carefully
> studying the implementation of git-cherry. "git cherry A...B"
> precalculates the patch-ids from the side of the merge base that has the
> fewest commits and then walks the other side to compare them. While it
> is walking the other side I think it also looks at which paths were
> changed to avoid calculating the patch-id for commits that cannot match.
> It also batches fetches the blobs it needs in partial clones.
>
> As far as I can see the implementation here makes a separate upstream
> revision walk for each branch, and recalculates the upstream diffs each
> time which seems less efficient than it could be.

Thanks for spelling that out!

> > to be useful for identifying rebased branches. But
> > of course even rebase-merged branches can end up with minor
> > differences (say, a commit was made upstream before that branch was
> > rebased with an identical change; no conflict occurs, but the new
> > commit differs from the old by not having that change).
>
> Yes if a branch has been rebased before it is merged it may be altered
> such that we cannot detect it.

Yep. I'm not sure what Harald (or we) would want to do here.
git-range-diff has trouble detecting these scenarios today, so maybe
matching that and later finding a way to improve is ok.

> > In the squash case, I suppose the best we can do is check that all our
> > changes were applied at some point between the merge-base and the tip.
> > There probably won't be any tree-same commits, though maybe a
> > (premature?) optimization can return early if the trees match exactly.
>
> If we have
>
> (topic)  D - C - B - A
>                        \
>   (main)    M - Q - P - O -
>              \          /
>                - - S - -
>
> where M is a squashed merge of topic I think we have
>
>      M^2^{tree} == topic^{tree}
>      Merge-base(M^1, M^2) == Merge-base(topic, topic@{upstream})
>      $(git rev-list --count --right-only M^1...M^2) == 1

Perhaps we are thinking of 2 different things? In practice when I see
a merge created using GitHub's squash and merge option (and, I think,
also when using `merge --squash`), there is no second parent. You
could instead just get

    (main) S - Q - P - O

where S is A+B+C+D applied to Q (i.e., closer to a cherry-pick with
--no-commit).

And we know that S is not necessarily tree-same to topic's D, so…?

-- 
D. Ben Knoble

@gitgitgadget-git

Copy link
Copy Markdown

Harald Nordgren wrote on the Git mailing list (how to reply to this email):

> Even an efficient implementation is going to be a lot slower when it is
> trying to find branches that have been squashed, so I think we probably
> do want a way to turn it off. That's especially true in partial clones
> where we'll have to download a bunch of blobs to do the squash
> detection. So long as it isn't diabolically slow enabling it by default
> is probably fine.

A bit slower (depends on how much!) could be worth it for improved
usability. This is not a command that users will run multiple times a
day.

I would hope we could have it on by default and add a flag to turn it
off instead.

For partial or shallow clones, we should probably warn the user that
detection won't be able to search the full history, and offer them to
unshallow, etc. It's very reasonable that some parts are turned off,
when the user doesn't want to hold the complete repo.



Harald

@gitgitgadget-git

Copy link
Copy Markdown

"Kristoffer Haugsbakk" wrote on the Git mailing list (how to reply to this email):

On Thu, Oct 8, 2026, at 20:42, Harald Nordgren wrote:
>> Even an efficient implementation is going to be a lot slower when it is
>> trying to find branches that have been squashed, so I think we probably
>> do want a way to turn it off. That's especially true in partial clones
>> where we'll have to download a bunch of blobs to do the squash
>> detection. So long as it isn't diabolically slow enabling it by default
>> is probably fine.
>
> A bit slower (depends on how much!) could be worth it for improved
> usability. This is not a command that users will run multiple times a
> day.

I would really like a solid delete operation that I can run, say,
overnight. I have better hygiene practices today but I didn’t back then,
so things have piled up.

One big cleanup is worth a nightly run. So I don’t care about how slow
it is.

>[snip]

@gitgitgadget-git

Copy link
Copy Markdown

Harald Nordgren wrote on the Git mailing list (how to reply to this email):

On Fri, Oct 9, 2026 at 10:16 AM Kristoffer Haugsbakk
<kristofferhaugsbakk@fastmail.com> wrote:
>
> On Thu, Oct 8, 2026, at 20:42, Harald Nordgren wrote:
> >> Even an efficient implementation is going to be a lot slower when it is
> >> trying to find branches that have been squashed, so I think we probably
> >> do want a way to turn it off. That's especially true in partial clones
> >> where we'll have to download a bunch of blobs to do the squash
> >> detection. So long as it isn't diabolically slow enabling it by default
> >> is probably fine.
> >
> > A bit slower (depends on how much!) could be worth it for improved
> > usability. This is not a command that users will run multiple times a
> > day.
>
> I would really like a solid delete operation that I can run, say,
> overnight. I have better hygiene practices today but I didn’t back then,
> so things have piled up.
>
> One big cleanup is worth a nightly run. So I don’t care about how slow
> it is.

I would love to have a `full-clean` command that bundles
`deleted-merged` and all `prune` commands, and uses sensible defaults
for each one. (I have a hard time believing that it would need to run
overnight anyway).

I have known people who just delete the whole repo on any signs of
trouble. People out there are so scared of Git, so let's give them
sensible defaults to work with.


Harald

@gitgitgadget-git

Copy link
Copy Markdown

Harald Nordgren wrote on the Git mailing list (how to reply to this email):

Interesting to note is that merged branches inside our repo change
when Junio signs off on them. So without this functionality they are
not cleaned either.


Harald

@gitgitgadget-git

Copy link
Copy Markdown

Phillip Wood wrote on the Git mailing list (how to reply to this email):

Hi Ben

On 08/10/2026 18:51, D. Ben Knoble wrote:
> On Sun, Oct 4, 2026 at 5:54 AM Phillip Wood <phillip.wood123@gmail.com> wrote:
>>
>> If we have
>>
>> (topic)  D - C - B - A
>>                         \
>>    (main)    M - Q - P - O -
>>               \          /
>>                 - - S - -
>>
>> where M is a squashed merge of topic I think we have
>>
>>       M^2^{tree} == topic^{tree}
>>       Merge-base(M^1, M^2) == Merge-base(topic, topic@{upstream})
>>       $(git rev-list --count --right-only M^1...M^2) == 1
> > Perhaps we are thinking of 2 different things? In practice when I see
> a merge created using GitHub's squash and merge option (and, I think,
> also when using `merge --squash`), there is no second parent. You
> could instead just get
> >      (main) S - Q - P - O
> > where S is A+B+C+D applied to Q (i.e., closer to a cherry-pick with
> --no-commit).

Oh, you're right, I'd misremembered how "merge --squash" worked.

> And we know that S is not necessarily tree-same to topic's D, so…?

I agree we need to use patch ids rather than trees in that case.

Thanks

Phillip

@HaraldNordgren
HaraldNordgren force-pushed the branch-delete-squashed branch from 8839e59 to ec88bde Compare October 9, 2026 14:41
@gitgitgadget-git

Copy link
Copy Markdown

Phillip Wood wrote on the Git mailing list (how to reply to this email):

Hi Harald

On 08/10/2026 19:42, Harald Nordgren wrote:
>> Even an efficient implementation is going to be a lot slower when it is
>> trying to find branches that have been squashed, so I think we probably
>> do want a way to turn it off. That's especially true in partial clones
>> where we'll have to download a bunch of blobs to do the squash
>> detection. So long as it isn't diabolically slow enabling it by default
>> is probably fine.
> > A bit slower (depends on how much!) could be worth it for improved
> usability. This is not a command that users will run multiple times a
> day.
> > I would hope we could have it on by default and add a flag to turn it
> off instead.

That's my hope too

> For partial or shallow clones, we should probably warn the user that
> detection won't be able to search the full history, and offer them to
> unshallow, etc. It's very reasonable that some parts are turned off,
> when the user doesn't want to hold the complete repo.
I've not really though much about shallow clones, but for partial clones I think it is perfectly reasonable to go and fetch the blobs we need to calculate the patch ids and warn in the documentation that it can be slow.

Thanks

Phillip

@gitgitgadget-git

Copy link
Copy Markdown

Phillip Wood wrote on the Git mailing list (how to reply to this email):

Hi Harald

On 09/10/2026 12:18, Harald Nordgren wrote:
> Interesting to note is that merged branches inside our repo change
> when Junio signs off on them. So without this functionality they are
> not cleaned either.
That's true for a patched based workflow like the one on this list whether or not the maintainer signs off the commits. It is slightly different from the squash merge case because you need to look for the individual commits upstream, rather than the combined changes from the branch.

Thanks

Phillip

@gitgitgadget-git

Copy link
Copy Markdown

Harald Nordgren wrote on the Git mailing list (how to reply to this email):

> I've not really though much about shallow clones, but for partial clones
> I think it is perfectly reasonable to go and fetch the blobs we need to
> calculate the patch ids and warn in the documentation that it can be slow.

Would you say the blobless case should be handled now, or can be left
as a follow-up topic?


Harald

@HaraldNordgren

Copy link
Copy Markdown
Contributor Author

/submit

@gitgitgadget-git

Copy link
Copy Markdown

Submitted as pull.2425.v2.git.git.1791711665.gitgitgadget@gmail.com

To fetch this version into FETCH_HEAD:

git fetch https://github.com/gitgitgadget/git/ pr-git-2425/HaraldNordgren/branch-delete-squashed-v2

To fetch this version to local tag pr-git-2425/HaraldNordgren/branch-delete-squashed-v2:

git fetch --no-tags https://github.com/gitgitgadget/git/ tag pr-git-2425/HaraldNordgren/branch-delete-squashed-v2

@gitgitgadget-git

Copy link
Copy Markdown

Junio C Hamano wrote on the Git mailing list (how to reply to this email):

"Harald Nordgren via GitGitGadget" <gitgitgadget@gmail.com> writes:

> Branches merged on GitHub with "Squash and merge" or "Rebase and merge" are
> never deleted by git branch --delete-merged, because their tips are not
> reachable from the upstream. This treats such a branch as merged when some
> upstream commit contains all of its changes, and names that commit in the
> output:
>
> Deleted branch topic (was 1a2b3c4, landed as 9f8e7d6).

Interesting.

After rebasing, you probably can (re)use the same algorithm as used
in range-diff to match the commits and their rewritten counterparts,
but I wonder how "squash" would go.  Presumably you can locally make
a trial squash merge and probe for a similar commit on the upstream
commit.

On the other hand, unlike deliberate rebasing done during a patch
iteration, rebase done on the upstream to integrate your changes
would not rewrite the log messages, so matching the commits based on
the log message may work better than a range-diff between iteration
vN and v(N+1) in the submitted patches.

>  * Diff each upstream commit once and share the result between all branches,
>    faster than v1.
>  * Fix a branch merged with a merge commit being reported as "landed as" the
>    merge when another branch was searched first, and add a test for it. Show
>    the topic commit itself in "landed as" for work that arrived through a
>    merge.
>  * Use git merge --squash as the example in the commit message and mention
>    GitHub's squash and rebase buttons.
>  * Add --no-squashed to skip the squash search.
>
> Harald Nordgren (2):
>   branch: let --delete-merged find squash merged branches
>   branch: add --no-squashed to skip the squash search

This a bit unexpected two-patch organization; I would have expected
that the cases where our side and upstream side should correspond
one-to-one (i.e., what you call rebase-and-merge) and other cases
(i.e., squash merge) to require very different matching algorithm,
which would lead to two-patch organization where one adds support
for rebase and the other adds support for squash.

@gitgitgadget-git

Copy link
Copy Markdown

Phillip Wood wrote on the Git mailing list (how to reply to this email):

On 12/10/2026 01:30, Junio C Hamano wrote:
> "Harald Nordgren via GitGitGadget" <gitgitgadget@gmail.com> writes:
> >> Branches merged on GitHub with "Squash and merge" or "Rebase and merge" are
>> never deleted by git branch --delete-merged, because their tips are not
>> reachable from the upstream. This treats such a branch as merged when some
>> upstream commit contains all of its changes, and names that commit in the
>> output:
>>
>> Deleted branch topic (was 1a2b3c4, landed as 9f8e7d6).
> > Interesting.
> > After rebasing, you probably can (re)use the same algorithm as used
> in range-diff to match the commits and their rewritten counterparts,
> but I wonder how "squash" would go.  Presumably you can locally make
> a trial squash merge and probe for a similar commit on the upstream
> commit.
> > On the other hand, unlike deliberate rebasing done during a patch
> iteration, rebase done on the upstream to integrate your changes
> would not rewrite the log messages, so matching the commits based on
> the log message may work better than a range-diff between iteration
> vN and v(N+1) in the submitted patches.

I think it would be helpful if the cover letter explained in more detail what this series actually does to detect squashed merges and compared that to other approaches such as using patch ids or test merges. As far as I can see the approach here is, to a first approximation, checking

	$(git merge-tree B C) == C^{tree}

to see if C is a squashed merge of B, only without using merge-ort but by calling merge_ll() on each path instead. That means we're not checking if C is the same as merging B into C^, C may contain additional changes as well. I think it also means we're missing out on rename detection. It would be good to get a clear idea of what the tradeoffs of the various approaches are before any more code is written.

Thanks

Phillip

>>   * Diff each upstream commit once and share the result between all branches,
>>     faster than v1.
>>   * Fix a branch merged with a merge commit being reported as "landed as" the
>>     merge when another branch was searched first, and add a test for it. Show
>>     the topic commit itself in "landed as" for work that arrived through a
>>     merge.
>>   * Use git merge --squash as the example in the commit message and mention
>>     GitHub's squash and rebase buttons.
>>   * Add --no-squashed to skip the squash search.
>>
>> Harald Nordgren (2):
>>    branch: let --delete-merged find squash merged branches
>>    branch: add --no-squashed to skip the squash search
> > This a bit unexpected two-patch organization; I would have expected
> that the cases where our side and upstream side should correspond
> one-to-one (i.e., what you call rebase-and-merge) and other cases
> (i.e., squash merge) to require very different matching algorithm,
> which would lead to two-patch organization where one adds support
> for rebase and the other adds support for squash.

@gitgitgadget-git

Copy link
Copy Markdown

Harald Nordgren wrote on the Git mailing list (how to reply to this email):

> After rebasing, you probably can (re)use the same algorithm as used
> in range-diff to match the commits and their rewritten counterparts,
> but I wonder how "squash" would go.  Presumably you can locally make
> a trial squash merge and probe for a similar commit on the upstream
> commit.
>
> On the other hand, unlike deliberate rebasing done during a patch
> iteration, rebase done on the upstream to integrate your changes
> would not rewrite the log messages, so matching the commits based on
> the log message may work better than a range-diff between iteration
> vN and v(N+1) in the submitted patches.

Matching on commit messages sounds very brittle to me (GitHub mangles
them on squash), and also unnecessary when we can compare the files
themselves.

> > Harald Nordgren (2):
> >   branch: let --delete-merged find squash merged branches
> >   branch: add --no-squashed to skip the squash search
>
> This a bit unexpected two-patch organization; I would have expected
> that the cases where our side and upstream side should correspond
> one-to-one (i.e., what you call rebase-and-merge) and other cases
> (i.e., squash merge) to require very different matching algorithm,
> which would lead to two-patch organization where one adds support
> for rebase and the other adds support for squash.

Finding squashes is a superset of finding rebase merges. Although
slower, which is why we need to be able to turn it off. So yes, then
we need separate the code paths for squash and merge finding.

Then I'm thinking 3 commits:

    branch: let --delete-merged find rebase merged branches
    branch: let --delete-merged find squash merged branches
    branch: add --no-squashed to skip the squash merge check


Harald

@gitgitgadget-git

Copy link
Copy Markdown

Harald Nordgren wrote on the Git mailing list (how to reply to this email):

> I think it would be helpful if the cover letter explained in more detail
> what this series actually does to detect squashed merges and compared
> that to other approaches such as using patch ids or test merges.

Ok.

> As far
> as I can see the approach here is, to a first approximation, checking
>
>         $(git merge-tree B C) == C^{tree}
>
> to see if C is a squashed merge of B, only without using merge-ort but
> by calling merge_ll() on each path instead. That means we're not
> checking if C is the same as merging B into C^, C may contain additional
> changes as well. I think it also means we're missing out on rename
> detection.

The check is "does C already contain everything B changed", not "is C
exactly the squash of B". For deleting the branch it could be enough
since nothing is lost.

But it means "landed as" can point to a commit with other changes in
it as well, maybe that's not good.

I think following renames is a bad basis for assuming merged.
Sometimes the goal of a topic is a rename, and then it should not be
dropped.


Harald

@gitgitgadget-git

Copy link
Copy Markdown

"D. Ben Knoble" wrote on the Git mailing list (how to reply to this email):

On Mon, Oct 12, 2026 at 8:42 AM Harald Nordgren
<haraldnordgren@gmail.com> wrote:
>
> > After rebasing, you probably can (re)use the same algorithm as used
> > in range-diff to match the commits and their rewritten counterparts,
> > but I wonder how "squash" would go.  Presumably you can locally make
> > a trial squash merge and probe for a similar commit on the upstream
> > commit.
> >
> > On the other hand, unlike deliberate rebasing done during a patch
> > iteration, rebase done on the upstream to integrate your changes
> > would not rewrite the log messages, so matching the commits based on
> > the log message may work better than a range-diff between iteration
> > vN and v(N+1) in the submitted patches.
>
> Matching on commit messages sounds very brittle to me (GitHub mangles
> them on squash), and also unnecessary when we can compare the files
> themselves.

Well, I think Junio was talking about the rebase cases for log
messages, but I have been known to cherry-pick and edit the log
message of small contributions that aren't up to my personal standards
to help contributors land their changes, in which cases you *would*
care more about patch ID than log message.

-- 
D. Ben Knoble

@gitgitgadget-git

Copy link
Copy Markdown

Junio C Hamano wrote on the Git mailing list (how to reply to this email):

Phillip Wood <phillip.wood123@gmail.com> writes:

> detection. It would be good to get a clear idea of what the tradeoffs of 
> the various approaches are before any more code is written.

It sounds like a very sensible approach.

At least the one-to-one part of the operation should learn from what
Dscho did in range-diff, especially the part that does take
similarity of the log messages and authorship information, instead
of paying attention only to patch IDs.  I am not convinced that
one-to-one rebase implemented as a subset of squash merge would
perform sufficiently well, either.  From the user interface angle,
it also is not clear to me if a single --delete-merged that enables
all variants by default with individual knob to optionally disable
one variant (squash) would be a good extensible one, when we may
need to deal with the third (and other) kinds of ways to integrate
your work without preserving the commit identity, like selective
cherry-picks.

"git range-diff" works out which commit in one range was rewritten as
which commit in the other before it shows anything. Other commands
want that pairing too, for example to tell whether a local branch was
rebased onto its upstream.

Move the reading and pairing out of show_range_diff() and add
range_diff_match(), which hands the pairs back to the caller.

No change in behavior.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
A branch that upstream integrated with

    git rebase main topic
    git checkout main
    git merge --ff-only topic

is never deleted by "git branch --delete-merged". This is what the
"Rebase and merge" button on GitHub does. The upstream holds rewritten
copies of the branch's commits, so its tip is not reachable from it and
it looks unmerged forever.

Pair the branch's commits with the upstream commits since the fork
point the same way "git range-diff" pairs them, so a commit is still
found when upstream changed lines close to it before the rebase. Treat
the branch as merged when every commit has a counterpart and the
counterpart of the tip contains everything the branch changed, so that
local edits made after the branch was integrated are not lost. Name
that commit in the output so the user can see where the work went:

    Deleted branch topic (was 1a2b3c4, landed as 9f8e7d6).

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
A branch that upstream integrated with

    git merge --squash topic
    git commit

is still kept by "git branch --delete-merged". A single squash commit
is not a counterpart of each of the branch's commits, so it is not
found as a rebase. The "Squash and merge" button on GitHub lands pull
requests this way.

Add --squashed, which searches the upstream since the fork point for
the first commit that contains everything the branch changed, and
names it in the output. A file counts when the commit has the
branch's version of it, or when merging the branch's version into the
commit's leaves it unchanged. The first such commit is used, so the
branch is deleted even if upstream later reverted the changes.
Nothing is lost, since that commit keeps them in the upstream history.

This looks into every upstream commit that touches the branch's files
and may have to read them, so it is slower, and in a partial clone it
may download blobs. It is therefore only done when asked for.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant