skip to content

How does Git store a stash internally — what commits and refs does git stash push create?

level: middleimportance: should knowfreq 36%

answer

  1. Nothing new was invented for this feature
  2. It lives outside refs/heads
  3. More than one parent per entry
  4. The numbering comes from a reflog
  5. refs/stash plus stash@{n} positions

basics

~20 s

A stash is ordinary commits. git stash push writes a commit whose tree is your working tree, with your HEAD commit as first parent and a commit recording the index state as second; refs/stash points at it, and stash@{n} indexes that ref's reflog.

solid answer

~50 s

There is nothing magic about the stash: it is a small commit graph parked outside your branches. `git stash push` builds a commit capturing the index state, then a stash commit whose tree is the full working-tree state, with two parents — the current `HEAD` commit first and the index commit second. With `-u` a third parent holds the untracked files. `refs/stash` is then pointed at that commit. The `stash@{n}` addressing you type is **reflog** syntax on `refs/stash`: pushing a new stash moves the ref and pushes the older entries down the reflog, which is why the numbers shift and why they are positions rather than stable names. Because the entries are real commits, you can inspect them with ordinary tools — `git log --graph --oneline refs/stash` or `git show stash@{0}` work exactly as they would on a branch.

code

console · 8 lines
console
$ git stash push -u -m "parser wip"
$ git cat-file -p refs/stash
tree 4d2c1f0...
parent 9a3f21c...   # HEAD when stashing
parent 7bb0a41...   # index state
parent c05e93d...   # untracked files (-u only)
...
WIP on feature: parser wip

go deeper

for a junior

Know that stashes are stored in the repository itself under refs/stash, and that stash@{0} is the newest entry with older ones numbered upward.

for a middle

Describe the objects: an index commit, optionally an untracked commit, and a stash commit whose tree is the working tree and whose parents are HEAD and the index commit. Explain stash@{n} as reflog positions.

for a senior

Draw conclusions from the model — why restores are merges that can conflict, why --index is possible at all, why entries renumber, and why ordinary commands like git show and git log work on them.

for a principal

Use it to argue about where in-progress work should live: stashes are unnamed, unpushable, repository-local refs, so anything that must survive, be reviewed, or be shared belongs on a branch instead.

## The claim "A stash is a commit" sounds like trivia, but it explains nearly every stash behaviour that confuses people: why entries renumber, why `git show` works on them, why a restore can conflict, and why entries survive branch switches. ## What `git stash push` builds When you stash, Git creates commit objects — not a patch file, not a scratch directory: 1. **An index commit.** A commit whose tree is the current index (your staged content), with `HEAD` as its parent. Its message looks like `index on <branch>: <sha> <subject>`. 2. **An untracked commit**, only when you used `-u` or `-a`. Its tree contains the untracked (and with `-a`, ignored) files. It has no parent. 3. **The stash commit.** Its tree is the full **working-tree** state. Its parents, in order, are: the `HEAD` commit you were on, the index commit, and — if present — the untracked commit. Its message is the familiar `WIP on <branch>: <sha> <subject>`, or your own text when you pass `-m`. Having both a working-tree tree and an index tree recorded is what allows `git stash apply --index` to reproduce your original staged/unstaged split; without `--index`, Git just applies the working-tree state and everything comes back unstaged. After writing these objects, Git resets your working tree and index to `HEAD`. ## Where it is stored The stash commit is pointed to by a single ref: **`refs/stash`**. It is deliberately not under `refs/heads/`, so it is not a branch, does not appear in `git branch`, and is not pushed by a normal push. ## Why `stash@{n}` renumbers One ref can only point at one commit, so how do multiple stashes coexist? Through the ref's **reflog**. Each time you stash, `refs/stash` is updated to the new stash commit and the previous value is recorded in the reflog for that ref. `stash@{0}` means "the current value of `refs/stash`", `stash@{1}` means "its value one entry ago", and so on — the same `<ref>@{n}` syntax you use for any ref. Two consequences follow directly: - **Numbers are positions, not identities.** Creating a stash shifts every existing entry down by one, so a `stash@{2}` you noted five minutes ago may now be `stash@{3}`. Always re-list before acting on a numbered entry. - **`git stash list` is a reflog listing**, which is why entries carry the branch and commit they were made on in their message. ## Inspecting with ordinary commands Because they are commits, normal plumbing and porcelain work: ``` git stash list git show stash@{0} # the stash's diff git log --graph --oneline refs/stash git cat-file -p refs/stash # raw commit object: tree and parents git rev-parse stash@{1} # the commit id of an entry ``` `git cat-file -p refs/stash` is the demonstration that settles the question: you can literally read the two (or three) `parent` lines. ## Why restores conflict Applying a stash is a three-way merge, not a file copy: Git merges the stash's working-tree state against your current `HEAD` using the stash's first parent as the base. If the branch has moved or you have other changes, the merge can conflict exactly as any merge can. That is a direct consequence of the storage model, not a quirk of the command. ## What `drop` and `clear` actually do `git stash drop` removes an entry from the reflog of `refs/stash`; `git stash clear` empties the whole thing. Neither immediately deletes the underlying commit objects — they simply stop being referenced, which is the ordinary Git story for unreferenced objects. That is why `git stash branch` can hand an entry a real branch: it is just pointing a branch ref at commits that already exist. ## Stashes are repository-wide, not per branch One more thing the model explains: `refs/stash` is a single per-repository ref. Stashes are not scoped to the branch you made them on — you can pop a stash created on `feature/a` while sitting on `main`. The branch name in the entry's message is documentation, nothing more. Whether the restore applies cleanly depends on the merge, not on where you made it. ## What a strong answer covers Name `refs/stash`, describe the commit with `HEAD` and the index commit as parents (plus the untracked commit with `-u`), explain `stash@{n}` as reflog positions on that ref, and draw one conclusion from the model — renumbering, conflicts as merges, or `--index` being possible because the index tree was recorded.

  • Why do stash numbers change after you create a new stash?
    Because `stash@{n}` is reflog syntax on `refs/stash`, not a name. Each new stash updates that ref and pushes the previous values one position further back, so every existing entry's index increases by one. Re-run `git stash list` before acting on a numbered entry, and use `-m` messages so you can identify entries by content.
  • How does recording an index commit make `git stash apply --index` possible?
    The stash commit's second parent is a commit whose tree is exactly the index as it was, so Git has a recorded copy of your staged content separate from the working-tree state. `--index` replays that tree into the index while applying the working-tree state, reproducing the original staged/unstaged split. Without it, everything returns unstaged.
  • Are stashes tied to the branch they were created on?
    No. `refs/stash` is a single repository-wide ref, so every stash lives in one list regardless of branch, and you can apply an entry while on any branch. The branch name in the `WIP on <branch>` message is only a label recording where it was made; whether the restore succeeds depends on the three-way merge, not on the branch.
  • Why can applying a stash produce conflict markers?
    Because applying is a three-way merge: Git merges the stash's recorded working-tree state into your current `HEAD`, using the commit you were on when you stashed as the base. If the same lines changed on the branch in the meantime, the merge conflicts exactly as a branch merge would.

saying these in an interview costs you the question

  • Thinking the stash is a patch file in .git
  • Believing stashes are stored per branch
  • Treating stash@{1} as a stable identifier
  • Assuming a stash commit has a single parent
  • Thinking a stash restore is a plain file copy

context