Skip to contentSkip to conversation

Detecting squash-merged branches (and question about git-diff-tree)

DISCUSSION Max Gautier 2 participants 5 messages Dec 3, 2024

lore
Share & tools
lore
#0 lore raw

Hi,

I tend to work on project which do a lot of "squash-merging" e.g, merge branches by having a robot squash the branch in a new commit on top of the main branch.

This makes it a bit hard to remove my branches when they are "squash-merged" (in contrast to using git branch --merged)

I started a little script to detect such branches; initially I used git cherry, but this only detect the case where the branch has 1 commit, which is not enough.

Sharing below if anyone is interested and/or want to give some feedback (warning, this is probably full of bash-ism/gnu-ism).

Which leads me to my actual question:
I wanted to use diff-tree in --stdin mode (instead of calling it repeatedly in a loop), feeding it my target branch and
the list of relevant commits, but apparently --merge-base and --stdin are mutually exclusive. What's the reason for that ?
I suppose it's related to the 3 possible line forms diff-tree accepts in --stdin mode, but I didn't find a spelled out explanation in the original thread implementing --merge-base [1].

Is there another alternative for computing the patches ids of branches in that way ? A '%(mergebase)' token for git for-each-ref would also work, but there is no such thing either that I know of.

(Of course, the script as such runs ~reasonably~ well, but it does spend 95% of it's time waiting for subprocess, which bugs me a little^^)

Thanks for reading me !

[1]: https://lore.kernel.org/git/[email protected]/

---

#!/bin/bash
# $1 : target ref (in which we search for squashed branches) # (default: upstream/HEAD)
# ${@:2} (all scripts args after the first one): git for-each-ref

# patterns for refs candidates for squash-merge detection
# (default: refs/{remotes/origin,heads}/}

declare -A commit_by_patch_ids oldest_merge_base=${1-upstream/HEAD} ref_patterns=${@:2} ref_patterns=${ref_patterns:-refs/remotes/origin/ refs/heads/}

for ref in $(git for-each-ref ${ref_patterns} \
                --format='%(objectname)' \
                --no-merged=${1-upstream/HEAD} )
do
  patch_id=( $(git diff-tree -p --merge-base ${1-upstream/HEAD} $ref \
              | git patch-id --stable) )
  commit_by_patch_ids[$patch_id[0]]=$ref
  # Caveat:
  # It's possible for different commit to have the save patch-id
  # (for instance on a feature branch do: git checkout feature;git branch
  # old;git rebase main -> old and feature would probably have the same
  # patch-id, if I understand this correctly)
  # proper treatment of this would need to use array of commits by
  # patch-id, but bash does not support multidimensional arrays.

# Check oldest commit we will need to go back to when checking if a

  # patch-id exist in the source branch.
  # This assumes that branches are not squash-merged before their fork
  # point.  This avoids going back all the way to the first commit,
  # which can be prohibitively expensive on repository with a long
  # history (e.g, linux kernel tree takes 13 minutes on a recent machine
  # for git log -p | git patch-id)
  oldest_merge_base=$(git merge-base $oldest_merge_base $ref)
done

declare -a squashed
# Extract commits whose patch-id exist in the target branch. #

for patch_id in $(git log -p ${oldest_merge_base}..${1-upstream/HEAD} \
                 | git patch-id --stable | cut -d ' ' -f 1)
do
    if [[ -n "${commit_by_patch_ids[$patch_id]+exists}" ]];then
        squashed+=("--points-at=${commit_by_patch_ids[$patch_id]}")
    fi

done

printf "%s\n" "$(git for-each-ref $ref_patterns \
                    --format='%(refname:short)' \
                    ${squashed[@]})"
Signature · 1 line
Max Gautier
#1 lore raw

Hi Max

Quoted 6 lines · Max Gautier

Hi,

I tend to work on project which do a lot of "squash-merging" e.g, merge branches by having a robot squash the branch in a new commit on top of the main branch.

Quoted 7 lines

This makes it a bit hard to remove my branches when they are "squash-merged" (in contrast to using git branch --merged)

I started a little script to detect such branches; initially I used git cherry, but this only detect the case where the branch has 1 commit, which is not enough.

I’m working on a program/script to detect merged/squashed/rebased (or cherry-picked) refs. Is it okay if I contact you if I finish it? ;)

There’s also this program which says it can do the same thing (not tried):

https://github.com/foriequal0/git-trim

My program tries to be like a for-each-ref for merged etc. refs. More focused on reporting first since that can then be used for deletion or whatever else.

#2 lore raw
Quoted 2 lines

I’m working on a program/script to detect merged/squashed/rebased (or cherry-picked) refs. Is it okay if I contact you if I finish it? ;)

Feel free to, yes

Quoted 8 lines

There’s also this program which says it can do the same thing (not tried):

https://github.com/foriequal0/git-trim

My program tries to be like a for-each-ref for merged etc. refs. More focused on reporting first since that can then be used for deletion or whatever else.

Hum, I'm wondering why it didn't find this earlier, I did search though. It apparently use git commit-tree to create a temporary commit, and git cherry on that to check if it's in the target branch.
I wonder if this wouldn't cause problems in case of conflicts though, during the git commit-tree process ?

Signature · 1 line
Max Gautier
#3 lore raw
Quoted 9 lines · Max Gautier
Quoted 3 lines

My program tries to be like a for-each-ref for merged etc. refs. More focused on reporting first since that can then be used for deletion or whatever else.

Hum, I'm wondering why it didn't find this earlier, I did search though. It apparently use git commit-tree to create a temporary commit, and git cherry on that to check if it's in the target branch.
I wonder if this wouldn't cause problems in case of conflicts though, during the git commit-tree process ?

Maybe, I haven’t tried. But using something that creates a commit in the DB seems a bit unnecessary/excessive in any case.

Your diff approach seems more direct.

Signature · 1 line
Kristoffer Haugsbakk
#4 lore raw
In reply to Max Gautier

Uh, btw, there is a typo in the script in the previous email, it should actually be:

commit_by_patch_ids[${patch_id[0]}]=$ref

on line 20 (instead of $patch_id[0])

Not sure how I ended up sending the wrong version.

Signature · 1 line
Max Gautier