In my new company, our Git workflow requires squashing commits before merging them into the main branch. This keeps the commit history clean and concise, making it easier to understand the changes introduced by each feature or bug fix.

However, it also introduces a small inconvenience: I can no longer easily delete completed feature branches with the following command:

git branch --merged | grep -v "\\*" | grep -v "master" | grep -v "main" | xargs -n 1 git branch -d

Instead, I have to use:

git branch -D <branch_name>

to force-delete branches that have been squash-merged. Since git branch -D deletes a branch regardless of whether it has been fully merged, I have to be very careful not to accidentally remove branches that still contain unmerged work.

There are several ways to handle this situation. For example, you can check whether the corresponding remote branch has been deleted. However, none of the existing approaches feels quite right.

To solve this problem, I asked AI for a better solution. The idea is simple: delete a branch if all of its changes already exist in main , regardless of whether they were merged, rebased, cherry-picked, or squash-merged.

[alias]
    cleanup = "!f() { \
        base=${1:-$(git branch --show-current)}; \
        current=$(git branch --show-current); \
        protected=\"dev main master $current\"; \
        \
        confirm_delete() { \
            branch=\"$1\"; reason=\"$2\"; \
            read -p \"Delete branch '$branch' ($reason)? [y/N] \" ans; \
            case \"$ans\" in \
                y|Y) git branch -D \"$branch\"; echo \"Deleted $branch\" ;; \
                *) echo \"Skipped $branch\" ;; \
            esac; \
        }; \
        \
        for branch in $(git for-each-ref --format='%(refname:short)' refs/heads); do \
            case \" $protected \" in *\\" $branch \\"*) continue ;; esac; \
            if git merge-base --is-ancestor \"$branch\" \"$base\"; then \
                git branch -d \"$branch\"; \
                continue; \
            fi; \
            if ! git cherry \"$base\" \"$branch\" | grep -q '^+'; then \
                confirm_delete \"$branch\" \"rebased/cherry-picked\"; \
                continue; \
            fi; \
            mb=$(git merge-base \"$base\" \"$branch\" 2>/dev/null) || continue; \
            tree=$(git rev-parse \"$branch^{tree}\"); \
            squash=$(git commit-tree \"$tree\" -p \"$mb\" -m _); \
            if git cherry \"$base\" \"$squash\" | grep -q '^-'; then \
                confirm_delete \"$branch\" \"squash-merged\"; \
            fi; \
        done; \
    }; f"

It first checks for normal merges, then rebased or cherry-picked commits using git cherry , and finally detects squash merges by comparing synthetic commits.

To use it, add this alias to your ~/.gitconfig file, then run:

git cleanup

The command safely cleans up local branches that have already been merged or whose changes are already present in the current branch, even if they were integrated through a rebase, cherry-pick, or squash merge. This lets me clean up old feature branches with much more confidence, without worrying about accidentally deleting work that has not actually made it into the target branch yet.

I hope you find this useful!

Happy coding!

Inspiration from life

Bo Cheng is a software engineer. He has been writing code since 2004, and is currently living in Chino Hills, CA.

chengbo chengbo1983


Published