8

Where can I get informatin about how git stashing works internally? I'm interested in detailed explanation similiar to the one about git objects in the 9.2 Git Internals - Git Objects of git-scm book.

EDIT: I'm updating my question based on the information that I received from that link. Is my logic described below correct?
HEAD is on the branch br1. The last commit 'br1-c0' on that branch had the following tree:

somefile.txt (text inside - 'some text') anotherfile.txt

I've modified somefile.txt to have 'updated text' as text inside. I'm stashing changes:

1) one commit is created, which has the following tree: somefile.txt (text inside - 'updated text') anotherfile.txt AND has link to commit 'br1-c0' and state of file index.

2) the working tree is reverted to 'br1-c0' commit.

Max Koretskyi
  • 101,079
  • 60
  • 333
  • 488
  • 2
    [git help stash, section "discussion"](https://www.kernel.org/pub/software/scm/git/docs/git-stash.html#_discussion) – knittl Aug 30 '13 at 07:24
  • @knittl: There is more to it then that - the stashes are a separate branch and the storage of stashes is reflog. – Maciej Piechotka Aug 30 '13 at 07:39
  • 1
    @MaciejPiechotka: no, the stashes are not a separate branch. They live in their own namespace refs/stash. The reflog has nothing to do with stashes but logs all head updates of branches and HEAD. The only interesting bit about stashes is the fact that it has two parents (original HEAD and index), the stash commit itself contains the (modified) working tree. – knittl Aug 30 '13 at 07:45
  • @knittl: I guess it depends how you define the branch (it is not a local branch as it does not live in `.refs/heads/` but remote branches are also in `.refs/remotes`) - they are implemented as branch. Regarding the stashes - even the syntax displayed by `git stash list` is the same syntax as for reflog so the lookup is implemented as reflog. So while technically it might be not a branch it is implemented as one. – Maciej Piechotka Aug 30 '13 at 07:53
  • BTW `git shash drop` is implemented as `git reflog delete --updateref --rewrite "${REV}"` according to my copy of `/usr/libexec/git-core/git-stash`. – Maciej Piechotka Aug 30 '13 at 07:55

2 Answers2

5

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 2881ede55d619570a82bb7312257c4e43bd3b334
commit 2881ede55d619570a82bb7312257c4e43bd3b334
Author: Maciej Piechotka <uzytkownik2@gmail.com>
Date:   Fri Aug 30 09:27:10 2013 +0200

    index on master: dafe337 sss

diff --git a/test.c b/test.c
index b9a1dd0..7beafd5 100644
--- a/test.c
+++ b/test.c
@@ -1 +1,2 @@
 dddd
+fff
% git show 7f86a90fb4e57590d6fe5026b7408306a757132a
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

diff --cc test.c
index 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 | cat
7f86a90 stash@{0}: WIP on master: dafe337 sss
% cat .git/refs/stash
0dee308c461955e13a864c9a904a69d611e82730
% cat .git/logs/refs/stash
0000000000000000000000000000000000000000 7f86a90fb4e57590d6fe5026b7408306a757132a Maciej Piechotka <uzytkownik2@gmail.com> 1377847630 +0200 WIP on master: dafe337 sss
7f86a90fb4e57590d6fe5026b7408306a757132a 0dee308c461955e13a864c9a904a69d611e82730 Maciej Piechotka <uzytkownik2@gmail.com> 1377847983 +0200 WIP on master: dafe337 sss
David Neiss
  • 8,161
  • 2
  • 20
  • 21
Maciej Piechotka
  • 7,028
  • 6
  • 39
  • 61
  • can you please check my understanding as I layed it out in the edit of my question? – Max Koretskyi Aug 30 '13 at 08:45
  • 2
    @Maximus: Closely - two commits are created. One is the tree with index (those changed you `git-add`ed) `I` which refers to the `br1-c0` (`H`) and one is the stash (`W`) itself which have tree as is the stated of tracked files at the time of making the commit referring both to `H` and `I`. The `ref/stash` is updated to point to commit `W` and `br1` is reset to `H`. If I read your edit correctly you omitted the `I`. In my example `H` is dafe3, `I` is 2881e and `W` 7f86a. If `--keep-index` is not passed the `W` commit have the same tree as `I`. – Maciej Piechotka Aug 30 '13 at 09:04
  • Thanks! I'll think it through and come back if I don't understand anything :). – Max Koretskyi Aug 30 '13 at 09:14
  • so if two commits are created pointing to Index tree and Working tree, does it mean I can apply stash to restore my tree to Index versioned or to Working version? I thought that stash apply restores tree to the version of Working tree always. – Max Koretskyi Sep 01 '13 at 16:50
  • 1
    @Maximus: If you use `--keep-index` it will restore the working tree to saved working tree and index to index. If you don't use `--keep-index` it will set the working tree and index to the state of working tree. – Maciej Piechotka Sep 01 '13 at 18:09
  • Thanks! I'm also curios whether I can apply stash on any branch, not specifically the one I created it on? If so, will it try to merge these changes if conflicts arise? – Max Koretskyi Sep 02 '13 at 06:01
  • 1
    @Maximus: Yes. It will. However it might be quicker to just try it then ask here and wait 13h. – Maciej Piechotka Sep 02 '13 at 19:15
2

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 status

On branch master
Changes to be committed:
    (use "git restore --staged <file>..." to unstage)
                modified:   CONTRIBUTING.md

Changes 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.md

Untracked 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 the Index.(CONTRIBUTING.md)
  • Changes not staged for commit denotes the tracked files that are modified 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 on which git stash command is used.

Git Stash Command Modified Tracked File (Working Dir) Index Untracked File Ignored
README.md CONTRIBUTING.md LICENSE temp/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 ignored like respectively LICENSE and temp/stash.out .

git status

On branch master
Your branch is up-to-date with 'origin/master'.
Untracked files:
    (use "git add <file>..." to include in what will be committed)

    LICENSE

nothing 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 status

On branch master
Your 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 49482afa4ab999deada67c65dc5d38be89aed867
tree 936c8b08ac5a8e91bb6cc38387d2cca93167e0ae
parent 031ca106c13b1603675ea1ce8da8b3da852e27cd
parent b558b9e7621fe508c7c18713cd62c78e80e2017e
parent dfac0d769262fa4b8ea40003d24052c4509a7f3a
author Eric Bouchut <ebouchut@gmail.com> 1627056522 +0200
committer 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 by 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 merge commit, 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 log displays non requested information (commit SHA1, date, and author) in addition to 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 Commit

The command below **lists** the **staged files** of the most recent stash commit.

```lang-shell
git log  --name-only -1 --format='' 'stash@{0}^2'

In order to get the content of the (changesets in the) Index in 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.32 onwards git show now has the --only-untracked option to list the untracked files 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'
Eric Bouchut
  • 59
  • 1
  • 6