Git – The Stash and the Reflog

How to Create a Droplet with DigitalOcean

The Stash

Do you ever feel overwhelmed in your daily development cycle
when the constant interruptions, demands for bug fixes, and requests from
coworkers or managers all pile up and clutter the
real work you are trying to do? If so, the
stash was designed to help you!

The stash is a mechanism for capturing your work in
progress, allowing you to save it and return to it later when convenient.
Sure, you can already do that using the existing branch and commit
mechanisms within Git, but the stash is a quick convenience mechanism that
allows a complete and thorough capturing of your index and working
directory in one simple command. It leaves your repository clean,
uncluttered, and ready for an alternate development direction. Another
single command restores that index and working directory state completely,
allowing you to resume where you left off.

Let’s see how the stash works with the canonical use case:
the so-called interrupted work flow.

In this scenario, you are happily working in your Git repository and
have changed several files and maybe even staged a few in the index. Then,
some interruption happens. Perhaps a critical bug is discovered and lands
on your plate and must be fixed immediately. Perhaps your team lead has
suddenly prioritized a new feature over everything else and insists you
drop everything to work on it. Whatever the circumstance, you realize you
must stash everything, clean your slate and work tree, and start afresh.
This is a perfect opportunity for git
stash
!

    $ cd the-git-project
    # edit a lot, in the middle of something

    # High-Priority Work-flow Interrupt!
    # Must drop everything and do Something Else now!

    $ git stash save

    # edit high-priority change
    $ git commit -a -m "Fixed High-Priority issue"

    $ git stash pop

And resume where you were!

The default and optional operation to git stash is save. Git also supplies a default log message
when saving a stash, but you can supply your own to better remind you what
you were doing. Just supply it in the command after the then-required
save argument:

    $ git stash save "WIP: Doing real work on my stuff"

The acronym WIP is a
common abbreviation used in these situations meaning work in
progress.

To achieve the same effect with other, more basic Git commands
requires manual creation of a new branch on which you commit all of your
modifications, re-establishing your previous branch to continue your work,
and then later recovering your saved branch state on top of your new
working directory. For the curious, that process is roughly this
sequence:

    # ... normal development process interrupted ...

    # Create new branch on which current state is stored.
    $ git checkout -b saved_state
    $ git commit -a -m "Saved state"

    # Back to previous branch for immediate update.
    $ git checkout master

    # edit emergency fix
    $ git commit -a -m "Fix something."

    # Recover saved state on top of working directory.
    $ git checkout saved_state
    $ git reset --soft HEAD^

    # ... resume working where we left off above ...

That process is sensitive to completeness and attention to detail.
All of your changes have to be captured when you save your state, and the
restoration process can be disrupted if you forget to move your HEAD back as well.

The git stash save
command will save your current index and working directory state and clear
them out so that they again match the head of your current branch.
Although this operation gives the appearance that your modified files and
any files updated into the index using, for example, git add or git
rm
, have been lost, they have not. Instead, the contents of your
index and working directory are actually stored as independent, regular
commits and are accessible through the ref refs/stash.

    $ git show-branch stash
    [stash] WIP on master: 3889def Some initial files.

As you might surmise by the use of pop to restore your state, the two basic
stash commands, git stash save and git
stash pop
, implement a stack of stash states. That allows your
interrupted work flow to be interrupted yet again! Each stashed context on
the stack can be managed independently of your regular commit
process.

The git stash pop command
restores the context saved by a previous save operation on top of your current working
directory and index. And by restore here, I mean that the pop operation
takes the stash content and merges those changes into
the current state rather than just overwriting or replacing files. Nice,
huh?

You can only git stash pop into a clean working
directory. Even then, the command may or may not fully succeed in
recreating the full state you originally had at the time it was saved.
Because the application of the saved context can be performed on top of a
different commit, merging may be required, complete with possible user
resolution of any conflicts.

After a successful pop
operation, Git will automatically remove your saved state from the stack
of saved states. That is, once applied, the stash state will be
dropped. However, when conflict resolution is needed, Git
will not automatically drop the state, just in case you want to try a
different approach or want to restore it onto a different commit. Once you
clear the merge conflicts and want to proceed, you should use the git stash drop to remove it from the stash
stack. Otherwise, Git will maintain an ever growing[23] stack of contexts.

If you just want to recreate the context you have saved in a stash
state without dropping it from the stack, use git
stash apply
. Thus, a pop
command is a successful apply followed
by a drop.

Tip

In fact, you can use git stash
apply
to apply the same saved stashed context onto several
different commits prior to dropping it from the stack.

However, you should consider carefully if you want to use git stash apply or git
stash pop
to regain the contents of a stash. Will you ever need
it again? If not, pop it. Clean the stashed content and referents out of
your object store.

The git stash list
command lists the stack of saved contexts from most to least
recent.

    $ cd my-repo
    $ ls
    file1  file2

    $ echo "some foo" >> file1

    $ git status
    # On branch master
    # Changes not staged for commit:
    #   (use "git add <file>..." to update what will be committed)
    #   (use "git checkout -- <file>..." to discard changes in working directory)
    #
    #    modified:   file1
    #
    no changes added to commit (use "git add" and/or "git commit -a")

    $ git stash save "Tinkered file1"
    Saved working directory and index state On master: Tinkered file1
    HEAD is now at 3889def Add some files

    $ git commit --dry-run
    # On branch master
    nothing to commit (working directory clean)

    $ echo "some bar" >> file2

    $ git stash save "Messed with file2"
    Saved working directory and index state On master: Messed with file2
    HEAD is now at 3889def Add some files

    $ git stash list
    stash@{0}: On master: Messed with file2
    stash@{1}: On master: Tinkered file1

Git always numbers the stash entries with the most recent
entry being zero. As entries get older, they increase in numerical order.
And yes, the different stash entry names are stash@{0} and stash@{1}, as explained in The Reflog.

The git stash show
command shows the index and file changes recorded for a given stash entry,
relative to its parent commit.

    $ git stash show
     file2 |    1 +
     1 files changed, 1 insertions(+), 0 deletions(-)

That summary may or may not be the extent of the information you
sought. If not, adding -p to see the diffs might be more
useful. Note that by default the git stash
show
command shows the most recent stash entry, stash@{0}.

Because the changes that contribute to making a stash state
are relative to a particular commit, showing the state is a state-to-state
comparison suitable for git diff,
rather than a sequence of commit states suitable for git log. Thus, all the options for git diff may also be supplied to git stash show as well. As we saw previously,
--stat is the
default, but other options are valid, too. Here, -p is
used to obtain the patch differences for a given stash state.

    $ git stash show -p stash@{1}
    diff --git a/file1 b/file1
    index 257cc56..f9e62e5 100644
    --- a/file1
    +++ b/file1
    @@ -1 +1,2 @@
     foo
    +some foo

Another classic use case for git
stash
is the so-called pull into a dirty tree
scenario.

Until you are familiar with the use of remote repositories and
pulling changes (see Getting Repository Updates), this
might not make sense yet. But it goes like this. You’re developing in your
local repository and have made several commits. You still have some
modified files that haven’t been committed yet, but you realize there are
upstream changes that you want. If you have conflicting modifications, a
simple git pull will fail, refusing to
overwrite your local changes. One quick way to work around this problem
uses git stash.

    $ git pull
    # ... pull fails due to merge conflicts ...

    $ git stash save
    $ git pull
    $ git stash pop

At this point you may or may not need to resolve conflicts created
by the pop.

In case you have new, uncommitted (and hence
untracked) files as part of your local development, it is
possible that a git pull that would
also introduce a file of the same name might fail, thus not wanting to
overwrite your version of the new file. In this case, add the
--include-untracked option on your git stash so that it also
stashes your new, untracked files along with the rest of your
modifications. That will ensure a completely clean working directory for
the pull.

The --all option will gather up the
untracked files as well as the explicitly ignored files from the .gitignore and exclude files.

Finally, for more complex stashing operations where you wish to
selectively choose which hunks should be stashed, use the
-p or --patch option.

In another similar scenario, git
stash
can be used when you want to move modified work out of the
way, enabling a clean pull –rebase.
This would happen typically just prior to pushing your local commits
upstream.

    # ... edit and commit ...
    # ... more editing and working...

    $ git commit --dry-run
    # On branch master
    # Your branch is ahead of 'origin/master' by 2 commits.
    #
    # Changed but not updated:
    #   (use "git add <file>..." to update what will be committed)
    #   (use "git checkout -- <file>..." to discard changes in working directory)
    #
    #       modified:   file1.h
    #       modified:   file1.c
    #
    no changes added to commit (use "git add" and/or "git commit -a")

At this point you may decide the commits you have already made
should go upstream, but you also want to leave the modified files here in
your work directory. However, git refuses to pull:

    $ git pull --rebase
    file1.h: needs update
    file1.c: needs update
    refusing to pull with rebase: your working tree is not up-to-date

This scenario isn’t as contrived as it might seem at first. For
example, I frequently work in a repository where I want to have
modifications to a Makefile, perhaps
to enable debugging, or I need to modify some configuration options for a
build. I don’t want to commit those changes, and I don’t want to lose them
between updates from a remote repository. I just want them to linger here
in my working directory.

Again, this is where git stash
helps:

    $ git stash save
    Saved working directory and index state WIP on master: 5955d14 Some commit log.
    HEAD is now at 5955d14 Some commit log.

    $ git pull --rebase
    remote: Counting objects: 63, done.
    remote: Compressing objects: 100% (43/43), done.
    remote: Total 43 (delta 36), reused 0 (delta 0)
    Unpacking objects: 100% (43/43), done.
    From ssh://git/var/git/my_repo
       871746b..6687d58  master     -> origin/master
    First, rewinding head to replay your work on top of it...
    Applying: A fix for a bug.
    Applying: The fix for something else.

After you pull in upstream commits and rebase your local commits on
top of them, your repository is in good shape to send your work upstream.
If desired, you can readily push them now:

    # Push upstream now if desired!
    $ git push

or after restoring your previous working directory
state:

    $ git stash pop
    Auto-merging file1.h
    # On branch master
    # Your branch is ahead of 'origin/master' by 2 commits.
    #
    # Changed but not updated:
    #   (use "git add <file>..." to update what will be committed)
    #   (use "git checkout -- <file>..." to discard changes in working directory)
    #
    #       modified:   file1.h
    #       modified:   file1.c
    #
    no changes added to commit (use "git add" and/or "git commit -a")
    Dropped refs/stash@{0} (7e2546f5808a95a2e6934fcffb5548651badf00d)

    $ git push

If you decide to git push
after popping your stash, remember that only completed, committed work
will be pushed. There’s no need to worry about pushing your partial,
uncommitted work. There is also no need to worry about pushing your
stashed content: the stash is purely a local notion.

Sometimes stashing your changes leads to a whole sequence of
development on your branch and, ultimately, restoring your stashed state
on top of all those changes may not make direct sense. In addition, merge
conflicts might make popping hard to do. Nonetheless, you may still want
to recover the work you stashed. In situations like this, git offers the
git stash branch command to help you.
This command converts the contents of a saved stash into a new branch
based on the commit that was current at the time the stash entry was
made.

Let’s see how that works on a repository with a bit of history in
it.

    $ git log --pretty=one --abbrev-commit
    d5ef6c9 Some commit.
    efe990c Initial commit.

Now, some files are modified and subsequently stashed:

    $ git stash
    Saved working directory and index state WIP on master: d5ef6c9 Some commit.
    HEAD is now at d5ef6c9 Some commit.

Note that the stash was made against commit d5ef6c9.

Due to other development reasons, more commits are made and the
branch drifts away from the d5ef6c9
state.

    $ git log --pretty=one --abbrev-commit
    2c2af13 Another mod
    1d1e905 Drifting file state.
    d5ef6c9 Some commit.
    efe990c Initial commit.

    $ git show-branch -a
    [master] Another mod

And although the stashed work is available, it doesn’t apply cleanly
to the current master branch.

    $ git stash list
    stash@{0}: WIP on master: d5ef6c9 Some commit.

    $ git stash pop
    Auto-merging foo
    CONFLICT (content): Merge conflict in foo
    Auto-merging bar
    CONFLICT (content): Merge conflict in bar

Say it with me: Ugh.

So reset some state and take a different approach, creating a new
branch called mod that contains the
stashed changes.

    $ git reset --hard master
    HEAD is now at 2c2af13 Another mod

    $ git stash branch mod
    Switched to a new branch 'mod'
    # On branch mod
    # Changes not staged for commit:
    #   (use "git add <file>..." to update what will be committed)
    #   (use "git checkout -- <file>..." to discard changes in working directory)
    #
    #    modified:   bar
    #    modified:   foo
    #
    no changes added to commit (use "git add" and/or "git commit -a")
    Dropped refs/stash@{0} (96e53da61f7e5031ef04d68bf60a34bd4f13bd9f)

There are several important points to notice here. First, notice
that the branch is based on the original commit d5ef6c9, and not the current head commit
2c2af13.

    $ git show-branch -a
    ! [master] Another mod
     * [mod] Some commit.
    --
    +  [master] Another mod
    +  [master^] Drifting file state.
    +* [mod] Some commit.

Second, because the stash is always reconstituted against the
original commit, it will always succeed and hence will be dropped from the
stash stack.

Finally, reconstituting the stash state doesn’t
automatically commit any of your changes onto the new branch. All the
stashed file modifications (and index changes, if desired) are still left
in your working directory on the newly created and checked out
branch.

    $ git commit --dry-run
    # On branch mod
    # Changes not staged for commit:
    #   (use "git add <file>..." to update what will be committed)
    #   (use "git checkout -- <file>..." to discard changes in working directory)
    #
    #    modified:   bar
    #    modified:   foo
    #
    no changes added to commit (use "git add" and/or "git commit -a")

At this point you are of course welcome to commit the changes onto
the new branch, presumably as a precursor to further development or
merging as you deem necessary. No, this isn’t a magic bullet to avoid
resolving merge conflicts. If there were merge conflicts when you tried to
pop the stash directly onto the master
branch earlier, trying to merge the new branch with the
master will yield the same effects and the same merge
conflicts.

    $ git commit -a -m "Stuff from the stash"
    [mod 42c104f] Stuff from the stash
     2 files changed, 2 insertions(+), 0 deletions(-)

    $ git show-branch
    ! [master] Another mod
     * [mod] Stuff from the stash
    --
     * [mod] Stuff from the stash
    +  [master] Another mod
    +  [master^] Drifting file state.
    +* [mod^] Some commit.

    $ git checkout master
    Switched to branch 'master'

    $ git merge mod
    Auto-merging foo
    CONFLICT (content): Merge conflict in foo
    Auto-merging bar
    CONFLICT (content): Merge conflict in bar
    Automatic merge failed; fix conflicts and then commit the result.

As some parting advice on the git
stash
command, let me leave you with this analogy: you name your
pets and you number your livestock. So branches are named and stashes are
numbered. The ability to create stashes might be appealing, but be careful
not to overuse it and create too many stashes. And don’t just convert them
to named branches to make them linger!

The Reflog

OK, I confess: sometimes Git does something either
mysterious or magical and causes one to wonder what just happened.
Sometimes you simply want an answer to the question, Wait, where
was I? What just happened?
Other times, you do some operation and
realize, Uh oh, I shouldn’t have done that! But it is too
late and you have already lost the top commit with a week’s worth of
awesome development.

Not to worry! Git’s reflog has you covered in either case! By using
the reflog, you can gain the assurance that operations happened as you
expected on the branches you intended, and that you have the ability to
recover lost commits just in case something goes astray.

The reflog is a record of changes to
the tips of branches within nonbare repositories. Every time an update is
made to any ref, including HEAD, the
reflog is updated to record how that ref has changed. Think of the reflog
as a trail of bread crumbs showing where you and your refs have been. With
that analogy, you can also use the reflog to follow your trail of crumbs
and trace back through your branch manipulations.

Some of the basic operations that record reflog updates
include:

  • Cloning

  • Pushing

  • Making new commits

  • Changing or creating branches

  • Rebase operations

  • Reset operations

Note that some of the more esoteric and complex operations, such as
git filter-branch, ultimately boil down
to simple commits and are thus also logged. Fundamentally, any Git
operation that modifies a ref or changes the tip of a branch is
recorded.

By default, the reflog is enabled in nonbare repositories
and disabled in bare repositories.
Specifically, the reflog is controlled by the Boolean configuration option
core.logAllRefUpdates. It may be enabled using
the command git config core.logAllRefUpdates true or disabled with
false as desired on a per-repository
basis.

So what does the reflog look like?

$ git reflog show
a44d980 HEAD@{0}: reset: moving to master
79e881c HEAD@{1}: commit: last foo change
a44d980 HEAD@{2}: checkout: moving from master to fred
a44d980 HEAD@{3}: rebase -i (finish): returning to refs/heads/master
a44d980 HEAD@{4}: rebase -i (pick): Tinker bar
a777d4f HEAD@{5}: rebase -i (pick): Modify bar
e3c46b8 HEAD@{6}: rebase -i (squash): More foo and bar with additional stuff.
8a04ca4 HEAD@{7}: rebase -i (squash): updating HEAD
1a4be28 HEAD@{8}: checkout: moving from master to 1a4be28
ed6e906 HEAD@{9}: commit: Tinker bar
6195b3d HEAD@{10}: commit: Squash into 'more foo and bar'
488b893 HEAD@{11}: commit: Modify bar
1a4be28 HEAD@{12}: commit: More foo and bar
8a04ca4 HEAD@{13}: commit (initial): Initial foo and bar.

Although the reflog records transactions for all refs, git reflog show displays the transactions for
only one ref at a time. The previous example shows the default ref,
HEAD. If you recall that branch names
are also refs, you will realize that you can also get the reflog for any
branch as well. From the previous example, we can see that there is also a
branch named fred, so we can display
its changes in another command:

$ git reflog fred
a44d980 fred@{0}: reset: moving to master
79e881c fred@{1}: commit: last foo change
a44d980 fred@{2}: branch: Created from HEAD

Each line records an individual transaction from the history of the
ref, starting with the most recent change and going back in time. The
leftmost column contains the commit ID at the time the change was made.
The entries like HEAD@{7} from the
second column provide convenient names for the commit at each transaction.
Thus, HEAD@{0} is the most recent
entry, HEAD@{1} records where HEAD was just prior to that, etc. The oldest
entry, here HEAD@{13}, is actually the
very first commit in this repository. The rest of each line after the
colon describes what transaction occurred. Finally, for each transaction
there is a time stamp (not shown) recording when the event took place
within your repository.

So what good is all that? Here’s the interesting aspect of
the reflog: each of the sequentially numbered names like HEAD@{1} may be used as symbolic names of
commits for any Git command that takes a commit. For example:

$ git show HEAD@{10}
commit 6195b3dfd30e464ffb9238d89e3d15f2c1dc35b0
Author: Jon Loeliger <jdl@example.com>
Date:   Sat Oct 29 09:57:05 2011 -0500

    Squash into 'more foo and bar'

diff --git a/foo b/foo
index 740fd05..a941931 100644
--- a/foo
+++ b/foo
@@ -1,2 +1 @@
-Foo!
-more foo
+junk

That means that as you go about your development process, recording
commits, moving to different branches, rebasing, and otherwise
manipulating a branch, you can always use the reflog to reference where
the branch was. The name HEAD@{1}
always references the previous commit for the branch, HEAD@{2} names the HEAD commit just prior to that, etc. Keep in
mind, though, that although the history names individual commits,
transactions other than git commit are
present also. Every time you move the tip of your branch to a different
commit, it is logged. Thus, HEAD@{3}
doesn’t necessarily mean the third prior git
commit
operation. More accurately, it means the third prior
visited or referenced commit.

Tip

Botch a git merge and
want try again? Use git reset
HEAD@{1}
. Add --hard if desired.

Git also supports more English-like qualifiers for the part of the
reference within braces. Maybe you aren’t sure exactly how many changes
took place since something happened, but you know you want what it looked
like yesterday or an hour ago.

    $ git log 'HEAD@{last saturday}'
    commit 1a4be2804f7382b2dd399891eef097eb10ddc1eb
    Author: Jon Loeliger <jdl@example.com>
    Date:   Sat Oct 29 09:55:52 2011 -0500

    More foo and bar

    commit 8a04ca4207e1cb74dd3a3e261d6be72e118ace9e
    Author: Jon Loeliger <jdl@example.com>
    Date:   Sat Oct 29 09:55:07 2011 -0500

    Initial foo and bar.

Git supports a fairly wide variety of date-based qualifiers
for refs. These include words like yesterday, noon, midnight, tea,[24] weekdays, month names, A.M. and P.M. indicators, absolute
times or dates, and relative phrases like last
monday
, 1 hour ago, 10 minutes ago, and combinations of these
phrases such as 1 day 2 hours ago. And,
finally, if you omit the actual ref name and just use the @{ } form,
the current branch name is assumed. Thus, while on the bugfix branch, using just @{noon} refers to bugfix@{noon}.

Tip

The Git tool responsible for understanding references is
git rev-parse. Its manpage is
extensive and details more than you would ever care to know about how
refs are interpreted. Good luck!

Although these date-based qualifiers are fairly liberal, they are
not perfect. Understand that Git uses a heuristic to interpret them and
exercise some caution in referring to them. Also remember that the notion
of time is local and relative to your repository: these time-qualified refs reference the value of a ref in your local
repository only. Using the same phrase about time in a different
repository will likely yield different results due to different reflogs.
Thus, master@{2.days.ago} refers to the
state of your local master branch two
days ago. If you don’t have reflog history to cover that time period, Git
should warn you:

    $ git log HEAD@{last-monday}
    warning: Log for 'HEAD' only goes back to Sat, 29 Oct 2011 09:55:07 -0500.
    commit 8a04ca4207e1cb74dd3a3e261d6be72e118ace9e
    Author: Jon Loeliger <jdl@example.com>
    Date:   Sat Oct 29 09:55:07 2011 -0500

    Initial foo and bar.

One last warning. Don’t let the shell trick you. There is a
significant difference between these two commands:

    # Bad!
    $ git log dev@{2 days ago}

    # Likely correct for your shell
    $ git log 'dev@{2 days ago}'

The former, without single quotes, provides multiple command
line arguments to your shell, whereas the latter, with quotes, passes the
entire ref phrase as one command line argument. Git needs to see the ref
as one word from the shell. To help simplify the word break issue, Git
allows several variations:

    # These should all be equivalent
    $ git log 'dev@{2 days ago}'
    $ git log dev@{2.days.ago}
    $ git log dev@{2-days-ago}

One more concern to address. If Git is maintaining a transaction
history of every operation performed on every ref in the repository,
doesn’t the reflog eventually become huge?

Luckily, no. Git automatically runs a garbage collection
process occasionally. During this process, some of the older reflog
entries are expired and dropped. Normally, a commit that is otherwise not
referenced or reachable from some branch or ref will be expired after a
default of 30 days, and commits that are reachable expire after a default
of 90 days.

If that schedule isn’t ideal, the configuration
variables gc.reflogExpireUnreachable
and gc.reflogExpire, respectively, can
be set to alternate values in your repository. You can use the command
git reflog delete to remove individual
entries, or use the command git reflog
expire
to directly cause entries older than a specified time to
be immediately removed. It can also be used to forcefully expire the
reflog.

    $ git reflog expire --expire=now --all
    $ git gc

As you might have guessed by now, the stash and the reflog are
intimately related. In fact, the stash is implemented as a reflog using
the ref stash.

One last implementation detail: reflogs are stored under the
.git/logs directory. The file
.git/logs/HEAD contains the history
of HEAD values, whereas the
subdirectory .git/logs/refs/ contains
the history of all refs, including the stash. The sub-subdirectory
.git/logs/refs/heads contains the
history for branch heads.

All the information stored in the reflogs, specifically everything
under the .git/logs directory, is
ultimately transitory and expendable. Throwing away the .git/logs directory or turning the reflog off
harms no Git-internal data structure; it simply means references like
master@{4} can’t be resolved.

Conversely, having the reflog enabled introduces references to
commits that might otherwise be unreachable. If you are trying to clean up
and shrink your repository size, removing the reflog may enable the
removal of otherwise unreachable (i.e., irrelevant) commits.


[23] Technically, not growing without bounds. The stash is subject to
reflog expiration and garbage collection.

[24] No, really. And yes, that is 5:00 P.M.!

Comments are closed.