Oops. You messed up with rebase
and have lost a days work. Or maybe
you live in fear of that happening, so you avoid using destructive
operations such as rebase
and reset
altogether!
Reference Log
Your “Reference Log”, i.e. reflog, is a recording of every time a ref
was updated in your local repository. A ref, or reference, can be
thought of as a symbolic link to a git SHA (the commit hash). Examples
of these are branch names, tags, HEAD
, etc.
So this means every time you do something that changes your HEAD
, a
record is created of that in your reflog
. Switch to a branch? Rebase?
Reset? All of these operations will result in a new log entry! See for
yourself, run git reflog
in one of your projects and see what it shows
you.
Garbage Collection
The second important system of git we depend on for recovery is the garbage collector. When you execute a “destructive” operation in git, AFAIK it’s never actually deleting commits locally. It’s creating new ones, and changing your refs to point at them. The old ones are still around, albeit in a “detached” state since there are no longer refs pointing to them, save for the reflog. Therefore, you need the SHA (commit hash) in order to interact with them.
git
can have this property without wasting a bunch of excess space due
to its use of garbage collection. When you run a git command,
occasionally git will decide the repository needs to be cleaned up, and
will run the garbage collector. It has many jobs, but two are of
particular interest for this article:
- Run
git reflog expire
, which by default will expire all reflog entries older than 90 days, but can be configured withgc.reflogExpire
- Find any commits that cannot be reached via branches, tags, the reflog, etc, and delete them
Recovery
Understanding how the reflog
and garbage collection interact, we know
how to reach old commits that have been “destroyed” by operations like
rebase or reset. This only works if we have commits to go back to
though. Knowing this, I like to make a habit of committing frequently,
and will often have a single commit with a WIP1 message at my
HEAD
, and continually amend this commit as I work with:
$ git commit --amend --no-edit
When I’m confident I have a functional unit of work2, I will amend
it without the --no-edit
flag, give it a nice message, and get to work
on the next piece. Sometimes I end up with changes in HEAD
that belong
in the next commit, so I’ll:
$ git reset @^
to “destroy” my WIP commit, with the contents ending up unstaged in my index$ git add -N <files>
if there are any untracked files I want included$ git add -p
to interactively choose which hunks to stage$ git commit
And get to work on the next piece, with my previous changes now unstaged in my index. Understanding how these systems in git work enables us to work fearlessly, and strive for better commits.
As always, please read the manpages:
$ man git-reflog
$ man git-gc