Deleting branches that have been merged
Posted on Sat 07 November 2015 in Repository maintenance
Once branches have been merged into the main branch, all that's effectively left of the branch is the ref pointing to an older commit. This ref may be worth keeping, but many people seem to want to get rid of those refs. Git does not provide a tool to do this, but does provide the toolkit to create such a command yourself.
git rev-parse
can be used to find the full ref of the branch you want to take as destination branch, where branches should be merged into.git for-each-ref --merged $branch --base refs/heads/
can be used to find stale branches (but beware: $branch itself will be part of its output)git for-each-ref --merged $branch --base refs/remotes/$remote
can be used to find stale branches (but beware: refs/remotes/$remote/$branch itself may be part of its output)git branch
andgit push
can be used to actually delete the branches.
You can hack this up in a shell script, but I prefer using python for my quick
hacks. The python script below implements a git delete-merged-branches
command that can delete merged branches either locally or remotely. If you put
it somewhere on your $PATH
, you can call it for example as git
delete-merged-branches master
and it will delete all branches merged into
master.
It has a few safety measures:
- Refs pointing to the same commit as $branch will not be deleted
- When deleting from a remote, it does a fetch first (of course there is still a race condition there)
- It has a --noop mode, which you really should use before actually deleting refs.
But first let's see it in action. We'll create some bogus branches pointing to ancestors of HEAD, essentially making them merged.
$ git branch yoink HEAD~1
$ git branch yoink2 HEAD~2
$ git branch yoink3 HEAD~3
$ git push origin yoink yoink2 yoink3
Total 0 (delta 0), reused 0 (delta 0)
To https://git.example.com/example.git
* [new branch] yoink -> yoink
* [new branch] yoink2 -> yoink2
* [new branch] yoink3 -> yoink3
Let's see what git delete-merged-branches would do.
$ git delete-merged-branches --noop
Would delete refs/heads/yoink
Would delete refs/heads/yoink2
Would delete refs/heads/yoink3
$ git delete-merged-branches --remote origin --noop master
Would delete refs/remotes/origin/yoink
Would delete refs/remotes/origin/yoink2
Would delete refs/remotes/origin/yoink3
That sounds sane, let's go ahead and delete them.
$ git delete-merged-branches --remote origin
To https://git.example.com/example.git
- [deleted] yoink
- [deleted] yoink2
- [deleted] yoink3
$ git delete-merged-branches
Deleted branch yoink (was 259b5e6).
Deleted branch yoink2 (was aa826b6).
Deleted branch yoink3 (was c29024e).
So if you're someone who likes cleaning up stale refs that have been merged, here's a tool for you.
Download git-delete-merged-branches
#!/usr/bin/python
import docopt
from whelk import Shell
import sys
usage = """git delete-merged-branches
Delete merged branches, either remote or locally
Usage:
git delete-merged-branches [--noop] [--remote=REMOTE] [<branch>]
"""
# Make sure we exit when a git command fails
def check(cmd, sp, res):
if not res:
if res.stderr:
sys.stderr.write(res.stderr)
sys.exit(res.returncode)
shell = Shell(exit_callback=check)
def main():
sys.argv.insert(1, 'delete-merged-branches')
opts = docopt.docopt(usage)
shell.git('rev-parse')
dest = opts['<branch>'] or 'HEAD'
dest = shell.git('rev-parse', '--symbolic-full-name', dest).stdout.strip()
dest_sha = shell.git('rev-parse', dest).stdout.strip()
base = 'refs/heads'
if opts['--remote']:
base = 'refs/remotes/%s' % opts['--remote']
# Make sure we're up to date with what the remote actually has
shell.git('fetch', opts['--remote'])
refs = shell.git('for-each-ref', '--merged', dest, base).stdout.splitlines()
refs = [ref.split(None, 2) for ref in refs]
to_delete = []
for oid, reftype, refname in refs:
if oid == dest_sha or refname == dest:
# As a precaution, don't delete branches that point to the current commit
continue
if opts['--noop']:
print("Would delete %s" % refname)
elif opts['--remote']:
ref = refname.replace('refs/remotes/%s/' % opts['--remote'], 'refs/heads/')
to_delete.append(ref)
else:
branch = refname.replace('refs/heads/', '')
to_delete.append(branch)
if to_delete and not opts['--noop']:
kwargs = {'redirect': False}
if opts['--remote']:
shell.git('push', opts['--remote'], '--delete', *to_delete, **kwargs)
else:
shell.git('branch', '-d', *to_delete, **kwargs)
if __name__ == '__main__':
main()