fix(git): the fetch timeout reaches the whole tree, a tick covers every 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.
This commit is contained in:
Devvyn committed 2026-10-25 22:31:25 +08:00
1 parent ea4318947b
commit 3411d7f35a
15 files changed
+686 -165

No files matched your search

+8 -4
View File
@@ -89,17 +89,21 @@ bounds the Git window's Fetch button; raise it for repositories on a slow networ
**Git fetch on an interval** (synced, off by default) runs `git fetch` on a timer for the repositories the
Git window lists, so the `behind` number is current without a click, and **Git fetch: how often** (synced,
30 to 3600 seconds, default 300) is the gap between runs for a case with no choice of its own — timed from
30 to 3600 seconds, default 300) is the gap between runs for a session that has no override of its own — timed from
the start of a run, so a slow repository does not push the next one out. These are the only Git settings
that are synced rather than per device, because the server's scheduler is what acts on them: kept per
device, the scheduler would read nobody's number. **Session Options → Git** overrides both the switch and
the interval for one session, and that override wins even while the app switch is off; a session that follows
the app shows the app's values and its rows go inert until you turn **Follow the app setting** off. Nothing is
ever fetched for a Docker or remote (SSH) session, and a repository a container can write to is refused
before any git runs. Sessions open on the same folder share one run. Both default to off because `git fetch`
is the one git command Codeman runs that writes, and the one that can run a repository's own hooks
before any git runs. Sessions open on the same folder share one run, and a folder is fetched when ANY session open on it is due, so the order the sessions were read cannot decide which interval wins. Both default to off because `git fetch`
is the one git command the Git window runs that writes — cloning and worktree management write too, but only
when you asked for them at the moment you asked — and the one that can run a repository's own hooks
(`reference-transaction`, `pre-auto-gc` through the maintenance a fetch triggers, and config commands such as
`credential.helper`).
`credential.helper`), and the one that can run later by itself, on a timer. The ssh it runs is BatchMode so
nothing waits for a login, and that is a default, never an override: your own `GIT_SSH_COMMAND`, or the
repository's `core.sshCommand`, is used when either is set. ⚠️ The default's `StrictHostKeyChecking=accept-new`
records an unknown host key in `~/.ssh/known_hosts` without asking — BatchMode stops a prompt, not a write.
Most default to off. The stock desktop header is system stats, File Viewer, Tiles, and the gear.
**Header Stats Style** picks how the system stats and plan usage are drawn: *Compact*
+9 -5
View File
@@ -254,16 +254,20 @@ Click it for a draggable window, in the style of the File Viewer:
remote (SSH) sessions.
It is read-only apart from that fetch: Codeman never commits, checks out or changes the
repository, and `git fetch` writes remote-tracking refs only — it is the one git command here that
writes, and the one that can run a repository's own hooks (`reference-transaction`, `pre-auto-gc` through the
maintenance a fetch triggers, and config commands such as `credential.helper`), which is why nothing runs on
a timer until you opt in. A fetch holds git's ref lock, so an agent's own `git pull` in the same repository
repository, and `git fetch` writes remote-tracking refs only — it is the only git command the Git window
runs that writes, the only one that can run a repository's own hooks (`reference-transaction`,
`pre-auto-gc` through the maintenance a fetch triggers, and config commands such as `credential.helper`),
and the only one that can run later by itself, on a timer: cloning and worktree management write too, but
only when you asked for them at the moment you asked, which is why nothing runs on a timer until you opt in. A fetch holds git's ref lock, so an agent's own `git pull` in the same repository
can hit the same `cannot lock ref`, and a folder is fetched by one run at a time. It is not shown for Docker
or remote (SSH) sessions, and a repository at or inside a Docker case
workspace is skipped even from a local session, before any git runs (a container can write there,
and git would run that repository's own configuration on the host). Fetch asks nothing of you: it
uses the sign-in your own git already has, and it gives up quickly rather than waiting on an
invisible login prompt. The
invisible login prompt. That BatchMode ssh is a default, never an override: your own
`GIT_SSH_COMMAND`, or the repository's `core.sshCommand`, is used when either is set, and ⚠️ the default's
`StrictHostKeyChecking=accept-new` records an unknown host key in `~/.ssh/known_hosts` without asking —
BatchMode stops a prompt, not a write. The
data comes from `GET /api/sessions/:id/git-status`, `GET /api/sessions/:id/git-diff` and, on that
click or the interval you turned on, `POST /api/sessions/:id/git-fetch`
(see the [API reference](https://github.com/Ark0N/Codeman/blob/master/docs/api-reference.md)).