how does stashing work in git - internals how does stashing work in git - internals git git

how does stashing work in git - internals


Git is open source so source code ;) (or google)

In any case the stashes are list of commits. You can see how they are constructed by creating a stash:

 # git stash --keep-index # git stash list stash@{0}: WIP on master: dafe337 sss # git log 'stash@{0}' | cat commit 7f86a90fb4e57590d6fe5026b7408306a757132a Merge: dafe337 2881ede Author: Maciej Piechotka <uzytkownik2@gmail.com> Date:   Fri Aug 30 09:27:10 2013 +0200     WIP on master: dafe337 sss commit 2881ede55d619570a82bb7312257c4e43bd3b334 Author: Maciej Piechotka <uzytkownik2@gmail.com> Date:   Fri Aug 30 09:27:10 2013 +0200     index on master: dafe337 sss commit dafe33716c2e5aee994612c88d8142f1163c624e Author: Maciej Piechotka <uzytkownik2@gmail.com> Date:   Fri Aug 30 09:25:40 2013 +0200     sss

Sss is first commit (HEAD) while the rest two commits is the save of current index (staged changes) and the merge contains unstaged changes:

% git show 2881ede55d619570a82bb7312257c4e43bd3b334commit 2881ede55d619570a82bb7312257c4e43bd3b334Author: Maciej Piechotka <uzytkownik2@gmail.com>Date:   Fri Aug 30 09:27:10 2013 +0200    index on master: dafe337 sssdiff --git a/test.c b/test.cindex b9a1dd0..7beafd5 100644--- a/test.c+++ b/test.c@@ -1 +1,2 @@ dddd+fff% git show 7f86a90fb4e57590d6fe5026b7408306a757132acommit 7f86a90fb4e57590d6fe5026b7408306a757132aMerge: dafe337 2881edeAuthor: Maciej Piechotka <uzytkownik2@gmail.com>Date:   Fri Aug 30 09:27:10 2013 +0200    WIP on master: dafe337 sssdiff --cc test.cindex b9a1dd0,7beafd5..551a609--- a/test.c+++ b/test.c@@@ -1,1 -1,2 +1,3 @@@  dddd+ fff++ggg

Now the list of stashes is an existing structure - reflog (n.b. useful structure on its own) and the name is... stash. So stashes are implemented de-facto as a branch with moving head and what we are interested in is reflog. To make it more interesting I created a second stash which created commit 0dee308c461955e13a864c9a904a69d611e82730.

% git reflog stash | cat7f86a90 stash@{0}: WIP on master: dafe337 sss% cat .git/refs/stash0dee308c461955e13a864c9a904a69d611e82730% cat .git/logs/refs/stash0000000000000000000000000000000000000000 7f86a90fb4e57590d6fe5026b7408306a757132a Maciej Piechotka <uzytkownik2@gmail.com> 1377847630 +0200 WIP on master: dafe337 sss7f86a90fb4e57590d6fe5026b7408306a757132a 0dee308c461955e13a864c9a904a69d611e82730 Maciej Piechotka <uzytkownik2@gmail.com> 1377847983 +0200 WIP on master: dafe337 sss


I wrote a blog post Git Stash Internals with my understanding of this topic.
I share the gist of it below, hoping this may help you and others.Feedback is welcome.

git statusOn branch masterChanges to be committed:    (use "git restore --staged <file>..." to unstage)                modified:   CONTRIBUTING.mdChanges not staged for commit:    (use "git add <file>..." to update what will be committed)    (use "git restore <file>..." to discard changes in working directory)                modified:   README.mdUntracked files:    (use "git add <file>..." to include in what will be committed)                LICENSE

There are 3 sections:

  • Changes to be committed denotes the content of theIndex.(CONTRIBUTING.md)
  • Changes not staged for commit denotes the tracked files that aremodified in the Working Dir.(README.md)
  • Untracked files denotes the files git does not know about yet (LICENSE)

In this example, I assume that there is only one stash created and I use stash@{0} to reference it.

The table below describes what files are stashed and where (commit) depending onwhich git stash command is used.

Git Stash CommandModified Tracked File (Working Dir)IndexUntracked FileIgnored
README.mdCONTRIBUTING.mdLICENSEtemp/stash.out
git stash☑️ stash@{0}☑️ stash@{0}^2⤬ 
git stash -u☑️ stash@{0}☑️ stash@{0}^2☑️ stash@{0}^3
git stash -a☑️ stash@{0}☑️ stash@{0}^2☑️ stash@{0}^3☑️ stash@{0}^3

Now, let's take a look at what each git stash command does.

git stash

By default, git stash sets aside:

  • any tracked file that is modified and not ignored: README.md
  • the Index: CONTRIBUTING.md

It does not set aside files that are either untracked or ignoredlike respectively LICENSE and temp/stash.out .

git statusOn branch masterYour branch is up-to-date with 'origin/master'.Untracked files:    (use "git add <file>..." to include in what will be committed)    LICENSEnothing added to commit but untracked files present (use "git add" to track)

If instead, I want to stash both tracked and untracked files,that is all my current work in progress (both README.md and LICENSE)plus the content of the index, (CONTRIBUTING.md), then I need to use git stash -u, like so:

git stash -u
git statusOn branch masterYour branch is up-to-date with 'origin/master'.nothing to commit, working directory clean

To also stash the ignored files, you can use git stash -a instead.

Stash Stack

When adding a stash, git creates a stash commit, pushes it on top of the stash stack.This shifts existing stash entries downwards (if any).The reference stash@{0} always denotes the top of the stash stack.Each time you stash something else it is pushed downwards, hence:

  • stash@{0} denotes the most recent stash created,
  • stash@{1} denotes the second to last stash created,
  • stash@{2} denotes the third to last stash created, and so on.

Now that we know what is stashed, let's take a look at the way it is stored internally.

Now that we have set the scene, let's take at a look at how things are working under the hood.

What is in a stash?

Let's figure out what is in our most recent stash.
This section assumes we ran git stash -u in the example's repository so we end up with this log.

git stash - log

Now let's take a look at the stash@{0} commit.

git log --format=raw -1 stash@{0}commit 49482afa4ab999deada67c65dc5d38be89aed867tree 936c8b08ac5a8e91bb6cc38387d2cca93167e0aeparent 031ca106c13b1603675ea1ce8da8b3da852e27cdparent b558b9e7621fe508c7c18713cd62c78e80e2017eparent dfac0d769262fa4b8ea40003d24052c4509a7f3aauthor Eric Bouchut <ebouchut@gmail.com> 1627056522 +0200committer Eric Bouchut <ebouchut@gmail.com> 1627056522 +0200    WIP on master: 031ca10 Add README

Do note that the parents of the stash commit are listed in order (first (031ca10), then second (b558b9e) then third (dfac0d7)).

git stash commit parents

The stash commit stash@{0} (49482a) is a merge commit with 3 parents in this case because we stashed the untracked files, (2 parents y default).
It also contains the non ignored files of the working dir that were modified at the time of the stash.

Let's meet the parents:

  • stash@{0}^1 (031ca10) denotes the first parent of the stash commit.
    This was the current commit (HEAD) at the time of the stash.
  • stash@{0}^2 (b558b9e) denotes the second parent of the stash commit.
    It contains the changesets present in the Index at the time of the stash.
    The Index is aka. as the staging area. This is where the files you add with git add are stored before they can be committed.
  • stash@{0}^3 (dfac0d7) denotes the third parent of the stash commit.
    It contains the untracked files (-u) and ignored files (-a)present in the working tree at the time of the stash.
    git stash creates it only when you use any of the -u or -a options.

Why do we need to dive deep into the inner workings of git stash?

Up until version 2.32, git did not offer a simple way to list and show the untracked files in a stash commit.This is why we need to know the git stash internals to do this.You are now ready to understand what is next.

Modified Files in the Working Dir of a Stash Commit

Here is how to list modified files in the Working Dir of the most recent stash commit:

 git log -m --first-parent -1  --format='' --name-only 'stash@{0}'

Here we drill down on the merge commit (-m) and focus only on the first commit(-1) of the first parent (--first-parent), that is the stash commit itself.

ℹ️ By default, git log does not display details about any parent of a mergecommit, unless we use -m and when we do use this option, it displays what is requested for each and every parent. As this is not what we want here, we restrict only to the first parent.

For whatever reason, even with --name-only, git logdisplays non requested information (commit SHA1, date, and author) in additionto the file names. I noticed this issue in git version 2.32.0.This is why I use --format='' as a workaround to remove them.

Now, here is how to view what changed in the modified files of the Working Dir of the most recent stash commit:

git log -m --first-parent -1   -p 'stash@{0}'# Stashed Files of a Stash CommitThe command below **lists** the **staged files** of the most recent stash commit.```lang-shellgit log  --name-only -1 --format='' 'stash@{0}^2'

In order to get the content of the (changesets in the) Indexin this stash commit:

git log  -1  -p 'stash@{0}^2'

Untracked Files of a Stash Commit

Here is how to list the untracked files in the most recent stash commit.

From git version 2.32onwards git show now has the --only-untracked option to list the untrackedfiles of a stash.

ℹ️This also lists the ignored files if you used git stash -a to also stash the ignored files.

git stash show --only-untracked --name-only 'stash@{0}'

Before git version 2.32, we should have used instead one of the following 2 alternatives:

git show --name-only 'stash@{0}^3:'

Please note the colon sign (:) at the end.

git ls-tree -r 'stash@{0}^3' --name-only

Here is how to view the content of the untracked files (and ignored file(s)if any) in the most recent stash commit.

From git version 2.32 onwards you can use the --only-untracked option of git show.

git stash show --only-untracked -p 'stash@{0}'

Before git version 2.32, use instead:

git log -p 'stash@{0}^3'