5

I have a small repo that has a couple of commits:

* a0fc4f8 (HEAD -> testbranch) added file.txt  
* e6e6a8b (master) hello world now  
* f308f53 Made it echo  
* f705657 Added hello  
* 08a2de3 (tag: initial) initial  

Also:

$ git status  
On branch testbranch  
nothing to commit, working directory clean  

I can not understand the following behavior. On this state I run: $ git reset initial
I see now:

* e6e6a8b (master) hello world now  
* f308f53 Made it echo  
* f705657 Added hello  
* 08a2de3 (HEAD -> testbranch, tag: initial) initial  

What I was expecting: Commit a0fc4f8 would be deleted since it is unreachable.
What happened:
1) Doing git show a0fc4f8 still shows the commit
2) Doing git status shows the file.txt that was added by commit a0fc4f8 as untracked and file hello that was added by commit f705657 also shows up as untracked.
3) Running git gc or git gc --prune=all does not delete a0fc4f8 although it is not reachable anymore and has no name/tag associated with it.
Why are these happening?

Update:

$ git fsck  
Checking object directories: 100% (256/256), done.  
Checking objects: 100% (15/15), done.    

Update 2:

$ git log --all --decorate --graph --oneline  
* e6e6a8b (master) hello world now  
* f308f53 Made it echo  
* f705657 Added hello  
* 08a2de3 (HEAD -> testbranch, tag: initial) initial  

$ git gc --force  
Counting objects: 15, done.  
Delta compression using up to 4 threads.  
Compressing objects: 100% (8/8), done.  
Writing objects: 100% (15/15), done.   
Total 15 (delta 1), reused 15 (delta 1)   

$ git log --all --decorate --graph --oneline  
* e6e6a8b (master) hello world now  
* f308f53 Made it echo  
* f705657 Added hello  
* 08a2de3 (HEAD -> testbranch, tag: initial) initial  

$ git show a0fc4f8 Still shows the commit

Update 3:

$ git reflog testbranch  
08a2de3 testbranch@{0}: reset: moving to initial  
a0fc4f8 testbranch@{1}: commit: added file.txt  
e6e6a8b testbranch@{2}: branch: Created from HEAD  
Jim
  • 18,826
  • 34
  • 135
  • 254

1 Answers1

7

1) Doing git show a0fc4f8 still shows the commit

This is by design. The unreachable objects are not removed immediately for several reasons:

  • maybe you ran the last command by mistake (or provided wrong arguments to it), you realize the error and want to go back to the previous state;
  • the gain of removing an unreachable object (saving some amount of disk space) is too small compared to the amount of work required to complete the action.

Pruning the unreachable object is performed automatically from time to time. It is also performed by some git commands (fetch and push are some of them).

2) Doing git status shows the file.txt that was added by commit a0fc4f8 as untracked and file hello that was added by commit f705657 also shows up as untracked.

You ran git reset without specifying a mode. The default mode is --mixed and that means:

  • the branch is moved to the commit specified in the command (initial in this case);
  • the index is reset to match the new commit pointed by the branch;
  • the working tree is not modified.

This explains why the files are in the directory (the third bullet) and why they are untracked (the second bullet; the index matches the initial commit but these files didn't even exist when it was created).

3) Running git gc or git gc --prune=all does not delete a0fc4f8 although it is not reachable anymore and has no name/tag associated with it.

git gc also checks the branch reflogs for references. If your testbranch branch has the reflog enabled then the most recent entry in the reflog points to commit a0fc4f8 (this is where the testbranch branch was before you ran git reset). You can check if the reflog is enabled for branch testbranch by running git reflog testbranch. If it prints something you'll find the commit a0fc4f8 on the second line, at position testbranch@{1}. The notation name@{n} means the prior nth value of branch name (the commit it was pointing to, n moves in the past).

You can find more about the way git gc works in the documentation.

In the Notes section it reads:

git gc tries very hard to be safe about the garbage it collects. In particular, it will keep not only objects referenced by your current set of branches and tags, but also objects referenced by the index, remote-tracking branches, refs saved by git filter-branch in refs/original/, or reflogs (which may reference commits in branches that were later amended or rewound).

If you are expecting some objects to be collected and they aren’t, check all of those locations and decide whether it makes sense in your case to remove those references.

axiac
  • 68,258
  • 9
  • 99
  • 134
  • I see `a0fc4f8` in the reflog but it is not the most recent entry is it? Please see updated OP – Jim Dec 29 '16 at 22:02
  • You are right, it's the second entry. The first entry (`testbranch@{0}`, the most recent) is the current position of the branch. [`testbranch@{1}`](https://git-scm.com/docs/gitrevisions#gitrevisions-emltrefnamegtltngtemegemmaster1em) is its previous position (i.e. where it was before `git reset` moved it.) – axiac Dec 29 '16 at 22:40
  • 1
    So if reflog has a reference to something git gc ignores it? But then this means that git gc would never delete the commit?Won't that commit always be part of the reflog history? – Jim Dec 29 '16 at 22:56
  • 1
    There is a configurable expiration threshold (by default it's 30 days) for unreachable commits that are referred in the reflog. The expired commits are pruned from the database and the reflogs that refer them are updated. Read the docs (on https://git-scm.com/docs/git-gc or `git help gc` in your console), the config entries are described there. – axiac Dec 29 '16 at 23:05
  • 1
    Besides reflog entries (which keep objects alive until the reflog entry itself is deleted), *all* objects get a grace period, 14 days by default, before they will be pruned. The grace period is designed to allow various commands to write temporary objects and then make them permanent: as long as they finish within two weeks of when they start, they work. This is the thing you (@Jim, that is) are overriding with `--prune=all`. If you expire your reflog you can get the object to go away. – torek Dec 30 '16 at 01:22