9

I know how to delete all local branches that have been merged. However, and I believe this is due to Github's new pull request squash and merge feature, I find myself being left with a lot of local branches that are unmerged, but if merged into master would result in no changes.

How can I prune these local branches, i.e. those local branches which haven't necessarily been merged, but wouldn't affect master (or, more generically, the current branch)?

Community
  • 1
  • 1
Claudiu
  • 206,738
  • 150
  • 445
  • 651
  • Do you really expect to have many local branches sitting around which have no changes compared to master? Wouldn't this imply that someone created a branch and did nothing with it? – Tim Biegeleisen Apr 05 '16 at 14:28
  • The process is: I create a local branch, I make changes, I push it to remote, I submit a PR, the PR gets squash-merged into master, I fast-forward my local master, and... my local branch remains unmerged (`git branch -d` doesn't delete it), but it has no changes compared to master (`git diff branch_name` has no results). So if I have done 10 PRs, I will have 10 unmerged local branches with no changes. – Claudiu Apr 05 '16 at 14:54
  • I don't like this workflow. Why not just kill the branch after merging back into `master`? – Tim Biegeleisen Apr 05 '16 at 14:55
  • 1
    The remote branch does get killed, after the PR gets merged. I'd *like* the local branch to be automatically killed... I have a `alias git-delete-merged="git branch --merged master | grep -v '\* master' | xargs -n 1 git branch -d"` which does just this, but it doesn't work on these branches because they aren't technically merged. I *could* delete my local branch manually like an animal once the PR is submitted, but it's not as nice as having it happen automatically - before squash merges I just did an alias `git-reset-master`, which FF my master and deletes the merged, and it just worked (TM). – Claudiu Apr 05 '16 at 15:04
  • For posterity, what I ended up doing was this: `git fetch -p && git branch -D $(git branch -vv | grep gone | cut -d " " -f 3)`. It is not what I asked... but this deletes all local branches, which *had* remote branches, that are now deleted. This ended up matching my workflow fairly well, though it could end up killing something that would have been useful... – Claudiu May 05 '16 at 21:04

2 Answers2

3

There is no perfect solution but you can get close, perhaps close enough.

Be sure to start with a clean work tree and index (see require_clean_work_tree in git-sh-setup).

For each candidate branch $branch that might be delete-able:

  1. Find its merge target (presumably merge_target=$(git config --get branch.${branch}.merge)). Check out the merge target.
  2. Do a merge with --no-commit; or in step 1, check out with --detach so that you will get a commit you can abandon, if the merge succeeds.
  3. Test whether git thinks the merge succeeded, and if so, whether the current tree matches the previous tree, i.e., brought in no changes. If you can test exact matches, and if you allow the commit to happen (via --detach), you can do this last test very simply, without any diff-ing: run both git rev-parse HEAD^{tree} and git rev-parse HEAD^^{tree}1 and see if they produce the same hash. If you don't allow the commit, you can still git diff the current (HEAD) commit against the proposed merge. If you need to remove some noise from the diff (e.g., config files that should not be, but are anyway, in the commits), this gives you a place to do it.
  4. Reset (git merge --abort; git reset --hard HEAD; git clean -f or similar, depending on how you have decided to implement steps 1-3). This is just meant to make your work tree and index clean again, for the next pass.
  5. If the merge in step 3 worked and introduced no changes, you may delete the local branch. Otherwise, keep it.

In essence, this is "actually do the merge and see what happens", just fully automated.


1This notation looks a bit bizarre, but it's just HEAD^—the first parent of HEAD—followed by ^{tree}. Alternate spellings might be easier to read: HEAD~1^{tree} or ${merge_target}^tree, where ${merge_target} is the branch you checked out in step 1. Note that this assumes the merge succeeded. The merge result is in the exit status of git merge: zero means succeeded, nonzero means failed and needs manual assistance, presumably due to a merge conflict.

torek
  • 330,127
  • 43
  • 437
  • 552
  • Hmm I am intrigued! This seems like it should be enough for me to produce a command which will do what I like automatically. Now that I'm using Git more, I'm coming to appreciate it more as a coding language of sorts, instead of a monolithic version control system - if I know how it works, I can use the language (i.e. the built-in commands) to write new commands with the functionality I want (a.k.a. functions in other languages). – Claudiu Apr 06 '16 at 02:35
  • Yes, especially git's "plumbing" commands are meant for scripting. The top level `git merge` is not really a plumbing command but it will have to serve here. The main issue with a script that does this is that it could delete branches you created intending to work on, but haven't actually started working on yet, so you might want to filter your candidates down a bit in that case. – torek Apr 06 '16 at 03:29
0

If you run git "branch -v" the tracking branches which have changes will have "ahead" written next to them.

The other two options: "behind" and if nothing is written, means that the branches have no changes that will affect the branches they track.

You can therefore run "git fetch" to update the remote tracking branches then parse the "git branch -v" results to figure out which branches have no changes and which branches have.

Yakir Dorani
  • 59
  • 1
  • 4