Repository navigation
fix(files): list the changed files of a project behind a symlink - #1387
Conversation
`git rev-parse --show-toplevel` answers with the repository's real path, while the caller asks about the cwd it was given: `/tmp` and `/var/folders/…` are links on macOS, so the two spell the same folder differently. Every path git reported was then resolved against the real root and kept only if it matched the caller's cwd as a string, which never happens through a link — a project reached that way had an empty changed-file list: no changed badge, no line counts, no entries at all. - `resolveExistingPath()` (`lib/paths.ts`) resolves the nearest existing ancestor and keeps the remainder as written, so a deleted file works too. `samePath()` stays lexical and says so; this is the resolving half. - `getGitStatus()` rebases every reported path onto the cwd it was asked about, and takes its line-stat pathspec from the resolved cwd. - `getGitFileDiff()` compares and derives its pathspec from the resolved pair, so a path named through a link still resolves to the file Git means, and a path that leaves the repository stays refused.
|
The first run's The |
getGitFileDiff tried the lexical and the resolved spelling; the lexical one never finds an entry the resolved one misses (git lists nothing under a linked directory, and a linked file is refused at lstat either way), so compare the resolved pair only and reuse isWithinPath(). getGitStatus also resolves git's root, so a junction-opened project on Windows matches whichever spelling git prints. The resolveExistingPath test links with a junction on Windows and builds its missing path with resolve(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks. I reproduced it on main on macOS: a repo opened through I checked the security side. I pushed one small follow-up commit:
|
What it does
A project reached through a symlink had an empty changed-file list: no changed badge, no line counts, no entries at all.
resolveExistingPath()(lib/paths.ts): resolves the nearest existing ancestor and keeps the remainder as written, so a deleted file still works.samePath()stays lexical and says so; this is the resolving half.getGitStatus()(lib/git-changes.ts): rebases every path git reports onto the cwd the caller asked about, and takes the line-stat pathspec from the resolved cwd.getGitFileDiff()(lib/git-changes.ts): compares and derives its pathspec from the resolved pair, so a path named through a link still resolves to the file git means — and a path that resolves outside the repository stays refused.A folder that is not in a repository keeps answering
isGitRepository: falsewith nothing listed, and that is now pinned by a test.Why it matters (the broken behaviour)
On the base commit,
lib/git-changes.ts:getGitStatus(cwd)asks git for the repository root withgit rev-parse --show-toplevel(findRepositoryRoot,lib/git-changes.ts:30-32). That answer is the repository's real path.path.resolve(repositoryRoot, entry.path)and keeps it only ifisWithinPath(cwd, filePath)— a string comparison against the cwd the caller passed (lib/git-changes.ts:123-124).readTrackedLineStats,lib/git-changes.ts:61-66, called with the caller's cwd atlib/git-changes.ts:120).getGitFileDiff()compares the file against the repository root as strings (lib/git-changes.ts:207-209).On macOS those two never match when the project is behind a link: git says
/private/var/folders/…/project(the real path), the caller says/var/folders/…/project.path.resolvedoes not resolve symlinks, so every file is filtered out andfilescomes back empty — and the diff lookup answerssupported: false. The same is true for/tmp(a link to/private/tmp) and for any project the operator opened through a link of their own.How to reproduce (on main, before the fix)
Make a repository and modify a tracked file in it:
Link to it and open Pi Web on the link:
Open
/tmp/pi-web-repro/linkin Pi Web.Before the fix the Git panel is empty — no changed badge on
a.txt, no line counts, no entries — although the file is modified. The same happens for any project reached through/tmpor~/Library/Caches, which are links on macOS. After the fix the file is listed asMwith its line counts.Testing
Node 26.7.0 (
source ~/.nvm/nvm.sh && nvm use 26.7.0), in the worktree.npm test— pass:tests 2874 / suites 16 / pass 2873 / fail 0 / skipped 1(the one skip is the pre-existing Windows-only junction test inapp/api/git/diff/route.test.mjs).node_modules/.bin/tsc --noEmit— clean, no output.npm run lint— clean, no output.The new test fails on the untouched base commit and passes with the fix. On the base commit (
git checkout origin/main -- lib/git-changes.ts lib/paths.ts, tests kept):With the fix:
The test builds a repository in a temporary folder, makes a symlink of its own to it (so it works on any POSIX system, without relying on
/tmpbeing a link) and is skipped on Windows.Provenance
Ported by hand from the Bun-running fork https://github.com/brynne8/pi-web-bun, commit
478d5178("fix(files): name a changed file by the path the tree was given"). The fork's commit also touchedlib/git-revert.ts— a write path — which has the same cause (it derives the checkout's path from the unresolved pair). That part is deliberately not part of this PR and should be done separately.Notes
getGitFileDiffdrops any candidate path that leaves the repository, and the routes' allowed-root rules are unchanged).