How to cleanup local branches which tracking branch has been removed

We can use git fetch --prune to remove the remote-tracking branches that no longer exist on the remote. However, the local branches that track on them still exist. This post aims to tell you how to remove them using the git alias git cleanup and explain how it works.

TLDR

Step 1: Create a file git-cleanup under your PATH

Step 2: Paste the following scripts in git-cleanup:

#!/bin/bash
git fetch --prune && git branch -r | awk '{print $1}' | egrep -v -f /dev/fd/0 <(git branch -vv | grep origin) | awk '{print $1}' | xargs git branch -D

Note: The first line(shebang) should be either #!/bin/bash or #!/bin/zsh

Step 3: Make it executable:

chmod +x ./git-cleanup

Step 4: Add git alias:

git config alias.cleanup "!git-cleanup"

Then, you can call git cleanup directly in your command line!

How it works

Let's look at the shell script git-cleanup. Assume that we have the following branches in the local:

main -> origin/main
branch1 -> origin/branch1
branch3

Note: -> means track on

And the branches in the remote

origin/main
origin/branch2

The branch1 is merged and has been removed in the remote. And the branch3 is still under development and hasn't been pushed to remote yet.

According to the assumptions, let's look at how the script works!

git fetch --prune

This command removes the remote-tracking branch in your local.
See the git documentation for more information.

git branch -r

Show all the remote branches. Documentation

awk '{print $1}'

awk is a program that helps you parse your stdin. In this case the stdin is the result of git branch -r:

origin/HEAD -> origin/main
origin/main
origin/branch2

We are interested in the first field of each line. So we use the action 'print $1' to list out all the remote branches. The result will be:

origin/HEAD
origin/main
origin/branch2

egrep -v -f /dev/fd/0 <(git branch -vv | grep origin)

This is the most important part of this script. Let's look at git branch -vv | grep origin first.
git branch -vv shows the local branches with their tracking branches in the following:

main       <SHA> [origin/main] last commit message
branch1    <SHA> [origin/branch1] last commit message
branch3    <SHA> last commit message

Thus, we use grep to filter the lines with origin, which means it has a remote-tracking branch. So the output of git branch -vv | grep origin will be:

main    <SHA> [origin/main] last commit message
branch1 <SHA> [origin/branch1] last commit message

Then, we are going to compare the result of git branch -r and git branch -vv | grep origin to find the local branches which remote-tracking branch has been removed. We are using egrep with -v options to select the lines which is not in the git branch -r. We use -f option because <(), which we call Process Substitution, will be treat as a file. /dev/fd/0 means stdin, which is git branch -r in our case.
Thus until here, the result should be:

branch1 <SHA> [origin/branch1] last commit message

awk '{print $1}'

Again, we are using awk to parse the stdin. The result should be:

branch1

xargs git branch -D

Finally, we are going to delete the branches in the previous result one-by-one using xargs. You can think of xargs works like:

for line in stdin
 git branch -D line

Here I use -D to force delete even though the branch isn't fully merged. If you don't want to delete the branches that aren't fully merged, please use -d

#!/bin/bash

It is very important to add this line at the first of the script. This is called shebang and it is use to specify which shell are we going to execute this script. We are leveraging Process Substitution, which isn't supported in sh. So if not specifying this, the script may be triggered by sh and throws error because sh doesn't recognize <().

Reference

16