Ever had text fill your entire terminal window when all you did was a quick git branch? Here’s a quick pair of scripts which solves that problem by making it a simple matter of muscle memory to keep your repo completely clean.

The Idea

You’ve probably seen much worse than this:

When I’m in that situation, I type bin/clean-branches and I get this instead:

Some of these branches represent blog post ideas which were not actually good ideas, but that’s another story.

What matters is that my scripts automatically delete any branches that I’ve already merged into master — except for development, rc, or master itself, and except for any branch with refactor in the name — and make it effortless to spot branches that I’ve abandoned. For example, if it’s been 4 weeks since you made any commits on a branch, odds are good you’re done with it. But the script still leaves those branches for you to delete by hand, since it’d otherwise be a little reckless for easy, day-to-day use.

I wrote this on a project which used Git Flow, and sadly did not have continuous deployment, which is why it doesn’t delete development or rc. You can probably guess why it doesn’t delete master either. The scripts spare refactor branches since those are branches which you might actually want to keep around even though you’re not working on them every day. But the design goal is to have a script you can run 100 times a day as part of your muscle memory to keep your local repo easy to navigate, and to prevent it from developing garbage collection problems.

The Code

I’ve put this little system in a gist. It’s made up of two scripts.

The first script is in Ruby:

# this script will delete all fully-merged branches, except for
# branches marked "refactor." it also skips over deployment branches
# like master, development, etc.

require 'open3'

def get_branches
  git_branch = "git branch"
  branches = []

  Open3.popen3(git_branch) do |stdin, stdout, stderr, wait_thr|
    branches = stdout.read.split("\n")
  end

  branches
end

def whitespace(branches)
  branches.map {|branch| branch.strip}
end

def get_rid_of_current_branch_indicator(branches)
  branches.map do |branch|
    if branch.match(/^\* (.+)/)
      branch.match(/^\* (.+)/)[1]
    else
      branch
    end
  end
end

def filter_out_deployment_branches(branches)
  deployment_branches = %w{
    master
    development
    rc
  }

  branches - deployment_branches
end

def filter_out_refactoring_branches(branches)
  branches.select do |branch|
    ! branch.match(/refactor/)
  end
end

branches = get_branches
branches = whitespace(branches)
branches = get_rid_of_current_branch_indicator(branches)
branches = filter_out_deployment_branches(branches)
branches = filter_out_refactoring_branches(branches)

branches.each do |branch|
  puts "attempting to delete branch: #{branch}"
  git_delete_branch = "git br -d #{branch}"
  Open3.popen3(git_delete_branch) do |stdin, stdout, stderr, wait_thr|
    stdout.each_line {|line| puts line}
    stderr.each_line {|line| puts line}
  end
end

This is the main script in the system. It creates an array of branch names, throws out whitespace, filters out branches you want to keep, and gets rid of the little asterisk which git branch uses to tell you which branch is the current branch. In so doing, it generates a list of branches to delete. It then runs git branch -d against each branch in that list, since git branch will not delete an unmerged branch unless you use -D instead.

The second script is just a quick bash wrapper around the first one:

ruby bin/clean-branches.rb $* >/dev/null 2>/dev/null && git for-each-ref --sort='-authordate:iso8601' --format=' %(authordate:relative)%09%(refname:short)' refs/heads

The wrapper first invokes the Ruby script, silencing its output, and then uses some git magic I found on the internet somewhere to sort your branches in reverse chronological order and show you when they were last modified.

I typically put this in the bin subdir of a given repo, and add that subdir to the .gitignore (if it’s not there already). You could put this in ~/bin instead, and add it to your shell path, but the hard-coded list of branchnames to not delete is, in my opinion, repo-specific. If the messiness of copy/pasting this script for multiple repos bothers you, it’d be pretty easy to have it pull those branchnames from an env var or something, but in practice, this gets the job done.