161

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?

Palec
  • 10,298
  • 7
  • 52
  • 116
Samer Buna
  • 7,413
  • 8
  • 36
  • 53
  • possible duplicate of [How to delete erroneous merge commits?](http://stackoverflow.com/questions/3711792/how-to-delete-erroneous-merge-commits) – Josh Lee Sep 21 '10 at 23:43

7 Answers7

287

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.

tarsius
  • 7,061
  • 5
  • 30
  • 42
  • 1
    I tell you what commands to use which is not obvious - shouldn't gc be enough? If you never used git-reflog before you won't know. So now that you know what commands you have to use you should look up the mentioned options in their man pages. Of course I could instead just copy that information from there... – tarsius Aug 02 '12 at 16:56
  • 1
    Well actually I say exactly what it does: "remove all dangling commits and those reachable from the reflogs". If you don't know what reflogs are: again read the manual. – tarsius Aug 02 '12 at 16:58
  • 7
    While the answer given may be correct, @erikb85 is correct in pointing out that there was no education about what you were being told to do. Following up with RTFM is even less helpful. Yes, we should all read all of the documentation. In some cases perhaps the person doing the search doesn't grok the documentation enough to know what is going on. So, a little bit of education as to what the commands are doing would be helpful for everyone that finds this answer later. – Lee Saferite Mar 28 '13 at 16:31
  • @LeeSaferite hope you're all happy now :-) – tarsius Apr 23 '13 at 23:55
  • 13
    `git reflog expire --expire-unreachable=now --all` drops all your stashes! – Vsevolod Golovanov Jun 16 '17 at 19:15
  • I think this answer needs a clear warning, preferably at the top. My edit suggestion was rejected, because I guess I should suggest it to the author in a comment? Please either accept this edit https://stackoverflow.com/review/suggested-edits/26023983 or add a warning your own way. That it drops all your stashes is a big deal too! – Inigo May 04 '20 at 20:14
79

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.

Aristotle Pagaltzis
  • 101,052
  • 21
  • 94
  • 96
  • 2
    so you're basically saying that the reflows for dangling commits will be removed after a while automatically? – MoralCode Nov 23 '16 at 00:46
  • 2
    Basically: yes – except that the question is a bit confused. I’m saying that *all* reflog entries are removed automatically after a while, but you can change that through configuration settings. And because a commit is only called [dangling](https://git-scm.com/docs/gitglossary#gitglossary-aiddefdanglingobjectadanglingobject) when it has *nothing* pointing to it – including reflog entries –, “reflogs for dangling commits” are not a thing. They would be “reflogs for [unreachable](https://git-scm.com/docs/gitglossary#gitglossary-aiddefunreachableobjectaunreachableobject) commits”. – Aristotle Pagaltzis Feb 19 '17 at 05:37
  • 'They would be “reflogs for unreachable commits”.' But you said "commits referred to from your reflog are considered reachable." So how can "reflogs for unreachable commits" be a thing? I'm so confused. – LarsH Mar 09 '18 at 14:47
  • 1
    Yeah, I wasn’t consistent. Normally people don’t think about the reflog, and when they say “unreachable” it implies “from a ref”. Even `git help glossary` defines it that way… whereas its definition for “reachable” is not narrowed down that way, so they are contradictory. Funny – so what I said is actually consistent with the confusion in `gitglossary`… It’s not the concepts that are confusing, though, just the terminology. The point is that “dangling” commits are ones that *nothing* else points to. Would it help if I say “reflogs for *otherwise* unreachable commits”…? – Aristotle Pagaltzis Mar 10 '18 at 19:48
  • This is all very confusing. Let's make it simple. When on branch `master`, you do `git commit`, and get a commit `000001`. Then you do `git commit --amend`, which gives you commit `000002`. There are no tags or branches pointing to `000001` anymore, and you can't see it in your log without the `--reflog` option, but if you want, you could still get to it with `git checkout 000001`. Now the question is, Is `000001` a *dangling* commit, or an *unreachable* commit, or neither, or both? – chharvey Sep 21 '18 at 01:10
  • Both. Every dangling commit is an unreachable commit as well. To expand your example scenario, let’s say you commit thrice, and then you do `git reset HEAD~3` to throw away all of the commits you just made. Now you have three new unreachable commits (you cannot get to them from any branch or tag or other ref), the last one of which is dangling (you cannot even get to it from another unreachable commit). – Aristotle Pagaltzis Sep 22 '18 at 08:30
  • @AristotlePagaltzis re: **reflogs for dangling commits are not a thing**, this isn't strictly true. `git fsck --dangling` may not show a dangling commit that has a reflog entry, but `git fsck --lost-found` will. You may test this by creating a branch, making a commit, deleting the branch, running `git reflog` and then `git fsck --lost-found`. You will see `dangling commit: ` for a commit that has a reflog entry (as well as, now, an entry in `.git/lost-found/commit`) – De Novo Feb 22 '19 at 18:40
  • Then `git fsck --lost-found` misuses the term. Or at least uses it in a way that contradicts its definition in `git help glossary`. – Aristotle Pagaltzis Jun 08 '19 at 02:36
  • I did all these git reflog expire --expire-unreachable=now --all git gc --prune=now git fsck --unreachable --no-reflogs # no output git branch -a --contains # no output git show # still shows up i checked for branch --contains, tag --contains, tag --points-to, but nothing shows up except for "git show " The reflog is empty, lost+found is also empty. Any idea what else can be checked ? – Senthil A Kumar Nov 26 '19 at 10:26
  • @SenthilAKumar: the stash, [as in Leo Zhao’s answer](/a/46356540/9410)? It’s also conceivable that you have alternates set up (which won’t be affected by any cleanup performed within your own repository). – Aristotle Pagaltzis Jan 11 '20 at 17:48
25

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.

jakub.g
  • 30,051
  • 7
  • 78
  • 118
  • There's already an answer about tags (https://stackoverflow.com/a/37335660/450127), and it doesn't seem like this adds anything new. Shouldn't this be removed in favor of the earlier answer? – Ian Dunn Oct 10 '17 at 18:31
  • Indeed, somehow I overlooked that answer. Though 4 people found my answer helpful, so maybe it's not that useless? Also I grouped all possibilities into one concise answer. – jakub.g Oct 10 '17 at 21:00
  • 1
    Even if duplicated, this page may appear in Google Result, and it immediately helps people with the same issue, better than just redirecting people over and over again to links that *may* have the correct answer. – Alexandre T. Apr 12 '19 at 14:20
  • In my case, all solutions were partial. The missing part was exactly the tags. – olyk Sep 16 '20 at 23:33
14
git branch --contains 793db7f272ba4bbdd1e32f14410a52a412667042

probably just needs to be

git branch -a --contains 793db7f272ba4bbdd1e32f14410a52a412667042

to also report on branches from remotes

sehe
  • 328,274
  • 43
  • 416
  • 565
  • 1
    thanks, now I found my remotes/origin/next that still holds this commit. how to remove it? `git push -d origin next` doesn't help. – iRaS Apr 11 '18 at 20:17
  • 1
    @iRaS https://stackoverflow.com/questions/2003505/how-do-i-delete-a-git-branch-both-locally-and-remotely – sehe Apr 11 '18 at 20:24
  • thanks - the `git fetch --prune` did the trick. but in all answers I'm missing a check for tags that are referencing this commit. I still don't know how to check for tags with a commit (I removed all). – iRaS Apr 13 '18 at 06:22
  • But ... does this mean that commits that are only reachable from *remote* branches (and no local branches) are considered reachable, and therefore `git fsck --unreachable` is actually communicating over the network with the remote in order to find out which commits are reachable? – LarsH Apr 25 '18 at 20:38
  • 1
    Answered my own question... yes, commits that are only reachable from remote branches (and no local branches) are considered reachable; but `git fsck --unreachable` doesn't need to communicate over the network with the remote in order to find out which remote branches contain which commits. The remote branch info is stored locally, under e.g. `.git/refs/remotes/origin` (or in `packed-refs`). – LarsH Apr 25 '18 at 21:20
8

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.

Andrew Larsson
  • 790
  • 1
  • 17
  • 23
8

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
Lei Zhao
  • 716
  • 8
  • 9
2

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.

dublev
  • 3,767
  • 4
  • 15
  • 12