Chapter 11. The Stash and the Reflog

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 <[email protected]>
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 <[email protected]>
    Date:   Sat Oct 29 09:55:52 2011 -0500

    More foo and bar

    commit 8a04ca4207e1cb74dd3a3e261d6be72e118ace9e
    Author: Jon Loeliger <[email protected]>
    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 <[email protected]>
    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.!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.12.71.237