fix(git-status): landing fixes (#537)

- A cached list of repositories below a folder is re-checked against the
  Docker case workspaces as they are now, so a repository linked as a Docker
  workspace within the 30 s list cache is no longer inspected.
- A diff past runGit's 8 MB output bound is cut short from git's partial
  output instead of failing with a 500.
- The browser test waits for its slow route handler on unroute
  (unrouteAll behavior 'wait'), so a late route.continue() cannot fail the run.
- "Upstream is gone" now reads "Upstream not on remote", true for a branch
  that was never pushed as well as one deleted on the remote; docs mirrored.
- The diff route checks the repository against the workspace's own cached
  repository list (findWorkspaceRepo) and refreshes only that repository,
  instead of a fresh status of every repository in the folder.
- CLAUDE.md: a Key Patterns entry for the git read surface and its rules.
- The enclosing repository is identified with one cached rev-parse before
  any full status, so an unrelated repository above the workspace costs one
  process and its failure no longer hides the repositories below.
- Wiki: the bottom-bar indicator moves out of the header-controls table.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-10-05 19:51:59 +02:00
parent cf26853390
commit 2c38e77f8a
10 changed files with 283 additions and 83 deletions
+4 -3
View File
@@ -399,8 +399,9 @@ Object.assign(CodemanApp.prototype, {
const line = el('div', 'git-status-branchline');
if (data.upstream && data.upstreamGone) {
line.append(el('span', 'git-status-chip', `${data.branch || 'HEAD'} → ${data.upstream}`));
const gone = el('span', 'git-status-chip git-status-chip--warn', 'Upstream is gone');
gone.title = 'The remote branch was deleted (and pruned), so the commits below are on no remote.';
const gone = el('span', 'git-status-chip git-status-chip--warn', 'Upstream not on remote');
gone.title =
'The upstream branch does not exist on the remote (never pushed, or deleted and pruned), so the commits below are on no remote.';
line.append(gone);
} else if (data.upstream) {
line.append(el('span', 'git-status-chip', `${data.branch || 'HEAD'} → ${data.upstream}`));
@@ -473,7 +474,7 @@ Object.assign(CodemanApp.prototype, {
'div',
'git-status-note',
data.upstreamGone
? 'The upstream branch is gone from the remote, so these commits are on no remote.'
? 'The upstream branch does not exist on the remote (never pushed, or deleted), so these commits are on no remote.'
: 'This branch has no upstream, so these commits are on no remote yet.'
)
);
+13 -8
View File
@@ -18,8 +18,10 @@ import { getDataDir } from '../../config/instance.js';
import { findSessionOrFail } from '../route-helpers.js';
import {
emptyOverview,
findWorkspaceRepo,
getGitFileDiff,
getGitWorkspaceOverview,
getGitWorkspaceStatus,
type GitFileDiff,
type GitFileKind,
type GitRunner,
@@ -55,7 +57,9 @@ export function registerGitStatusRoutes(
// The diff of one file the panel lists. `repo` and `path` are matched against the CURRENT status
// (a repository this session's folder holds, a path git reported in it) rather than trusted, so
// the route cannot be pointed at an arbitrary directory or file.
// the route cannot be pointed at an arbitrary directory or file. The repository is checked against
// the overview's own (cached) list with the Docker roots as they are now, and only that one
// repository's status is refreshed: a click must not re-read every repository in the folder.
app.get('/api/sessions/:id/git-diff', async (req, reply): Promise<ApiResponse<GitFileDiff>> => {
const { id } = req.params as { id: string };
const { repo, path, kind } = req.query as { repo?: string; path?: string; kind?: string };
@@ -64,13 +68,14 @@ export function registerGitStatusRoutes(
reply.code(400);
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Git is not available for remote or Docker sessions');
}
const overview = await getGitWorkspaceOverview(session.workingDir, {
git,
fresh: true,
dockerWorkspaces: await dockerWorkspaces(),
});
const status = overview.repos.find((r) => r.status.repoRoot === repo)?.status;
const entry = status?.files.find((f) => f.path === path && f.kind === (kind as GitFileKind));
const repoRoot = repo
? await findWorkspaceRepo(session.workingDir, repo, { git, dockerWorkspaces: await dockerWorkspaces() })
: null;
const status = repoRoot ? await getGitWorkspaceStatus(repoRoot, { git, fresh: true }) : null;
const entry =
status?.state === 'ok'
? status.files.find((f) => f.path === path && f.kind === (kind as GitFileKind))
: undefined;
if (!status?.repoRoot || !entry) {
reply.code(404);
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'That file has no outstanding change any more');