QIAOPANG.COM · SELECTED ESSAYS (2013–2026) · 260 ESSAYSBrowse Chronologically (2013 → 2026) ↗
QiaoPang · Programmers LifeSelected Essays · Systems, Craft & Agentic Era
Timeline ↗

What Exactly Does Our Casual `git stash` Do Under the Hood?

What Exactly Does Our Casual `git stash` Do Under the Hood?

git stash is a programmer’s secret weapon. The boss drops a case in your lap (biangbiang) and says it needs a hot fix before end of day — what do you do? Stash, switch to the branch tied to the release, patch things up. Then the product manager slams down a feature update (kuangkuang) and says it’s simple, could you just knock it out first for me? What do you do? Stash, cut a new feature branch, roll up your sleeves, and get to work. Without git stash, life would be at least half as bright.

git is stubborn: if you haven’t committed the work in your hands, it refuses your request to switch branches — which is perfectly reasonable, since switching branches (which really just means switching to some commit in history; and git checkout -b creates a ref pointing at the current HEAD) involves updating the working directory and rebuilding git’s index. If the working directory has uncommitted changes, git certainly can’t just wipe out all your hard-earned effort.

That’s what makes git stash feel so warm and welcoming. It gathers your unfinished, hard-to-commit work (committing inevitably drags in commit hooks — running unit tests, running static checks) into a temporary holding area, so that once the new task is done you can git stash pop and restore your previous work. The mechanism behind it is remarkably like a CPU exception — which goes to show that programmers only ever have a handful of tricks up their sleeves; they just dress them up in different costumes for different scenes.

So what, exactly, is git stash doing behind the scenes? This is something I need to explore as I work on gitrocks. Come on now — dong ci da ci, dong ci da ci — shake out your tea-oil hair, roll those shoulders, and let’s type together (wow, you can really type~).

First Attempt

Create a new repo, git add hello.txt (content: hello world!\n), then git commit -a -m "init commit". Afterwards, git add goodbye.txt (content: goodbye world!). Once these operations are done in sequence, the entire objects / logs / refs directory looks like this:

    logs
          HEAD
          refs
              heads
                  master
    objects
          11
                a295f9eedc7635bce94d7d2f82328fa5032ed1
          55
                b909b4a7cad81fe818b364959775188bfccf3d
          60
                09101760644963fee389fc730acc4c437edc8f
          a0
                423896973644771497bdc03eb99d5281615b51
          info
          pack
    refs
        heads
              master
        tags

Let’s take a look at the init commit:

$ git cat-file -p 11a2
tree 6009101760644963fee389fc730acc4c437edc8f
author Author <author@example.com> 1574787652 -0800
committer Author <author@example.com> 1574787652 -0800
gpgsig -----BEGIN PGP SIGNATURE-----

 iQB1BAAWCAAdFiEEDAfXiyARvxtLDYya/74NqNQekKwFAl3dWksACgkQ/74NqNQekKzqAgD9HCMmZOL6JRNZZd1jKoDXR8d65q0/aIgeOFQ5SHcdFzUBAMcQypPkhC9dSmV3PydpnddnIp03I6Fja1d2O+bF3nsP
 =VDnV
 -----END PGP SIGNATURE-----


init commit

Next, let’s try a git stash. And lo — something magical happens! Under logs/refs, a stash reference appears, and under objects, three brand-new objects pop out with a satisfying kuang kuang kuang:

    logs
          HEAD
          refs
              heads
                    master
              stash
    objects
          11
                a295f9eedc7635bce94d7d2f82328fa5032ed1
          55
                b909b4a7cad81fe818b364959775188bfccf3d
          60
                09101760644963fee389fc730acc4c437edc8f
          68
                0c79419d0067c90c1103329d9e1effef0fd216
          a0
                423896973644771497bdc03eb99d5281615b51
          de
                8fcd1649da1ab5a6694622508c0e95178ddd02
          e8
                c012220c2e1116ffa1de1264773bd92d31f36b
          info
          pack
    refs
        heads
              master
        stash
        tags

Let’s see what these new objects actually are.

680c is a tree containing hello.txt and goodbye.txt. Normally, this tree would only be produced on commit:

$ git cat-file -p 680c
100644 blob 55b909b4a7cad81fe818b364959775188bfccf3d    goodbye.txt
100644 blob a0423896973644771497bdc03eb99d5281615b51    hello.txt

de8f is a bit interesting — it’s a commit, with commit message WIP on master: 11a295f init commit:

$ git cat-file -p de8f
tree 680c79419d0067c90c1103329d9e1effef0fd216
parent 11a295f9eedc7635bce94d7d2f82328fa5032ed1
parent e8c012220c2e1116ffa1de1264773bd92d31f36b
author Author <author@example.com> 1574787769 -0800
committer Author <author@example.com> 1574787769 -0800

WIP on master: 11a295f init commit

Here 11a295f is the commit from the earlier git cat-file -p 11a2. As you can see, this commit doesn’t carry my GPG signature.

e8c0 leaves me a little puzzled — it’s also a commit:

$ git cat-file -p e8c0
tree 680c79419d0067c90c1103329d9e1effef0fd216
parent 11a295f9eedc7635bce94d7d2f82328fa5032ed1
author Author <author@example.com> 1574787769 -0800
committer Author <author@example.com> 1574787769 -0800

index on master: 11a295f init commit

Comparing de8f and e8c0 carefully, we find that apart from the commit message being different, de8f also has an extra parent pointing at e8c0.

Now let’s look at what’s stored in the newly-added .git/logs/refs/stash:

$ cat .git/logs/refs/stash
0000000000000000000000000000000000000000 de8fcd1649da1ab5a6694622508c0e95178ddd02 Author <author@example.com> 1574787769 -0800    WIP on master: 11a295f init commit

And .git/refs/stash:

$ cat .git/refs/stash
de8fcd1649da1ab5a6694622508c0e95178ddd02

Finally, let’s see where master points:

$ cat .git/HEAD
ref: refs/heads/master
$ cat .git/refs/heads/master
11a295f9eedc7635bce94d7d2f82328fa5032ed1

Now the whole picture is clear. The relationships between the various objects are:

What exactly does our casual git stash do? Architecture diagram

Let’s see what happens with git stash pop:

    logs
          HEAD
          refs
              heads
                  master
    objects
      jec 11
                a295f9eedc7635bce94d7d2f82328fa5032ed1
          55
                b909b4a7cad81fe818b364959775188bfccf3d
          60
                09101760644963fee389fc730acc4c437edc8f
          68
                0c79419d0067c90c1103329d9e1effef0fd216
          a0
                423896973644771497bdc03eb99d5281615b51
          de
                8fcd1649da1ab5a6694622508c0e95178ddd02
          e8
                c012220c2e1116ffa1de1264773bd92d31f36b
          info
          pack
    refs
    ref heads
          ads master
        tags

.git/logs/refs/stash and .git/refs/stash are gone. Nothing else changed.

As a pedantic programmer, let’s run git stash again. Logically, the new stash should only produce a new .git/logs/refs/stash and .git/refs/stash file, without producing new commits — yet git stubbornly produces two new commits again, all the same:

$ git cat-file -p 9a06
tree 680c79419d0067c90c1103329d9e1effef0fd216
parent 11a295f9eedc7635bce94d7d2f82328fa5032ed1
parent 7bb55ed97e4d922a2e2ae04bf2f2f7e04807f5de
author Author <author@example.com> 1574788382 -0800
committer Author <author@example.com> 1574788382 -0800

WIP on master: 11a295f init commit

$ git cat-file -p 7bb5
tree 680c79419d0067c90c1103329d9e1effef0fd216
parent 11a295f9eedc7635bce94d7d2f82328fa5032ed1
author Author <author@example.com> 1574788382 -0800
committer Author <author@example.com> 1574788382 -0800

index on master: 11a295f init commit

This time we’re old hands at it:

$ cat .git/logs/refs/stash
0000000000000000000000000000000000000000 9a063baa8146a819548b0a59e27180ac6951ba3c Author <author@example.com> 1574788382 -0800    WIP on master: 11a295f init commit

$ cat .git/refs/stash
9a063baa8146a819548b0a59e27180ac6951ba3c

$ cat .git/refs/heads/master
11a295f9eedc7635bce94d7d2f82328fa5032ed1

With this information in hand, let’s sketch out the current shape of the DAG once more:

What exactly does our casual git stash do? Architecture diagram

We stash pop the goodbye.txt back out, then git commit -m "second commit":

$ git cat-file -p b139
tree 680c79419d0067c90c1103329d9e1effef0fd216
parent 11a295f9eedc7635bce94d7d2f82328fa5032ed1
author Author <author@example.com> 1574789596 -0800
committer Author <author@example.com> 1574789596 -0800
gpgsig -----BEGIN PGP SIGNATURE-----

 iQB1BAAWCAAdFiEEDAfXiyARvxtLDYya/74NqNQekKwFAl3dYeMACgkQ/74NqNQekKzuEQEAnpcMk/EBPhARtgKU+EplxhMUuJJOaFdka9bk2c22nL8BALaFzvsUlUdSwvZjFRrjW623Z0CktaIumyHpFefcGbME
 =vT0q
 -----END PGP SIGNATURE-----


second commit

$ cat .git/refs/heads/master
b1390fc37a960f97e9e637c3f82315da94120167

As you can see, a brand-new b139 commit is created. master points at it.

Finally, let’s look at the current DAG:

What exactly does our casual git stash do? Architecture diagram

Why does git stash generate two commits? Let’s keep digging.

Second Attempt

Since the object structure of the previous repo is already complex enough, let’s switch to another directory and create a new git repo. Then we repeat what we just did: add hello.txt, commit, then add goodbye.txt. At this point the database looks like this:

    logs
          HEAD
          refs
              heads
                  master
    objects
          55
                b909b4a7cad81fe818b364959775188bfccf3d
          60
                09101760644963fee389fc730acc4c437edc8f
          a0
                423896973644771497bdc03eb99d5281615b51
          c6
                2f16377b077275eace0239f24504becc359763
          info
          pack
    refs
        heads
              master
        tags

We modify hello.txt so it becomes hello tyr!. git status tells us one file is staged and one isn’t:

$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    new file:   goodbye.txt

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:   hello.txt

Let’s take a deep breath and run git stash:

    logs
          HEAD
          refs
              heads
                    master
              stash
    objects
          27
                8efbd09a21dbd2b443bd7a879013abfee434ca
          55
                b909b4a7cad81fe818b364959775188bfccf3d
          60
                09101760644963fee389fc730acc4c437edc8f
          62
                d7bceb43b713c838ce04075fa23fb263aa01f5
          68
                0c79419d0067c90c1103329d9e1effef0fd216
          a0
                423896973644771497bdc03eb99d5281615b51
          a4
                f7a95ef0b8dcbb8d89af09a488f7cb3166fadc
          c3
                efe55f18835756842af5f53a64d1d78dc5ed6c
          c6
                2f16377b077275eace0239f24504becc359763
          info
          pack
    refs
        heads
              master
        stash
        tags

Five new objects! Let’s explore:

$ git cat-file -p 278e
tree 680c79419d0067c90c1103329d9e1effef0fd216
parent c62f16377b077275eace0239f24504becc359763
author Author <author@example.com> 1574791207 -0800
committer Author <author@example.com> 1574791207 -0800

index on master: c62f163 init commit

$ git cat-file -p 62d7
100644 blob 55b909b4a7cad81fe818b364959775188bfccf3d    goodbye.txt
100644 blob c3efe55f18835756842af5f53a64d1d78dc5ed6c    hello.txt

$ git cat-file -p 680c
100644 blob 55b909b4a7cad81fe818b364959775188bfccf3d    goodbye.txt
100644 blob a0423896973644771497bdc03eb99d5281615b51    hello.txt

$ git cat-file -p c3ef
hello tyr!

$ git cat-file -p a4f7
tree 62d7bceb43b713c838ce04075fa23fb263aa01f5
parent c62f16377b077275eace0239f24504becc359763
parent 278efbd09a21dbd2b443bd7a879013abfee434ca
author Author <author@example.com> 1574791207 -0800
committer Author <author@example.com> 1574791207 -0800

WIP on master: c62f163 init commit

$ cat .git/refs/stash
a4f7a95ef0b8dcbb8d89af09a488f7cb3166fadc

$ cat .git/logs/refs/stash
0000000000000000000000000000000000000000 a4f7a95ef0b8dcbb8d89af09a488f7cb3166fadc Author <author@example.com> 1574791207 -0800    WIP on master: c62f163 init commit

Now it’s clear: commit a4f7 uses the tree 62d7, while commit 278e uses the tree 680c. 62d7 contains the modified hello.txt, and the stash reference points at a4f7. A diagram makes this more intuitive:

What exactly does our casual git stash do? Architecture diagram

In other words, when we run git stash, we stash two kinds of data:

  • Data in the staging area (goodbye.txt)
  • Data in the working area (the modified hello.txt)

Why This Design?

At first I found this quite baffling. This unfinished work gets tossed, without ceremony, into an immutable object database — isn’t that a waste of resources? Why not just drop it into a stash directory and be done with it? While building gitrocks, I explored some “clever” alternatives — putting it in a directory, throwing it into sqlite, and so on. But the deeper I dug, the more I realized git was right. git users, after all, may — like you and me — be running wild across a dozen branches a day. If everywhere they went they left behind a trail of git stash “I-was-here” marks, then you’d have to record the parent commit, generate a tree for the stash to preserve the directory structure, and store the modified files somewhere — and all of this is already implemented beautifully by the commit code. Using one piece of code to solve two problems — there’s a beauty in that simplicity. Beyond that, although the generated commits ultimately go unused, the intermediate trees and subtrees have a good chance of being reused, so none of the earlier computation is wasted. Finally, git provides gc, which recycles those orphans — after git stash pop, those left “circling the tree three times with no branch to rest on” — thereby saving space.

After nearly two hours of exploration, I calmly accepted a simple fact: your grandpa is still your grandpa. The ingenuity of git’s design still requires serious contemplation. The Master once said: “Do I possess knowledge? No, I do not.” If even Confucius could be that humble, then I still have work to do.

MORE IN Languages & Craft

Further Reading in This Column

View all essays in “Languages & Craft” →

Time to Meet jj: Git's Next Evolutionary Step

Git has ruled version control for twenty years, but its staging area, brittle rebases, and blocking merge conflicts are becoming liabilities in the AI era. jj rebuilds the mental model from the ground up — while staying fully Git-compatible. Here's why it deserves your attention.

New Hope (2025–2026)5,588 chars · 12 min read