How to cleanup local branches which tracking branch has been removed
Tags: git, bash
Published: 2021-06-28 08:03:49

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`:
```bash
#!/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](https://en.wikipedia.org/wiki/Shebang_(Unix))) should be either `#!/bin/bash` or `#!/bin/zsh`

**Step 3**: Make it executable: 
```bash
chmod +x ./git-cleanup
```

**Step 4**: Add git alias:
```bash
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](https://git-scm.com/docs/git-fetch) for more information.

### `git branch -r`
Show all the remote branches. [Documentation](https://git-scm.com/docs/git-branch)

### `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](https://en.wikipedia.org/wiki/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:
```python
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](https://en.wikipedia.org/wiki/Shebang_(Unix)) 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
* [stack overflow - How to prune local tracking branches that do not exist on remote anymore](https://stackoverflow.com/questions/13064613/how-to-prune-local-tracking-branches-that-do-not-exist-on-remote-anymore)