Fix #12: add git ls-tree
Fixes #12 (https://github.com/blindmansion/just-git/issues/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. So does git's <rev>:<path> form (HEAD:src, v1:src/lib): the path is relative to the root, or to the cwd when it starts with ./ or ../, and one trailing slash is allowed. A missing or non-canonical path (HEAD:nope, HEAD:/src, HEAD:src//lib, :src) fails with Not a valid object name, a blob path with not a tree object, and a ../ path that leaves the repository with git's outside repository message. 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. srcshows thesrctree entry itself.src/lists its contents.- Without
-r, the walk still descends into a tree when a path points inside it (git'sshow_recursive).git ls-tree HEAD src/lib/math.tsprints the blob, and-tadds thesrcandsrc/libtrees above it. -ddrops blobs but keeps gitlinks.-r -dimplies-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 fromsrc/libthesrc/libtree is./and thesrctree is../.--full-treelists 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.tshas 38 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--,-twith a nested path, literal (non-glob) paths, a subdirectory cwd with and without--full-tree,../paths,./and../for trees that contain the cwd,-z,--abbrevand--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. Seven more cover<rev>:<path>: a subtree,-rfrom a subtree,HEAD:as the root tree,./and../from a subdirectory, invalid paths, a blob path, and a../path that leaves the repository.test/interop/ls-tree.test.tsbuilds a repo with real git (executable, symlink, gitlink viaupdate-index --cacheinfo 160000, names with a space, a quote, a tab andé, an annotated tag). It then runs 73 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 whose040000entry points at a blob and checks that-rfails 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 withfatal: Not a valid object name HEAD:dir(exit 128).resolveRevisiondoesn't parse<rev>:<path>at all.git showparses it on its own and only finds blobs, sogit show HEAD:dirhas the same gap. The fix belongs in the shared rev-parse code.- The empty tree
4b825dc642cb6eb9a060e54bf8d69288fbee4904is built into git but doesn't resolve as a revision in just-git, sogit ls-tree 4b825dc...printsfatal: Not a valid object name 4b825dc...where git prints nothing and exits 0. This is already true on main.git rev-parseandgit difffail on it the same way. - An empty tree-ish (
git ls-tree '') exits 1 with anEISDIRerror from ref lookup, where git printsfatal: Not a valid object nameand exits 128.git show ''does the same on main, so this also belongs in the shared rev-parse code. -l/--long,--full-name,--object-onlyand--formatare not implemented and fail as unknown options.core.quotePath=falseis not read. Non-ASCII names are always octal-quoted.