skip to content

In Git, what does git ls-files --stage show that git ls-tree HEAD does not?

level: seniorimportance: should knowfreq 22%

answer

  1. one reads an object, one reads a file
  2. staged changes are not in HEAD yet
  3. an extra column with no tree equivalent
  4. three versions of one path mid-merge
  5. that is why write-tree can refuse

basics

~20 s

git 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 s

They 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
console
$ git ls-files --stage src/app.c
100644 6f1c2ab3d4e5f60718293a4b5c6d7e8f90a1b2c3 1	src/app.c
100644 9d3e5b7a1c2f4086d5e3b1a9c7f6e4d2b0a8c6e4 2	src/app.c
100644 1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d 3	src/app.c

go deeper

for a junior

Recall that one command lists what a commit contains and the other lists what is currently staged, so their output can legitimately differ.

for a middle

Explain the columns of each, and why staged additions, staged deletions and mode changes make the index diverge from HEAD's tree.

for a senior

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.

for a principal

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

context