In Git, what does git ls-files --stage show that git ls-tree HEAD does not?
answer
- one reads an object, one reads a file
- staged changes are not in HEAD yet
- an extra column with no tree equivalent
- three versions of one path mid-merge
- that is why write-tree can refuse
basics
~20 sgit ls-tree HEAD reads a committed tree object, while git ls-files --stage reads the index — so it shows staged additions and deletions HEAD lacks, plus a stage number per entry that exposes the three conflicting versions during a merge.
solid answer
~50 sThey read different data structures. `git ls-tree HEAD` lists the entries of the tree object recorded in the HEAD commit: a frozen snapshot in the object database, printed as mode, type, object ID and name. `git ls-files --stage` (or `-s`) lists the **index** — mode, object ID, **stage number**, name — which is the mutable staging area. So the index shows a newly `git add`ed file that HEAD has never seen, omits a staged deletion that HEAD still contains, and can differ in mode where only a permission bit was staged. The stage number is the real differentiator: normally every entry is stage 0, but a conflicted merge records the same path three times — stage 1 the merge base, stage 2 ours, stage 3 theirs — and `git ls-files -u` lists exactly those. This is why `git write-tree` fails mid-conflict: a tree can hold only one entry per name.
code
console · 4 lines$ git ls-files --stage src/app.c
100644 6f1c2ab3d4e5f60718293a4b5c6d7e8f90a1b2c3 1 src/app.c
100644 9d3e5b7a1c2f4086d5e3b1a9c7f6e4d2b0a8c6e4 2 src/app.c
100644 1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d 3 src/app.cgo deeper
Recall that one command lists what a commit contains and the other lists what is currently staged, so their output can legitimately differ.
Explain the columns of each, and why staged additions, staged deletions and mode changes make the index diverge from HEAD's tree.
Use stages diagnostically: read git ls-files -u to characterise a conflict precisely, and connect the multi-stage index to why write-tree refuses to run mid-merge.
Frame the index as a distinct data structure with its own state machine, and insist that tooling which manipulates staged state inspects it directly rather than inferring it from human-facing status output.
## Two different things being listed The confusion the question probes is between an object and a file. `git ls-tree` reads a **tree object** out of the object database — immutable, content-addressed, reachable from a commit. `git ls-files` reads `.git/index`, the **staging area** — a mutable binary file describing what the *next* commit would contain. `git ls-tree HEAD` prints, per entry: ``` <mode> <type> <object> <path> ``` By default it lists one directory level; `-r` recurses into subtrees, `-r -t` also emits the subtree entries themselves, `--name-only` strips everything but paths, `-l` adds blob sizes, and `-z` gives NUL-terminated records. `git ls-files --stage` prints, per entry: ``` <mode> <object> <stage> <path> ``` No type column, because index entries are always blobs or gitlinks, and one extra column that has no equivalent in a tree at all: the stage. ## What the index has that HEAD does not - **Staged additions.** `git add newfile` writes a blob and an index entry. HEAD's tree knows nothing about it until you commit. - **Staged deletions.** `git rm --cached old` removes the index entry while HEAD's tree still lists it, so the difference is a missing row rather than an extra one. - **Mode-only differences.** The index may record `100755` where HEAD's tree has `100644` because a permission change was staged. Both listings show a mode, so this is one of the few discrepancies you can spot by eye. - **Cached stat information.** The index stores per-file device, inode, size and timestamps so Git can decide a file is unchanged without rehashing it. That data is not printed by `--stage`, and it does not exist in trees at all. A stale cache is why `git update-index --refresh` sometimes settles phantom "modified" reports. The mode values themselves are a fixed small set in both listings: `100644` regular file, `100755` executable, `120000` symlink — whose blob contains the target path as text — and `160000` a gitlink, the submodule's recorded commit ID. ## Stages: the part with no tree equivalent In a clean index, every entry is at **stage 0**, meaning "merged, ready to commit". A conflicted merge instead records up to three entries for the same path: - **stage 1** — the version from the merge base, - **stage 2** — ours, the version from the branch you are on, - **stage 3** — theirs, the version being merged in. `git ls-files -u` (or `--unmerged`) lists only those. This is the authoritative view of a conflict, and it is more precise than reading conflict markers out of the file, because a path can be unmerged for reasons that produce no markers at all — a delete/modify conflict shows a missing stage, and a binary conflict shows stages with no textual merge attempted. It also explains a behaviour that otherwise looks arbitrary: `git write-tree` refuses to run while unmerged entries exist. A tree object maps each name to exactly one mode and one object ID, so three candidate versions of a path simply cannot be serialised. Resolving a conflict means collapsing those entries back to a single stage-0 entry, which is what `git add <path>` does during a merge. ## Diagnostic uses - **Inspecting a conflict precisely.** `git ls-files -u` tells you which paths are unmerged and which of the three sides are present, which distinguishes a content conflict from a delete/modify one. - **Explaining a stubborn diff.** When `git diff` and `git diff --cached` disagree with intuition, comparing `git ls-files -s <path>` against `git ls-tree HEAD <path>` shows exactly which of the three states — working tree, index, HEAD — is the odd one out. - **Finding accidental mode churn.** A `100755` in the index against `100644` in HEAD is the fastest explanation for a diff that appears to contain no content change. - **Listing untracked or ignored files.** `git ls-files --others --exclude-standard` enumerates files present on disk but absent from the index, honouring the standard ignore sources — the plumbing behind the untracked section of status. ## The one-sentence answer `git ls-tree` shows a commit's frozen snapshot; `git ls-files --stage` shows the mutable index, including entries HEAD lacks or lacks entries HEAD has, plus a stage column that encodes an in-progress merge — a state trees are structurally incapable of representing.
- What do stage numbers 1, 2 and 3 mean in git ls-files --stage output?Stage 1 is the merge base version of the path, stage 2 is ours — the side you had checked out — and stage 3 is theirs, the side being merged in. Stage 0 means the entry is fully merged and ready to commit. Resolving a conflict collapses stages 1 to 3 back into a single stage-0 entry.
- Why does git write-tree refuse to run while unmerged index entries exist?A tree object maps each name to exactly one mode and one object ID, so it cannot represent a path that currently has base, ours and theirs versions recorded. write-tree therefore errors out rather than picking arbitrarily, and only succeeds once every entry is back at stage 0.
- How would you list files that exist on disk but are not in the Git index?`git ls-files --others --exclude-standard`. `--others` selects paths that are neither tracked nor otherwise listed, and `--exclude-standard` applies the usual ignore sources so generated files do not flood the output. It is the plumbing behind the untracked-files section of status and is safe to pipe.
saying these in an interview costs you the question
- Thinks ls-files and ls-tree read the same data
- Says the index is just a list of filenames
- Believes stage numbers indicate change ordering
- Assumes conflicts exist only as markers in files
- Expects a tree object to hold multiple versions of a path