Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Delete stashed changes older than X days

Tags:

git

How could I delete all stashes older than X days?

I use git stash in my everyday workflow, which means I end up with months of old stashes.

like image 990
Ashley Coolman Avatar asked Jun 29 '17 06:06

Ashley Coolman


1 Answers

Use git reflog expire manually

The underlying mechanism for "pushed" stashes—i.e., any stash whose name is not just stash or stash@{0}, but rather stash@{1}, stash@{2}, and so on—is Git's reflogs. The reflog records the previous value of a reference, whenever the reference changes. (I'll define reference below, and complete the definition of the reflog there.)

Most reflog entries expire on their own, through the configurable gc.reflogExpire and gc.reflogExpireUnreachable settings (see both the git config documentation and the git reflog documentation). The refs/stash reference, however, is special: by default, its entries never expire, rather than expiring after 30 or 90 days.

You can run git reflog expire manually and override any of these. For instance:

git reflog expire --expire-unreachable=40.days refs/stash

tells Git to expire any stash entries, all of which are always unreachable (see below for details), that are at least 40 days old. Add --dry-run to the options to see which ones would expire, without actually expiring them ... though there is a minor flaw here: it does not print their numbers, i.e., it never says that it's going to toss, say, stash@{17}.

References and reflogs

Git's references are a generalization of branch names, tag names, remote-tracking branch names, and all those other names that Git has. A reference simply translates a name—something like master or v1.2 or stash—to one of Git's internal hash IDs.

A branch name is just a reference whose full name starts with refs/heads/. A tag name is just a reference whose full name starts with refs/tags/. A remote-tracking branch name is a reference whose full name starts with refs/remotes/ (and then has the name of the remote and another slash).

Some reference names, particularly branches, change quite often. For instance, your master branch, which is really refs/heads/master, changes every time you add a new commit to master. Whenever Git replaces the stored hash value for a reference, it may—depending on whether reflogs are turned on—save the previous hash value. These saved entries are your reflogs.

Each reflog entry has a time stamp. Reflog entries eventually age out and get expired, so that you do not wind up with thousands or millions of reflog entries.

Reachable and unreachable, and why they're not applicable for refs/stash

Branch names normally move in a forward fashion, e.g., one commit at a time. That is, we add new commits to a branch, and all the old commits that were on that branch are still on the branch. Sometimes we will pick up commits en-masse and add them all at once, in a "fast" forward fashion: all the new commits are on the branch, and all the old commits are also still on the branch. The branch name points to the tip commit on the branch, the right-most o in this diagram:

...--o--o--o   <-- branch

We add more commits, and the branch name still points to the tip:

...--o--o--o--o--o--o   <-- branch

but since each commit points back to its parent commit, all these commits are reachable.

Sometimes, though, we deliberately remove a commit, and replace it with another. For instance, if we have commits we have not pushed, we can use git commit --amend or git rebase -i to make some changes. When we do that, the old ones don't go away just get. Instead, they get shoved to the side:

...--o--o--X   <-- branch

becomes:

          X ...... branch@{1}
         /
...--o--o--Y   <-- branch

where Y is our replacement for X.

Note that the parent commit of Y is not X, but rather X's parent. This means commit X is not reachable from Y.

The two separate "reflogExpire" configuration items, for reachable and unreachable objects, refer to commits that can be found by starting at the current value of the reference—for our branch named branch, that's refs/heads/branch—and working backwards. Commit Y is reachable, and so are the os, but X is not. If there is a reflog entry pointing to one of the earlier os, it's reachable, but branch@{1} points to X which is unreachable.

Git's designers essentially believe that unreachable commits are less worthy, so their reflog entries should expire sooner. Thus, the default is 30 days for unreachable entries, and 90 days for the reachable ones.

The refs/stash reference, though, does not advance normally at all. Instead, it points to the current stash, which is a sort of bag-on-the-side: a commit—actually at least two commits and sometimes more—that is not on any branch (I call this a "stash bag"; see How to recover from "git stash save --all"?). This, in turn, means that no previous stash@{n} is reachable from the current refs/stash, ever! Every stash reflog entry is therefore unreachable at all times.

like image 63
torek Avatar answered Oct 04 '22 00:10

torek