If you’ve been working with git for long enough you’ve probably noticed the local branches tend to accumulate. The git branch command provides a handy option to filter branches where the tip commits also belongs to the current branch:

git branch --merged

This is great and works fine in the majority of case and is often found in oneliners to cleanup local branches but this is not enough when working with workflows where the commits are often rewritten a few times before they eventually make it in tree, for example the Gerrit workflow we use in OpenStack. Gerrit uniquely identifies the commit with the Change-Id, a small label it inserts in the commit message and we can make advantage of this to check if a commit has been merged or not and delete the local branch.

First we need to get the Change-Id:

# Get Change-Id of $commit
change_id=$(git log -n1 --pretty=format:%b $commit | awk '/Change-Id:/ {print $0}')

With this Change-Id we can now verify if it was merged or not in the current branch:

# Check that commit was merged into $current_branch
merged_commit=$(git log --pretty=format:%h --grep "$change_id" ${current_branch})
if [ -z "$merged_commit" ]; then
    # This change is missing from $current_branch
fi

Great, so now we can find commits that have not yet been merged in the current branch. But what about the ones that were merged? It is entirely possible someone pushed a new version of the patch in Gerrit, or maybe I made local changes to a patch I haven’t yet submitted for review and the change was merged in the meantime. So if I blindly delete the branch I may lose important local changes. How can I check the two versions of the patch are the same? The interdiff tool compares diff files and we can assume they’re the same when the output is empty:

if [[ $(interdiff <(git show $commit) <(git show $merged_commit) 2>&1) ]]; then
    # The patch that was merged differs from what I have in local branch
fi

Putting it all together:

#!/bin/bash

function prompt_for_missing_commit {
    commit=$1
    branch=$2
    current_branch=$3
    git log --oneline -n1 $commit
    read -p "Commit $commit in $branch is missing from $current_branch. Inspect? [Yn] " answer
    if ! [[ "${answer,,}" =~ ^(n|no)$ ]]; then
        git show $commit
    fi
}

function prompt_for_commit_diff {
    local_commit=$1
    merged_commit=$2
    local_branch=$3
    current_branch=$4
    git log --oneline -n1 $commit
    read -p "Commit $local_commit in $local_branch and $merged_commit in $current_branch differ. Inspect? [Yn] " answer
    if ! [[ "${answer,,}" =~ ^(n|no)$ ]]; then
        interdiff <(git show $local_commit) <(git show $merged_commit) | colordiff
    fi
}

current_branch=$(git symbolic-ref --short HEAD)

for branch in $(git for-each-ref --format='%(refname:short)' refs/heads/); do
    if [ "$branch" == "$current_branch" ]; then
        continue
    fi
    echo
    echo "Checking branch $branch"
    branch_differs=0
    for commit in $(git log --no-merges --pretty=format:"%h" ${current_branch}..${branch}); do
        change_id=$(git log -n1 --pretty=format:%b $commit | awk '/Change-Id:/ {print $0}')
        if [ -z "$change_id" ]; then
            branch_differs=1
            prompt_for_missing_commit $commit $branch $current_branch
            continue
        fi
        merged_commit=$(git log --pretty=format:%h --grep "$change_id" ${current_branch})
        if [ -z "$merged_commit" ]; then
            branch_differs=1
            prompt_for_missing_commit $commit $branch $current_branch
            continue
        else
            # Check that the merged patch is similar to what is in local branch
            # NOTE needs interdiff from patchutils and colordiff
            if [[ $(interdiff <(git show $commit) <(git show $merged_commit) 2>&1) ]]; then
                branch_differs=1
                prompt_for_commit_diff $commit $merged_commit $branch $current_branch
            fi
        fi
    done
    if [ $branch_differs -eq 0 ]; then
        read -p "$branch fully merged. Delete? [yN] " answer
        if [[ "${answer,,}" =~ ^(y|yes)$ ]]; then
            git branch -D $branch
        fi
    else
        read -p "$branch differs from $current_branch. Delete anyway? [yN] " answer
        if [[ "${answer,,}" =~ ^(y|yes)$ ]]; then
            git branch -D $branch
        fi
    fi
done