Listing and deleting Git commits that are under no branch (dangling?)

112k Views Asked by At

I've got a Git repository with plenty of commits that are under no particular branch, I can git show them, but when I try to list branches that contain them, it reports back nothing.

I thought this is the dangling commits/tree issue (as a result of -D branch), so I pruned the repo, but I still see the same behavior after that:

$ git fetch origin

$ git fsck --unreachable
$ git fsck

No output, nothing dangling (right?). But the commit exists

$ git show 793db7f272ba4bbdd1e32f14410a52a412667042
commit 793db7f272ba4bbdd1e32f14410a52a412667042
Author: ...

and it is not reachable through any branch as

$ git branch --contains 793db7f272ba4bbdd1e32f14410a52a412667042

gives no output.

What exactly is the state of that commit? How can I list all commits in a similar state? How can I delete commits like those?

11

There are 11 best solutions below

10
On BEST ANSWER

No output, nothing dangling (right?)

Note that commits referred to from your reflog are considered reachable.

What exactly is the state of that commit? How can I list all commits with similar state

Pass --no-reflogs to convince git fsck to show them to you.

How can I delete commits like those?

Once your reflog entries are expired, those objects will then also be cleaned up by git gc.

Expiry is regulated by the gc.pruneexpire, gc.reflogexpire, and gc.reflogexpireunreachable settings. Cf. git help config.

The defaults are all quite reasonable.

0
On

TL:DR;

A commit can be referenced by:

  • a branch
  • a tag (!)
  • a stash (!!)
  • a "replace" (!!?)

Use this command to find what it is exactly:

git for-each-ref --contains <SHA>

Long version

I did a filter-repo of the repo to clean up some huge files.

Obviously ended up with some dangling commits. I tried all the answers, and nothing was working.

git branch --contains <SHA>           # nothing!
git fsck --unreachable --no-reflogs   # nothing!

Then I found and deleted a tag thanks to this answer

Still the commit kept shwoing up!

So I finally ran this command:

git for-each-ref --contains <SHA>

And it returned

1b9bf9c63209a4728b2d3dc7946da836dc331bbd commit refs/stash

Bingo!

I ran git stash - and it was (oddly) empty. How can a stash reference a commit if I have nothing in my stash?

However running git stash drop several times has deleted some god forsaken "dangling" stashes based on obsolete branches and finally I was able to gc that commit, and my repo instantly got 15MB smaller.

Longer version

(From another user) I used the tips above but it returned

4c79e05b102c673e1f60242c022ff06e017b9a1d commit refs/replace/2acd8f182e14d7d367b3469fd8751f2d9738a884

what the heck is replace?

git replace -h

shows you can list delete and edit these 'replacement objects'. Weird. Running git replace -d 2acd8f removes the commit!

0
On

I accidentally hit the same situation and found my stashes contain reference to the unreachable commit, and thus the presumed unreachable commit was reachable from stashes.

These were what I did to make it truly unreachable.

git stash clear
git reflog expire --expire-unreachable=now --all
git fsck --unreachable
git gc --prune=now
0
On

If the stash is truly a stash that "doesn't exist" and not a tag,
git fsck --full
may help. It worked for me when no other solution did.

(Git: Remove broken stash describes my problem more accurately than this thread)

8
On

To remove all dangling commits (including those still reachable from stashes and other reflogs) do this:

git reflog expire --expire-unreachable=now --all
git gc --prune=now

But be certain that this is what you want. I recommend you read the man pages but here is the gist:

git gc removes unreachable objects (commits, trees, blobs (files)). An object is unreachable if it isn't part of the history of some branch. Actually it is a bit more complicated:

Stashes are implemented using the reflog (i.e not not branches or tags). That means that they are subject to garbage collection.

git gc does some other things but they are not relevant here and not dangerous.

Unreachable objects that are younger than two weeks are not removed so we use --prune=now which means "remove unreachable objects that were created before now".

Objects can also be reached through the reflog. While branches record the history of some project, reflogs record the history of these branches. If you amend, reset etc. commits are removed from the branch history but git keeps them around in case you realize that you made a mistake. Reflogs are a convenient way to find out what destructive (and other) operations were performed on a branch (or HEAD), making it easier to undo a destructive operation.

So we also have to remove the reflogs to actually remove everything not reachable from a branch. We do so by expiring --all reflogs. Again git keeps a bit of the reflogs to protect users so we again have to tell it not to do so: --expire-unreachable=now.

Since I mainly use the reflog to recover from destructive operations I usually use --expire=now instead, which zaps the reflogs completely.

5
On
git branch --contains 793db7f272ba4bbdd1e32f14410a52a412667042

probably just needs to be

git branch -a --contains 793db7f272ba4bbdd1e32f14410a52a412667042

to also report on branches from remotes

6
On

I had the same issue, still after following all the advice in this thread:

git reflog expire --expire-unreachable=now --all
git gc --prune=now
git fsck --unreachable --no-reflogs   # no output
git branch -a --contains <commit>     # no output
git show <commit>                     # still shows up

If it's not a reflog and not a branch, ...it must be a tag!

git tag                             # showed several old tags created before the cleanup

I removed the tags with git tag -d <tagname> and redid the cleanup, and the old commits were gone.

Update: If it turns out it's not a tag, it might also be a stash! Check this answer and this answer

0
On

First you should consider whether you really need to aggressively delete/prune all unreachable commits.

You might have gotten this message:

warning: There are too many unreachable loose objects; run 'git prune' to remove them.

And then git prune didn’t seem to solve the problem, so you are looking for knobs to delete things more aggressively.

The problem with pruning recent commits is that you can in the worst case (I don’t know how likely this is) corrupt things if you also interact with a remote.

The racy behavior occurs when a repository receives one or more pushes during this process. The main culprit is that the server advertises its objects at a different point in time from processing the objects that the client sent based on that advertisement.

[…] If one of these pushes happens before C is actually removed, then the repository can end up in a corrupt state.

So if “loose objects” is your direct problem—rather than lack of disk space, or just too many objects—then you can enable this experimental feature (as of Git 2.40.1):

git config gc.cruftPacks true

Now git gc will pack unreachable objects, which should get rid of that warning.


It seems on balance safer to use an experimental feature if it means that you won’t have a reason to prune very recent unreachable objects.

0
On

I know this is not a good solution but I did a filter-branch and ended up with duplicate, unreachable commits which didn't belong to any branch but couldn't remove them automatically, I tried every single solution posted here, absolutely nothing worked. So I pushed to a remote repository (github), deleted my local repository and then pulled again and got rid of all those unreachable commits

0
On

git gc --prune=<date> defaults to prune objects older than two weeks ago. You could set a more recent date. But, git commands that create loose objects generally will run git gc --auto (which prunes loose objects if their number exceeds the value of configuration variable gc.auto).

Are you sure that you want to delete these commits? gc.auto's default setting will ensure that the loose objects do not take up an unreasonable amount of memory, and storing loose objects for some amount of time is generally a good idea. That way, if you realize tomorrow that your deleted branch contained a commit you needed, you can recover it.

2
On

I had a similar issue. I ran git branch --contains <commit>, and it returned no output just like in the question.

But even after running

git reflog expire --expire-unreachable=now --all
git gc --prune=now

my commit was still accessible using git show <commit>. This was because one of the commits in its detached/dangled "branch" was tagged. I removed the tag, ran the above commands again, and I was golden. git show <commit> returned fatal: bad object <commit> - exactly what I needed. Hopefully this helps someone else that was as stuck as I was.