Repository navigation
feat(git): a Fetch button in the Git status header, and an interval for it app-wide and per case - #714
feat(git): a Fetch button in the Git status header, and an interval for it app-wide and per case#714opticon454 wants to merge 3 commits into
Conversation
…or it app-wide and per case Fetch is the only git command Codeman runs that writes, and the only one that runs a repository's own fetch-post hook, so it stays behind a click and off by default. The Git status window's header row gains Fetch; App Settings -> Bottom bar gains the switch and the interval, synced because the scheduler acts on them, and Session Options -> Git overrides both per case. The scheduler reads settings fresh each tick, times the interval from the start of a run, never overlaps runs, and re-checks the Docker and remote rules every tick before any git runs. Nothing but remote-tracking refs moves: HEAD, the index and the working tree are proven unchanged against real git while origin/main advances. Tests: src/git-fetch.ts and the scheduler, the per-case settings route, the synced/display setting split, the two new i18n rows, and the Fetch block of the Git status browser suite.
|
Pushed as Two things in it that the diff alone does not make obvious:
The |
|
Thanks for this, and for following your Git status work through with it. The PR adds a Fetch button to the Git status window and an opt-in fetch interval, app-wide in Settings and per session in Session Options → Git. The fetch itself is careful: fixed argv, the Docker rule checked before any git runs, a non-interactive environment, sanitized failure text, and the two settings wired as synced the way CLAUDE.md asks. Typecheck, lint, format, frontend syntax, the full 1. Overlapping ticks run a queued case twice ( 2. Sessions on the same folder fetch the same repository at the same time ( 3. Bounded probe before the unattended run ( 4. 5. 6. Two small wording fixes while you are in there: Once the scheduler changes are in, this will be a quick second look: the route, the fetch module, the settings wiring and the tests are in good shape. |
…e review (review) The scheduler is the part that runs with nobody watching, so it is decided per folder rather than per session: sessions sharing a folder are the same clone and share one run that both see, `inFlight`/`lastRunAt` are keyed by the resolved folder, the schedule is stamped when a folder is picked instead of when its run starts, and a tick is skipped while the previous one is still going. The click route joins a run already going for the same folder rather than starting a second fetch in one clone. The tick keeps ONE bound for the git processes of the whole run and hands it to every folder, and bounds its folders with a bound of its own: the same semaphore cannot serve both levels, because a folder holding a slot would then wait for a slot its own fetch needs. Both bounds are `FETCH_CONCURRENCY`, so the ceiling is unchanged. The bound claim in CLAUDE.md, the `Limiter` jsdoc and docs/api-reference.md now describes it as it is. Before anything is fetched the folder is probed with the bounded path probe, so a case on a mount that stopped answering is skipped and never touched. The rest of the review: the hooks are named as they really are (`reference-transaction`, `pre-auto-gc` through the maintenance a fetch triggers, config commands such as `credential.helper`) and `FETCH_CONFIG_ARGS` turns maintenance and gc off for an unattended run; the claim that an agent in the same repository is not disturbed is replaced by the ref-lock collision it actually is; the click route publishes its own run as `trigger: 'click'`; `autoSaveGitFetch` uses `_apiJson` so a 400 shows the error toast and applies the returned settings; and the two wording fixes. Tests: the scheduler pins one run per folder, the two bounds, the probe before anything runs, and that closing the session holding the override keeps the folder while another session is still open on it; the route test pins the click publish and the join, asserting the join before the held run is released — releasing first lets the run finish and clears the lane, so the joiner would be entitled to its own fetch and the test would pass for the wrong reason.
|
Thanks — the scheduler round is in, pushed as 1. Overlapping ticks — the schedule is stamped at selection: 2. Sessions on the same folder — grouped by On the open question I'd keep storage per session and present it as per session: the row now reads "Fetch this session" (it said "Fetch this case"), and the scheduler remembers every session on a folder, so closing the one that holds the override no longer stops the folder's fetching while another session is still open on it. What per-session storage can't fix is two sessions in one case disagreeing — that's now the choice the row is labelled as, rather than a claim about the case. Happy to move it to per-case storage if you'd rather, but that's a storage change plus a route change, so I didn't do it unasked. 3. Bounded probe — 4. Hook names — CLAUDE.md, 5. 6. Both wording fixes are in: One thing I deliberately didn't change: the first commit's message still says "fetch-post hook". I'd rather not amend a commit that is already pushed, so the corrected naming lives in the follow-up commit and in the code. Gate here: typecheck, lint, |
|
Thanks for the second round, and for taking every point from the first one so thoroughly. To recap for anyone reading along: this PR adds a Fetch button to the Git status window and an opt-in fetch interval, app-wide in Settings and per session in Session Options → Git, run by a server-side scheduler. All six round-one items and both wording fixes are in: the schedule is stamped at selection, a tick is skipped while the previous one runs, one shared limiter bounds the fetches, folders are grouped and the click joins a running fetch, the bounded probe runs first, the hooks are named correctly with 1. A timed-out fetch leaves git's children running ( 2. Sessions on one folder do not all get its run ( The question from round one is still open: whether to store the override per case, or present it as a per-session setting as this round does. The two-pass fix is needed either way. 3. 4. "The only git command Codeman runs that writes, and the only one that can run a repository's own hooks" ( 5. Two leftovers in 6. Small ones: Once 1 and 2 are in, this is a short third look: the routes, the settings wiring, the bounded probe and the tests are in good shape. |
…ry session on its folder, and the rest of the review (review) `execFile` signals the `git` parent only, so a fetch that times out can leave its http helper still running, and on the interval that repeats every run with nobody watching. `runFetch` now spawns `detached`, so the fetch and the helper it spawns share one process group, and the kill is SIGTERM to the negative pid then SIGKILL `FETCH_KILL_ESCALATION_MS` later; the escalation is not cancelled when the promise settles, because the caller is answered at the timeout while the group is still dying. The output cap is kept, and the clone's slot queue is not needed here. The tick is two passes: every enabled local session is collected by folder, a folder is due when ANY session open on it is due, the folder is probed once for the group, and the run is published to every session on it. A single pass decided by the order the sessions were read, so [long, short] published only the short one and the 3600 s session's row never updated again while its folder was fetched every 30 s. `gitNonInteractiveEnv` writes BatchMode ssh as a default, not an override: `core.sshCommand` is read inside the same slot and the same bound as the fetch it serves, and `fetchEnvFor` keeps the repository's answer when it has one, because that is usually the setup that works on a machine with several ssh setups. The `accept-new` host key it can write is stated next to the BatchMode note: a prompt is stopped, a write is not. The write claim is scoped to the Git panel and to the timer, with cloning and worktree management named as the writes that happen when the user asked for them, and the zh keys sit with the group of the UI surface their English string belongs to. Tests: the process-group test proves the tree by group membership, because the helper is a grandchild (`git fetch` runs `git remote-http`, which spawns `git-remote-http`) and because the group is what the negative pid reaches, and it remembers each process once found instead of re-scanning a group it had already emptied. The non-zero-exit test changed premises: `git fetch --quiet` in a repository with no remote exits 0 on git 2.47.3, so resolving empty stdout there was correct, and the case that has to reject is a remote pointing at a path that is not a repository. The join test asserts after the run and does not await the request before the held run is released, which would wait on the run the test is holding.
|
Round 2 actioned, in the code. 1. The timeout now reaches the whole tree ( The non-zero-exit test changed premises while I wrote it: 2. A tick is two passes ( 3. 4. Scoped as suggested: "the only git command the Git panel runs that writes, and the only one Codeman runs on a timer", with cloning and worktree management named as the writes that happen when the user asked. On the zh keys I read the instruction as "each key sits with the group of the UI surface its English string belongs to": the two App Settings → Bottom bar rows moved into the Bottom bar group in the order the page lists them, and the Session Options → Git keys are their own block in the session-dialogs region. Since a dictionary key is the English string it translates, the reworded sentences had to change in both places, and 5. Both leftovers rewritten: line 15 now says the panel fetches on the click, or on the interval when the session is on; the "not disturbed" line now says what is true — nothing commits or touches the working tree, but a fetch does hold git's ref lock, so an agent's 6. CLAUDE.md names The open question from round 1, unchanged: per-session storage, with the UI reading "Fetch this session". The two-pass fix is what carries the folder either way, so I did not change the storage. Gate: typecheck, lint, |
|
Thanks for the third round, and for following every point from the second one through to a test. This PR adds a Fetch button to the Git status window and an opt-in fetch interval, app-wide in Settings and per session in Session Options → Git, run by a server-side scheduler. All of round two is in: the timeout now reaches the whole process group (your test against a server that never answers passes here), a tick decides per folder in a second pass with both orders pinned, a repository's Three small bugs are left, each reproduced against the shipped code: 1. A session update resets "How often" while you type ( 2. A partial override shows defaults instead of the app values ( 3. The Smaller ones:
The design question from the first two rounds is still open: whether the override is stored per case, or stays per session as the Session Options rows now present it. All of the above is small, so I will fix it on top at merge time; you do not need to push another round for it. |
What
Two things for the Git status window:
git fetchfor the repositories the panel lists and reports each repository's result.Why it stays opt-in
git fetchis the only git command Codeman runs that writes, and the only one that runs a repository's ownfetch-posthook. An agent is often working in the same repository, so fetch moves remote-tracking refs and nothing else: HEAD, the index and the working tree are unchanged.Details worth a look
displayKeysand out of thesaveAppSettingsstrip destructure insettings-ui.js— a per-device value never reaches the server.FETCH_CONCURRENCY4, repo limit 12/50, 2 MB output cap, the git timeout per spawn, non-interactive env (GIT_TERMINAL_PROMPT=0+ BatchMode ssh).POST /api/sessions/:id/git-fetch-settings:nullclears that half, a field left out leaves the other,400for a remote or Docker case.SessionUpdatedevent asgitFetch; no new SSE event. Failure text reaching a client is control-stripped and credential-redacted, and the browser writes it withtextContent.Tests
test/git-fetch.test.ts— the fixed argv, the failure classification, and the two reads the scheduler depends on; a real git against a real local bare repository, proving the remote-tracking ref moves while HEAD, the index and the working tree do not.test/git-fetch-scheduler.test.ts— tick semantics, override precedence, no overlap, Docker/remote refusal.test/routes/session-routes-git-fetch-settings.test.ts,test/app-settings-structure.test.ts(the synced/display split),test/session-options-structure.test.ts,test/git-fetch-i18n.test.ts(the two new zh-CN rows).test/git-status.browser.test.ts— the Fetch block, 22 tests (browser suite, not in thenpm testgate).Full gate: 635 test files passed (1 skipped), 12638 tests passed (12 skipped). Typecheck, lint,
format:check,check:frontend-syntax(47 JS) andcheck:public-assets(68) are clean. No changesets added, nothing deployed.Docs: CLAUDE.md, README,
docs/api-reference.md,docs/wiki/Settings-Reference.md,docs/wiki/Working-With-Files.md.