4

How can I determine what is preventing a commit from being pruned from git by the following commands?

git reflog expire --expire=now --all

git gc --prune=now

Details

I want to completely remove a commit (with, e.g., commit hash XYZ) from my clone. If the above is not the correct command to do so (or if any of my following commands / deductions are incorrect), please let me know.

I know that XYZ remains in my clone after running the above prune because the following returns a log listing:

git log XYZ

I know that XYZ isn't in any branch because the following outputs nothing:

git branch --contains XYZ

I thought that XYZ wasn't in any stash because the following outputs nothing:

git stash list

XYZ, however, actually was in a stash, but a git bug prevented the stash from being listed.

XDR
  • 4,070
  • 3
  • 30
  • 54
  • 1
    Does the commit have a tag associated with it? Have a look at `git show-ref` to see any references. – cmbuckley Sep 05 '18 at 16:47
  • The `git show-ref` output includes `XYZ refs/stash`, yet `git stash list` still outputs nothing… – XDR Sep 05 '18 at 17:50
  • 1
    Interesting that it's the "latest" stash then... Try a `git stash clear` and see if that helps – cmbuckley Sep 05 '18 at 17:56
  • That fixed it. Why would `git stash list` output nothing if there's something in the stash? That makes no sense… – XDR Sep 05 '18 at 18:00
  • 1
    Only thing I've found remotely like this is https://stackoverflow.com/questions/22076944/git-stash-reporting-is-not-a-stash-reference – cmbuckley Sep 05 '18 at 18:57

1 Answers1

8

If there are no stashes and you've expired the reflogs, it seems reasonable to assume that the commit is reachable from some ref - but not all refs are branches.

You could try this:

git for-each-ref --format='%(refname)' |xargs -I {} git rev-list {} --format="%H {}" |grep ^<hash>

where <hash> is the ID of the commit you're looking to get rid of. In a simple test I ran

git for-each-ref --format='%(refname)' |xargs -I {} git rev-list {} --format="%H {}" |grep ^80c0ab

and got output like

80c0ab39850d7b3ef4969ab934d834f22959a317 refs/original/refs/heads/master

telling me that my target commit was kept alive by a ref under refs/original - in this case a pre-rewrite "backup ref" created by git filter-branch


Update - Some follow-up based on comments.

You note that the above command returns refs/stash, yet the stash list (per git stash list) is empty.

The thing is, the stash list uses a heavily manipulated reflog. And the command you used to clear reflogs

git reflog expire --expire=now --all

will have destroyed the key reflog. So now the stash commands don't know what to do and act like there are no stashes, but the stash ref does still exist, keeping anything from the most recent stash (or the full commit history reachable from the commit on which that stash was created) alive locally[1].

IMO that could be considered a bug. Scheduled reflog expiry by default leaves stash alone (for... well... this reason). Perhaps the argument goes that you specifically said to expire all reflogs, but I would argue that "all reflogs except the stash" would be a more useful definition of --all in this instance.

Well, whatever.

If you're sure you don't care about whatever was stashed

git update-ref -d refs/stash

and then resume your clean-up.


[1] "alive locally" because, at least by default, stash isn't shared. It is likely that cloning the repo, or pushing its refs into an empty remote, would not carry the offending commit along. However, this is dependent on the assumption that git will send a minimal pack - and AFAIK it isn't guaranteed to do that. So if you need the commit gone, then the safest thing is to reach a point where it doesn't exist locally, and then rebuild any remotes (etc) from that clean local repo.

Mark Adelsberger
  • 42,148
  • 4
  • 35
  • 52