I have written a shell script utility for deleting all merged local branches.
#!/usr/bin/env bash
# Remove references to remote branches that no longer exist.
git remote prune origin
# Create a file containing list of all merged branches.
git branch --merged > 5db3bb3c-718a-444c-b1ce-d90a5a0d1cb3.clc
nano 5db3bb3c-718a-444c-b1ce-d90a5a0d1cb3.clc
# Trim trailing and leading whitespace etc.
sed 's/^[ \t]*//;s/[ \t]*$//' < 5db3bb3c-718a-444c-b1ce-d90a5a0d1cb3.clc
# Soft delete all branches left in the file and then remove tmp file.
xargs git branch -d < 5db3bb3c-718a-444c-b1ce-d90a5a0d1cb3.clc
rm 5db3bb3c-718a-444c-b1ce-d90a5a0d1cb3.clc
Although I wrote this only for personal use, I will likely create a repository for it on my github account and I am not particularly familiar with shell script best practices.
(Edit: The script eventually ended up on github here)
5 Answers 5
Improve your file handling:
- Don't use a hard coded file name for temporary files.
- Don't spew temporary files into the current directory.
- Make sure you clean up temporary files even on error.
The first two can be addressed by leveraging mktemp
. The last can be addressed with a trap
.
branchesfile=$(mktemp)
trap "{ rm -f $branchesfile; }" EXIT
You should also honor the user's choice of default editor, rather than require nano. You can get the user's editor preference with git var GIT_EDITOR
:
$(git var GIT_EDITOR) $branchesfile
Git tools typically honor the exit code of the editor. If the editor exited with error, this should stop the script:
if [ $? -ne 0 ]; then
exit 1
fi
This allows the user to cancel the operation if they spot a problem they don't want to fix at the moment.
Putting this all together:
#!/usr/bin/env bash
branchesfile=$(mktemp)
trap "{ rm -f $branchesfile; }" EXIT
# Remove references to remote branches that no longer exist.
git remote prune origin
# Create a file containing list of all merged branches.
git branch --merged > $branchesfile
$(git var GIT_EDITOR) $branchesfile
if [ $? -ne 0 ]; then
exit 1
fi
# Trim trailing and leading whitespace etc.
sed 's/^[ \t]*//;s/[ \t]*$//' < $branchesfile
# Soft delete all branches left in the file and then remove tmp file.
xargs git branch -d < $branchesfile
There's one other issue: your script will try to keep going if any other commands error out. set -e
is one option to solve this, but there are caveats to using it. If you choose to use set -e
, you'll need to revise the exit code checking of the editor, since bash will exit before it gets to the if
.
DRY - don't repeat yourself
"Don't repeat yourself" is a popular programming principle. You could apply it here by not repeating your filename over and over.
MY_BRANCHES="5db3bb3c-718a-444c-b1ce-d90a5a0d1cb3.clc"
git branch --merged > $MY_BRANCHES
nano $MY_BRANCHES
# Trim trailing and leading whitespace etc.
sed 's/^[ \t]*//;s/[ \t]*$//' < $MY_BRANCHES
# Soft delete all branches left in the file and then remove tmp file.
xargs git branch -d < $MY_BRANCHES
rm $MY_BRANCHES
-
\$\begingroup\$ jpmc26 provides a more complete answer. \$\endgroup\$chicks– chicks2018年02月21日 14:16:27 +00:00Commented Feb 21, 2018 at 14:16
I am assuming you are starting nano
so that you may remove possible occurrences of master
branch (or some other static branch) from the list. You can do the same in a single line command:
git branch --merged | grep -vE "(^\*|master|other|fixed|names)" | xargs git branch -d
The ^*
check will remove the currently checkedout branch, and you can put in the other branch names in the grep pattern.
-
1\$\begingroup\$ Actually, my intention was to allow interactive changes. But perhaps I'll add an interactive mode argument and do this by default. \$\endgroup\$Rudi Kershaw– Rudi Kershaw2018年02月20日 18:15:26 +00:00Commented Feb 20, 2018 at 18:15
-
2\$\begingroup\$ A better idea would be to not delete branches that track a remote branch. \$\endgroup\$jpmc26– jpmc262018年02月20日 22:33:36 +00:00Commented Feb 20, 2018 at 22:33
I've actually written a script to do the same thing myself. My tips:
Advice
I wouldn't use
git branch
, its output is only meant to be human readable (in git parlance, it's part of git's porcelain). A better suited command isgit for-each-ref
which outputs its data in a machine readable format (part of git's 'plumbing').I'd also be more explicit about which branch you care about being merged into. I believe by default
git branch --merged
will show you all branches already merged into the current branch. That works if you're onmaster
or whatever your main/trunk/integration branch is. But, if you happen to be on a different branch, you'll end up deleting a bunch of branches that you didn't mean to.There's also no need to save the branch names to a file, just operate on them iteratively.
Make sure you're not deleting your main branch (it will show as merged into itself).
I also wouldn't just willy-nilly delete the branches without asking the user first.
I'd also use bash's ability to stop immediately when any command has an error.
Putting it all together
Stop the script immediately on any command failure:
set -e
To get the "main" branch of your repo you can do:
main_branch=$(git rev-parse --abbrev-ref HEAD)
To get the local branches that have been merged into your main branch:
git for-each-ref --merged "${main_branch}" --format '%(refname:short)' 'refs/heads/'
Now you'll want to iterate over that list. Git branch names are usually safe from oddball characters that might mess with shell parsing, so we could probably do:
for branch in $(git for-each-ref --merged "${main_branch}" --format '%(refname:short)' 'refs/heads/') ; do
# something here
done
I've found to be more explicit that we're dealing with one entry per line, and to take the entire line as the branch name no matter what characters we might find there, the following form is better:
while read branch ; do
# something here
done < <(git for-each-ref --merged "${main_branch}" --format '%(refname:short)' 'refs/heads/')
Now, what code do we put inside the loop? Well, first we'll want to make sure we don't delete the main branch:
test "${main_branch}" = "${branch}" && continue
Build the delete branch command
cmd="git branch -d '${branch}';"$'\n'
Ask the user about running the delete command (make sure we read from the terminal since we're inside the while loop which is reading from the git for-each-ref
command):
read -p "${cmd}Execute(y/N)? " run < /dev/tty
Only if the user responded with exactly 'y' do we run the command:
test "${run}" = "y" && eval "${cmd}"
The whole shebang
#!/usr/bin/env bash
set -e
main_branch=$(git rev-parse --abbrev-ref HEAD)
while read branch ; do
done < <(git for-each-ref --merged "${main_branch}" --format '%(refname:short)' 'refs/heads/')
test "${main_branch}" = "${branch}" ] && continue
cmd="git branch -d '${branch}';"$'\n'
read -p "${cmd}Execute(y/N)? " run < /dev/tty
test "${run}" = "y" && eval "${cmd}"
done < <(git for-each-ref --merged "${main_branch}" --format '%(refname:short)' 'refs/heads/')
Notes:
Now, if those local branches that you deleted had remote branches that should also be deleted, you'd want to do something like:
# Get the remote name of the main branch, probably just 'origin'
remote=$(git config "branch.${main_branch}.remote")
# Then inside the loop, you'd do something like this:
git push --delete "${remote}" "${branch}"
But it's a little more complicated because you'd want to first find out if this branch even has a remote set up, what the actual remote branch name is (could be different from the local), if its HEAD
is at (or an ancestor of) the local branch, etc.
The command below deletes anything that has been merged into master.
git branch --merged | egrep -v "(^\*|master|dev)" | xargs git branch -d
sed
command will actually do anything useful: you've directed its input from the file, but its output will just be to standard output, so the file won't be edited. I think you need the-i
option to edit the file in place. \$\endgroup\$xargs
already reads in lines with leading/trailing whitespace removed making thatsed
redundant. A strange looking file that is - is it some kind of Magic number? \$\endgroup\$mktemp
. \$\endgroup\$