fix(git): point the Changes tab and file browser at the directories they describe (#710) - #715
Open
edspencer wants to merge 2 commits into
Open
fix(git): point the Changes tab and file browser at the directories they describe (#710)#715edspencer wants to merge 2 commits into
edspencer wants to merge 2 commits into
Conversation
added 2 commits
August 6, 2026 23:23
…hey describe Verifying #710 (a project whose workingDir is a linked git worktree) turned up three surfaces left behind when #709 moved status/diff/commit onto workingDir. Each failed silently, and all three are visible on any project where the working directory is not the metadata directory — a worktree, a linked checkout, or a plain repo-backed clone. - The Changes tab's new-file view fetched an untracked file's content from /files/:name, the METADATA-dir surface, so every untracked row in the pane rendered 'File not found'. It now reads GET /git/file, which serves from the working directory and gates on git's own answer: a path git reports as untracked, and nothing else. That is both tighter than the dot-segment guard (.git is never reported, in a worktree it is a FILE and so would have passed as a leaf, disclosing the main checkout's gitdir; ignored files are never reported either) and looser where it should be (a new .github/workflows/ci.yml is an ordinary row the pane lists and must be able to render). - Remote and push were still hard-wired to projectsRoot while the header beside them showed the project's own branch, so the Push button offered to push Paddock's notes repo. Both take a directory now, defaulting to the store, and the pane asks about its own working directory. A notebook project resolves to the same repo as before. - The file browser joined projectsRoot + slug, which is the content dir only when the project is unmanaged or a notebook. A MANAGED project with a path: keeps its curated trio out at that path, so its Files tab listed nothing but project.yaml. It now browses contentDir — the same resolution the sweeper uses. The dot-segment guard on the notes surface is unchanged; the new working-dir reads opt out of it explicitly, with a git-verified allowlist in hand. Refs #710
Deploying paddock with
|
| Latest commit: |
e4907a2
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://ab3f8e98.paddock-7u2.pages.dev |
| Branch Preview URL: | https://fix-710-worktree-changes-tab.paddock-7u2.pages.dev |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Verifies #710 — a project whose
workingDiris a linked git worktree — and fixes what that turned up.The headline is that most of the surface was already right.
status,diffandcommitact on the worktree; the branch shown is the worktree's own; a commit lands on that branch and leaves the main checkout untouched; the grid's "N uncommitted" badge already covers linked projects throughdirtyCountAt. Three things had been left behind when #709 moved status/diff/commit ontoworkingDir, and each failed silently rather than loudly. All three are visible on any project whose working directory isn't its metadata directory — a worktree, a linked checkout, or a plain repo-backed clone.1. Every new file in the Changes tab rendered "File not found"
An untracked file has no diff, so the pane renders its content instead — and it fetched that from
/files/:name, the metadata-dir browser. For a linked or repo-backed project that is a different directory, so every untracked row 404'd. Reproduced on both shapes; a notebook project (where the two directories coincide) was the control that still worked.The pane now reads a new
GET /api/projects/:slug/git/file, served from the working directory and gated on git's own answer: a pathgit statusreports as untracked, and nothing else.That gate is deliberately not the dot-segment guard the notes browser uses, which is wrong here in both directions — the security-adjacent question the ticket asked to decide rather than leave to chance:
.gitis a file, not a directory, so it is the leaf — and a dotfile leaf is deliberately allowed there (an untracked.gitignorehas to render, per Files tab: browse subdirectories with nested URLs (flat top-level-only today) #259). Serving it would disclose the main checkout's gitdir path for no reason. The realconfigis safe either way: it lives outside the working directory, so containment already refuses it..github/workflows/ci.ymlis an ordinary row the pane lists, and a dot-directory rule refuses to render exactly that.Git never reports
.git, and never reports an ignored path, so.chats/and anything the repo excludes stay unreadable — while.github/…renders. The servable set is by construction the set the pane already displays, rather than a third notion of "allowed" to keep in step with the other two. The notes surface keeps its existing guard unchanged; the working-dir reads opt out of it explicitly, with the git-verified allowlist in hand.2. The Push button pushed the wrong repository
remote()andpush()were still hard-wired toprojectsRootwhile the header beside them showed the project's own branch. On a linked project the header readfeat/ad-detector · 3 uncommitted · [Push ↑N]where the branch was the worktree's and the ahead-count, remote URL and Push target were Paddock's own notes repo. In the test rig the store had no remote so the button merely sat disabled; on a real box the store does have one, and pressing it pushes the notes repo while the user believes they are pushing their worktree.Both methods take a directory now, defaulting to the store, and the pane asks
GET /api/projects/:slug/git/remote/POST /api/projects/:slug/git/pushabout its own working directory. The GitHub device-flow connection is genuinely fleet-level and rides along in the same payload, so the header is still one fetch. For a notebook project the working directory is insideprojectsRoot, so this resolves to the same repo, branch and upstream as before — a strict generalisation.3. A managed project with a
path:had an empty Files tabcontentDirForputs a managed project's curatedCLAUDE.md/OVERVIEW.md/CHANGELOG.mdout at the nominated path, but the browser joinedprojectsRoot + slugand listed a directory holding onlyproject.yaml;/files/CLAUDE.md404'd.project-files.tsnow takes a resolved directory instead of(root, slug)and the store passes the content dir — the same resolution the sweeper writes through — so there is one copy of it rather than two. Unmanaged and notebook projects are unchanged.The other ticket items, confirmed working
isRepo()ancestor-walk + cache (item 4). Nothing reports repo-ness for the wrong repo.repoAtis keyed by resolved path, and a non-repo linked directory stays a non-repo even while both the store and the worktree are repos. A linked path that is a subdirectory of some other repo does light up git for that enclosing repo, scoped correctly to the subtree by--show-prefix/--relative— which is a reasonable reading of "you pointed at a directory inside a repo", and now has a test pinning it.--abbrev-ref HEADis per-worktree and showsfeature-xwhile the main checkout sits onmain. With feat: project 'overview' + auto-update sweep (post-turn, rate-limited) #2 fixed, the whole header now describes one repository.dirtyCounts(item 3). Linked projects are not missing from the badge — feat!: managed/unmanaged projects, apath:on both axes, and the Changes tab on workingDir (#206, #597) #709 already added thedirtyCountAt(workingDir)fallback for exactly the projects the cheap store-wide sweep structurally cannot see. Verified end to end, with a test so it stays that way. No change needed.Testing
packages/server/test/integration/git-worktree.test.tsdrives the real routes against a real worktree with a real bare origin: 14 tests, of which 7 fail onmainand 7 are confirmations of what already worked. Full suite green (1932 server, 946 web), typecheck clean, OpenAPI spec regenerated (+3 routes).Also QA'd live against a built instance with a genuine linked worktree — the screenshots in the issue thread show the header on
feat/ad-detectorand an untracked.github/workflows/ci.ymlrendering, which is both the 404 and the over-blocking case in one row.Out of scope, as the ticket specified
Transcript relocation for Claude-Code-native worktrees (adjacent to #708) and branch-scoped chats (#213) are untouched. Nothing here needs either: a linked worktree is just a directory to Paddock, and
ensureProjectChatskeys onworkingDiras it always has.Closes #710