Skip to content

Fix #12: add git ls-tree - #14

Open
Hazzng wants to merge 4 commits into
blindmansion:mainfrom
Hazzng:feat/ls-tree
Open

Hazzng wants to merge 4 commits into
blindmansion:mainfrom
Hazzng:feat/ls-tree

Conversation

@Hazzng

@Hazzng Hazzng commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Fixes #12 (#12)

Summary

git ls-tree now works. Before this change it sat in KNOWN_UNIMPLEMENTED_COMMANDS, so git ls-tree -r HEAD printed "'ls-tree' is not implemented" and exited 1. Scripts and agents that check a committed file's mode or list files at a revision had to fall back to ls-files -s (index only) or git show <rev>:<dir>.

Supported surface:

git ls-tree [-r] [-d] [-t] [--name-only | --name-status] [--full-tree] [-z] [--abbrev[=<n>]] [--] <tree-ish> [[--] <path>...]

The tree-ish can be a commit, an annotated tag or a tree id, and <rev>^{tree} works too. Output is <mode> SP <type> SP <object> TAB <path>, with 040000 tree, 160000 commit for gitlinks, and 100755/120000 blobs.

Before and after

$ git ls-tree -r HEAD; echo rc=$?
# before (1.9.0): git: 'ls-tree' is not implemented. Run 'git help' for available commands.   rc=1
# after:          100755 blob 1a2485251c33a70432394c93fb89330ef214bfc9	run.sh   rc=0  (same as git)

How it works

src/commands/ls-tree.ts resolves the tree-ish with resolveRevision and peels tags and commits down to a tree. It then walks the tree with readTreeEntries from tree-ops.ts, the reader diffTrees already uses, which this PR exports. flattenTree can't serve -t and -d because it drops directory entries. Going through readTreeEntries also means a tree entry that points at a blob stops the walk with "Expected tree object, got blob" and exit 1. Git exits 1 there too, with error: Object <id> not a tree.

The tree-ish is the first operand whether it comes before or after --, so git ls-tree -- HEAD and git ls-tree -r -- HEAD work like git. With no tree-ish at all it prints usage: git ls-tree [<options>] <tree-ish> [<path>...] and exits 129. Git prints the option list after that line. just-git prints the usage line only, like its merge and rebase usage errors.

Errors come in git's order. An unknown tree-ish fails first. An empty path comes next (fatal: empty string is not a valid pathspec. please use . instead if you meant to match all paths, 128), checked across every path before any of them is resolved. A path outside the repository comes after that, and not a tree object comes last.

Path handling follows builtin/ls-tree.c rather than the shared pathspec code, because ls-tree treats paths differently from other commands:

  • Paths are literal. git ls-tree HEAD 'sp*' matches nothing, same as git.
  • src shows the src tree entry itself. src/ lists its contents.
  • Without -r, the walk still descends into a tree when a path points inside it (git's show_recursive). git ls-tree HEAD src/lib/math.ts prints the blob, and -t adds the src and src/lib trees above it.
  • -d drops blobs but keeps gitlinks. -r -d implies -t.
  • From a subdirectory with no paths, it lists that directory. Output paths are relative to the cwd (../run.sh). A tree that contains the cwd prints with a trailing slash, so from src/lib the src/lib tree is ./ and the src tree is ../. --full-tree lists from the root with full paths.
  • A path that leaves the repository fails with git's message and exit 128.

Paths are C-quoted the way git does with core.quotePath on ("q\"uote.txt", "\303\251.txt", "tab\tname"), and printed raw under -z. --abbrev uses uniqueAbbrev, --abbrev=<n> clamps to 4, and --abbrev=0 prints full ids, all matching git.

Outside the command file, the PR exports readTreeEntries from src/lib/tree-ops.ts. Registration touches src/commands/git.ts (registry entry, removal from KNOWN_UNIMPLEMENTED_COMMANDS) and adds | "ls-tree" to the GitCommandName union in src/git.ts. The registry is typed Record<GitCommandName, ...>, so the command can't register without that one line.

Tests

  • test/commands/ls-tree.test.ts has 31 in-memory tests with exact stdout, stderr and exit codes. They cover the root listing, -r, -d, -r -d, -r -t, --name-only/--name-status, a directory path with and without a trailing slash, a file path after --, -t with a nested path, literal (non-glob) paths, a subdirectory cwd with and without --full-tree, ../ paths, ./ and ../ for trees that contain the cwd, -z, --abbrev and --abbrev=4, a tree id as the tree-ish, the tree-ish after --, no tree-ish (usage, 129), an invalid tree-ish (fatal: Not a valid object name bogus, 128), a blob (fatal: not a tree object, 128), an empty path, a path outside the repository, and the order of those errors.
  • test/interop/ls-tree.test.ts builds a repo with real git (executable, symlink, gitlink via update-index --cacheinfo 160000, names with a space, a quote, a tab and é, an annotated tag). It then runs 50 invocations through both real git and just-git and asserts identical stdout, stderr and exit code. I checked that it catches defects by breaking quoting and the gitlink type. 17 of the first 40 cases failed. A separate test writes a tree whose 040000 entry points at a blob and checks that -r fails with exit 1, as git does.
  • Every test added for nested-cwd display, empty paths, the tree-ish after -- and the bad tree entry failed before its fix.
  • bun test: 3570 pass, 29 skip, 0 fail (baseline 3488 pass).

Not included

  • <rev>:<path> as a tree-ish fails with fatal: Not a valid object name HEAD:dir (exit 128). resolveRevision doesn't parse <rev>:<path> at all. git show parses it on its own and only finds blobs, so git show HEAD:dir has the same gap. The fix belongs in the shared rev-parse code.
  • The empty tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904 is built into git but doesn't resolve as a revision in just-git, so git ls-tree 4b825dc... prints fatal: Not a valid object name 4b825dc... where git prints nothing and exits 0. This is already true on main. git rev-parse and git diff fail on it the same way.
  • An empty tree-ish (git ls-tree '') exits 1 with an EISDIR error from ref lookup, where git prints fatal: Not a valid object name and exits 128. git show '' does the same on main, so this also belongs in the shared rev-parse code.
  • -l/--long, --full-name, --object-only and --format are not implemented and fail as unknown options.
  • core.quotePath=false is not read. Non-ASCII names are always octal-quoted.

Hazzng added 4 commits October 9, 2026 14:50
Lists a tree-ish with -r, -d, -t, --name-only, --name-status, --full-tree, -z and --abbrev, using git's literal path matching, cwd-relative paths and C-style quoting.
Covers gitlinks, quoted names, subdirectory cwds and every supported flag against the real binary.
Replaces the trailing-slash string encoding with a { path, listContents } filter so the matching rule lives in the type.
…s and -- before the tree-ish

From a nested cwd, trees that contain the cwd now print as ../ (and ./),
not ..; an empty path is fatal like git; the tree-ish may follow --, and
no tree-ish prints usage with exit 129. Errors follow git's order. The
walk reuses readTreeEntries (now exported) so a tree entry pointing at a
blob fails instead of listing nothing.
@Hazzng

Hazzng commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

@blindmansion could you review this when you have a moment? It adds git ls-tree (-r, -d, -t, --name-only, --full-tree, -z, --abbrev, path filters), checked against real git output. We need this in prod.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement git ls-tree

1 participant