In 2022 I wrote about how I use a bash function to delete all merged git branches with a single terminal command. This works great for branches that have been merged, but not squashed. Given the team I'm working on right now like to squash and merge pull requests on GitHub, and also how I like to keep my dev environment clean, it was time to update the clean up function to take squashed branches into account.
Now, this was a total rabbit hole I went down, so bear with me.
The difference between "merge" and "squash and merge"
The default option when merging pull requests on GitHub is merge. When you merge a pull request on GitHub, all commits from a feature branch are added to the base branch in a merge commit. A merge commit preserves the full history of the changes being merged, allowing you to see an intertwining history of events that happened on both branches after a feature branch is merged into the base branch.
On GitHub, a default merge uses the --no-ff option in git, which means no fast-forward. This option is what creates the merge commit and allows you to inspect the complete history of both the feature and base branches.
When you squash and merge a pull request, all commits on the feature branch are "squashed" into a single commit, using the fast-forward option. A fast-forward commit merges all file changes after the most recent change on the base branch. This loses the distinct feature branch timeline of changes. This can be useful when:
- You want to keep your commit history tidy on the base branch
- You're merging small changes on short-lived branches
- You don't need to preserve the feature branch history
Why the original bash script doesn't work for squashed branches
Here's the original bash script I wrote to delete all merged branches. The first line, git checkout main && git branch --merged
, checks out the main branch, and lists all branches that can be detected as being merged into the main branch.
# delete all branches merged into main
dam() {
echo "=== Deleting all merged branches ==="
git checkout main && git branch --merged | grep -vE "(^\*|main)" | xargs git branch -d
echo "☑️ Done!"
}
When you do a squash merge, Git takes all the changes from the feature branch, compresses them into a new commit, and applies it to main as the latest change. The original branch’s commits are not in the history, so Git still sees that branch as “not merged". The git branch --merged
command won't match any branches, and so won't find any branches to delete.
The script that finds and deletes squashed branches
To delete all squashed branches that merged using the fast-forward option we need to take a different approach. This script is named dams(), which stands for "delete all merged squashed". To use the script:
- add the following code to your .bashrc or .zshrc file (switch out main for what you usually call your main branches if necessary)
- save the file
- source the new changes (or reload your terminal)
- run dams in a git project directory (without the brackets)
- enjoy the cleanup!
- (This may need to be adjusted according to your shell, please don't come for me.)
dams() {
echo "=== Deleting merged and squash-merged branches ==="
# Ensure main is checked out and up to date
git checkout main &&
git pull &&
# Loop over all local branches except main
for branch in $(git for-each-ref --format='%(refname:short)' --exclude=refs/heads/main refs/heads/); do
# Skip if branch does not exist on remote
if ! git ls-remote --exit-code --heads origin "$branch" >/dev/null 2>&1; then
echo "Skipping $branch (not on remote)"
continue
fi
# Delete only if content is fully in main
if git diff --quiet "$branch" main; then
git branch -D "$branch"
echo "Deleted $branch"
else
echo "Keeping $branch (has unmerged changes)"
fi
done
echo "☑️ Done!"
}
Let's break down what the script does, and learn some things about Git!
Check out the main branch and update it
First we want to make sure we checkout main and pull in the latest changes to compare the other branches against.
git checkout main && git pull &&
Loop over local branches and check for changes
Here, the script loops over the branch names returned from the command [git for-each-ref](https://git-scm.com/docs/git-for-each-ref)
, which iterates over all branches (or refs) that match a pattern and shows them according to the given format. The pattern excludes the main branch (--exclude
), and the --format
requests the short branch name. refs/heads/
at the end of the line instructs the script to look only at local branches.
for branch in $(git for-each-ref --format='%(refname:short)' --exclude=refs/heads/main refs/heads/); do
Check if a local branch has been pushed to the remote
This following code runs [git ls-remote](https://git-scm.com/docs/git-ls-remote)
to check if the branch in the loop has been pushed to the remote to avoid deleting branches that haven't been pushed. The --exit-code
flag instructs the command to exit with status "2" when no matching refs are found in the remote repository. In Bash, if considers exit code 0 as true and non-zero as false. Exit code 2 will evaluate to false, so with the ! at the start of the if
, we're checking if git ls-remote
returns false. If it does, we skip deleting that branch.
The >/dev/null 2>&1
part instructs the script to throw away both stdout and stderr output so that it produces no output at all, to keep the terminal free of noise. The script still produces a success or failure via the exit code, but suppresses all output.
# Skip if branch does not exist on remote
if ! git ls-remote --exit-code --heads origin "$branch" >/dev/null 2>&1; then
echo "Skipping $branch (not on remote)"
continue
fi
Check for diffs and delete the local branch
This part of the script checks for diffs between the branch in the loop and the main branch. Again, in Bash, if considers exit code 0 as true and non-zero as false, so whilst this may look backwards compared traditional programming languages, if there is no diff found between the branches, then the branch will be deleted using [git branch -D](https://git-scm.com/docs/git-branch#Documentation/git-branch.txt--D)
.
I decided to use the -D
flag to force the delete in this script, seeing as I'm operating on my local machine and it's safe to force delete local branches in my case, especially if we have checked at the start of the script if the local branch has been pushed to the remote.
# Delete only if content is fully in main
if git diff --quiet "$branch" main; then
git branch -D "$branch"
echo "Deleted $branch"
else
echo "Keeping $branch (has unmerged changes)"
fi
It uses the --quiet
flag to suppress all output, as we don't need to view that whilst the script runs. Without the --quiet
flag, here's what the command outputs in isolation when there is a diff:
And here's how the output looks when the full script runs and finds some branches to delete that have been squashed and merged on GitHub:
Probably don't use this script
You probably shouldn't trust random bash scripts written by random people found on the internet, especially where your precious version-controlled work is concerned, so you probably shouldn't use this. This is my disclaimer. Use this script with caution!
Top comments (0)