Skip to content

fix: snapshot files in repositories without commits - #1015

Open
ethanhawkes-gif wants to merge 1 commit into
Nano-Collective:mainfrom
ethanhawkes-gif:fix/empty-repo-snapshots
Open

fix: snapshot files in repositories without commits#1015
ethanhawkes-gif wants to merge 1 commit into
Nano-Collective:mainfrom
ethanhawkes-gif:fix/empty-repo-snapshots

Conversation

@ethanhawkes-gif

Copy link
Copy Markdown

Summary

  • check work-tree validity separately from commit existence
  • skip only the committed-file diff when HEAD is absent, then collect
    non-ignored untracked files with git ls-files --others --exclude-standard
  • update source/services/file-snapshot.ts and add the regression case
    FileSnapshotService returns untracked files from a repository without commits
    in source/services/file-snapshot.spec.ts; the case initializes a temporary
    repository with no commits and verifies root, nested, and ignored files

Test plan

  • pnpm run test:ava -- source/services/file-snapshot.spec.ts
  • pnpm run test:types

Closes #1010

@will-lamerton will-lamerton left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this - the diagnosis is right and the error handling degrades safely. I merged it into main locally and confirmed it merges cleanly, typechecks, and passes all 30 tests in file-snapshot.spec.ts. A few things before it can go in.

The fix misses staged files

On an unborn branch we set modifiedOutput = '' and rely only on git ls-files --others --exclude-standard. --others excludes anything in the index, so the fix stops working the moment a user stages. Running the merged code against git init && git add .:

{"files":[],"truncated":false,"available":true}

Zero files, for a repo where the whole tree is staged. git init then git add . is a very common opening move, and git checkout --orphan hits the same path with a fully populated index. That affects checkpoint-manager.ts:125 and both scans in acp-timeline.ts.

git diff handles an unborn HEAD natively, so one word covers it:

modifiedOutput = execSync('git diff --name-only --cached', {...}).trim();

The show-ref probe is redundant

rev-parse --verify HEAD already answers "does HEAD exist". The show-ref probe only exists to work out which command in the inner try threw, and it only needs to because git diff is inside that same try. throw headError on line 131 is also thrown inside the try whose own catch (showRefError) then catches and re-inspects it. It happens to rethrow correctly today (both commands exit 128), but only by accident - any git command that ever exits 1 there would be silently treated as an unborn branch.

Moving git diff out removes the whole nested-catch structure:

let hasHead = true;
try {
	execSync('git rev-parse --verify HEAD', {cwd: this.workspaceRoot, stdio: ['pipe', 'pipe', 'pipe']});
} catch {
	hasHead = false;
}

// An unborn branch has no HEAD to diff against, but its index can still be
// full (`git init && git add .`), so diff the index instead of skipping.
const modifiedOutput = execSync(
	hasHead ? 'git diff --name-only HEAD' : 'git diff --name-only --cached',
	{cwd: this.workspaceRoot, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe']},
).trim();

A real git diff failure then falls straight to the outer catch, which is the old behaviour the comment says it wants to preserve.

Smaller points

  • git rev-parse --is-inside-work-tree never checks its output - in a bare repo it prints false and exits 0, so the comment overstates what it does. With the simplification above it is fully redundant with the existing error handling, and dropping it takes the per-scan subprocess count back from 4 to 2 (getModifiedFilesResult runs twice per opaque tool call in acp-timeline).
  • No changeset. changeset-check is deliberately non-blocking, so its green tick is not evidence one exists. This is a user-facing fix, so please run pnpm changeset.
  • The new test is a genuine regression test (it fails without the fix), but please add the staged-file case above, and call getModifiedFilesResult() so it can assert available: true - that flag is what acp-timeline.ts branches on for unborn repos.
  • Nit: import {execSync} from 'child_process' sits after the fs import; file-snapshot.ts itself orders child_process first.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Hi @ethanhawkes-gif, thanks for this PR! It looks like a codeowner has left feedback
or review activity and there are still some outstanding items to wrap up.

Whenever you get a chance, could you take a look at the open comments?
If anything is unclear or you'd like a hand, just reply here and we'll help you get it across the line.

`getModifiedFilesResult` diffed against HEAD, which does not exist on an
unborn branch, so the whole scan fell to its catch and reported git as
unavailable. Every file in a freshly `git init`ed workspace looked
unmodified and a checkpoint taken there restored nothing.

Check for HEAD first and diff the index with `git diff --name-only --cached`
when there is none. `git ls-files --others` alone is not enough: it excludes
anything already in the index, so `git init && git add .` (and
`git checkout --orphan`, which starts with a fully populated index) would
still have yielded zero files.

A real `git diff` failure now falls straight to the outer catch, which is
the pre-existing behaviour, instead of being routed through a nested catch
that re-inspected the error's exit status.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ethanhawkes-gif
ethanhawkes-gif force-pushed the fix/empty-repo-snapshots branch from d41ba41 to 1c72787 Compare September 5, 2026 17:19
@ethanhawkes-gif

Copy link
Copy Markdown
Author

All six points addressed in 1c72787. The branch was 109 commits behind main, so I rebased first — that turned out to matter for both CI reds.

Index-diff instead of the work-tree probe (1–3). getModifiedFilesResult now checks git rev-parse --verify HEAD and diffs git diff --name-only --cached when there is no HEAD, in the shape you suggested. Two notes where the rebase moved things: the --is-inside-work-tree probe you asked me to drop was already removed on main, and getModifiedFiles has since been refactored into a thin wrapper, so the change sits in getModifiedFilesResult. The nested show-ref catch is gone, so a genuine git diff failure falls to the outer catch and still reports git as unavailable. Two subprocesses per scan plus the existing ls-files.

Staged-file test (4). Added alongside the untracked one, asserting available and the file list. I wrote it before the source change to confirm it bites: against the old code it fails with available: false and an empty list — your reported {"files":[],...} — and passes after. --others alone would not have covered this, since git ls-files --others excludes anything already in the index, which is exactly the git init && git add . case.

Changeset (5) is .changeset/snapshot-files-without-commits.md; pnpm test:changesets passes. Import order (6) fixed in the spec — child_process above fs.

Full suite: 7520 passed, 1 skipped, 0 failed; test:types and test:format clean.

On the two red checks — both were stale-base, not this PR. The Semgrep finding is plugins/vscode/src/chat-webview-provider.ts, which this PR never touches; it was fixed on main by e2770f1 (#887) at 2026-08-28 10:33 +0100, and this branch was forked from b448f2b (#881) at 10:09 the same morning, 24 minutes earlier. The 51 unit-test failures were all in source/vscode/chat-panel-*.spec.ts with none in file-snapshot; those specs pass locally on the rebased branch. The rebase should clear both, but worth a fresh CI run to confirm rather than taking my word for it.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] git diff --name-only HEAD fails silently in new git repos

2 participants