Hari
Hari

Reputation: 193

Deleting Git stashes on a specific branch alone

git stash drop [<stash>] does not seem to have a branch option.

Source: http://git-scm.com/docs/git-stash

I guess it's a very obvious requirement to delete all stashes done on a specific branch, once that branch has been merged into master and is no longer needed. Is there a way to do it? I have some 40-50 stashes to be deleted.

Upvotes: 0

Views: 68

Answers (1)

torek
torek

Reputation: 490078

Stashes are not really "on a branch", they're on specific commits (see this answer to a different question for a diagram of what I mean). That said, any specific commit can be said to be "on" zero or more branches, so given a branch name, you could find and test each stash:

let to_drop = empty_set()
for (s in all_stashes):
    let c = commit_of(s)
    if is_on_branch(c, branch):
        add_to_set(s, to_drop)
for s in reverse(to_drop):
    execute("git stash drop " + s)

more or less (this is in pseudo-Python, which I find a bit more like pseudo-code than pseudo-shell). This defines a notion of a stash being "on a branch", which is what I use in the rest of this answer.

(The reason for dropping in reverse is that "all stashes" is likely to go "stash@{0}, stash@{1}, stash@{2}, ..., stash@{n}" and when we drop some stash, all higher-numbered stashes are renumbered, so if we work backwards we can avoid the need to renumber remaining stashes.)

To turn the above into working shell script code, we just need to do the obvious translation. Here are a bunch of pieces (largely untested, some pieces tested in isolation):

# Produce a list of all refs/stash@{n} and sha-1.
# Might be nice if we could use --reverse here, but
# we can't.
all_stashes() {
    git log -g --pretty=format:"%gd %H" refs/stash
}

(The above is just git stash list hacked up a bit to print the stash-name and full hash.)

# Determine whether the given commit ($2) is on the given branch ($1).
# If the commit is on multiple branches, we say "yes" if ANY of those
# match $1 (but the match must be exact).  Note that the while loop
# runs in a subshell so we use an eval trick here.  (Probably would
# be better to just write this in awk.)
#
# Note that "git branch --contains" prints "  A -> B" for indirect
# branches; we ignore these.  It prefixes the current branch with "* "
# and others with "  ", and we discard these prefixes.  You may want
# to fiddle with the first decision.
is_on() {
    local branch
    eval $(git branch --contains $2 | sed -e '/ -> /d' -e 's/^..//' |
        while read branch; do
            [ "$branch" = "$1" ] && echo return 0
        done)
    return 1
}

# Given branch name ($1), find all stashes whose parent commit
# is on that branch.
stashes_on_branch() {
    local refname hash

    all_stashes | while read refname hash; do
        is_on "$1" ${hash}^ && echo "$refname"
    done
}

(Incidentally, sh seems to allow return 0 1 2 3 but bash complains, so the above might need to actually break out of the loop after echoing return 0 on some systems.)

For reversing a list of lines, "tac" would be ideal but most systems do not have it. We could use bash arrays and so on but this is a lot simpler, albeit inefficient and subject to problems if a branch is named -e for instance:

# Reverse a list of lines
reverse() {
    local line text nl=$'\n'
    while read line; do
        text="$line$nl$text"
    done
    echo -n "$text"
}

With all those pieces it's now trivial to drop the stashes that are "on" a given branch:

for stash in $(stashes_on_branch foobranch); do git stash drop $stash; done

All that said, just because a branch is merged doesn't necessarily mean all stashes that are "on" that branch should be dropped. If that's your particular workflow, fine; but this is unusual enough that there's no particular reason that the stash script should have this built-in.

Upvotes: 1

Related Questions