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
+66
View File
@@ -10,6 +10,7 @@ import {
MAX_FILES,
parseCommitLog,
parsePorcelainV2,
runGit,
type GitRunner,
} from '../src/git-workspace-status.js';
@@ -591,6 +592,23 @@ describe('getGitWorkspaceOverview', () => {
expect(names(some)).toEqual(['real-project']);
});
it('identifies an unrelated repository above the workspace before its status runs, so a failure there cannot hide the repositories below', async () => {
repoAt(home);
const ws = join(home, 'case');
repoAt(join(ws, 'proj'));
const calls: Array<[string, string]> = [];
const spy: GitRunner = async (cwd, args) => {
calls.push([cwd, args[0]]);
if (cwd === ws && args[0] === 'status') throw Object.assign(new Error('timed out'), { killed: true });
return runGit(cwd, args);
};
const o = await getGitWorkspaceOverview(ws, { home, git: spy });
expect(o.state).toBe('ok');
expect(names(o)).toEqual(['proj']);
// The dotfiles repository costs one rev-parse, never a full status.
expect(calls.filter(([cwd]) => cwd === ws).map(([, verb]) => verb)).toEqual(['rev-parse']);
});
it('ignores a repository above the home folder (including a repo at the very top)', async () => {
repoAt(top); // contains `home`, so it is above it
const ws = join(home, 'case');
@@ -752,6 +770,30 @@ describe('getGitFileDiff', () => {
expect(cut.diff.length).toBeLessThanOrEqual(MAX_DIFF_BYTES);
expect(cut.diff.endsWith('x')).toBe(true);
});
it('cuts a diff that overflowed the output bound instead of failing it', async () => {
const partial = ('+' + 'y'.repeat(99) + '\n').repeat(Math.ceil(MAX_DIFF_BYTES / 100) * 2);
const git = vi.fn(async () => {
throw Object.assign(new RangeError('stdout maxBuffer length exceeded'), {
code: 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER',
stdout: partial,
});
});
for (const kind of ['unstaged', 'untracked'] as const) {
const d = await getGitFileDiff('/r', { path: 'huge.txt', kind }, { git });
expect(d.truncated).toBe(true);
expect(d.diff.length).toBeLessThanOrEqual(MAX_DIFF_BYTES);
expect(d.diff.endsWith('y')).toBe(true);
}
});
it('a real untracked file past the output bound comes back cut short, not as an error', async () => {
writeFileSync(join(repo, 'huge.txt'), ('z'.repeat(99) + '\n').repeat(100_000)); // ~10 MB, over runGit's 8 MB
const d = await getGitFileDiff(repo, { path: 'huge.txt', kind: 'untracked' });
expect(d.truncated).toBe(true);
expect(d.diff.startsWith('diff --git')).toBe(true);
expect(d.diff.length).toBeLessThanOrEqual(MAX_DIFF_BYTES);
}, 30_000);
});
describe('Docker case workspaces are never inspected', () => {
@@ -812,6 +854,30 @@ describe('Docker case workspaces are never inspected', () => {
expect(existsSync(join(dock, 'RAN'))).toBe(false);
});
it('re-checks a cached list of repositories against the Docker workspaces as they are now', async () => {
let t = 1_000_000;
const ws = join(home, 'case');
repoAt(join(ws, 'a'));
repoAt(join(ws, 'b'));
const cwds: string[] = [];
const spy: GitRunner = (cwd, args) => {
cwds.push(cwd);
return runGit(cwd, args);
};
const first = await getGitWorkspaceOverview(ws, { home, git: spy, now: () => t });
expect(first.repos.map((r) => r.path)).toEqual(['a', 'b']);
t += 5000; // past the status cache, well inside the list's 30 s
cwds.length = 0;
const o = await getGitWorkspaceOverview(ws, {
home,
git: spy,
now: () => t,
dockerWorkspaces: [join(ws, 'a')],
});
expect(o.repos.map((r) => r.path)).toEqual(['b']);
expect(cwds).not.toContain(join(ws, 'a'));
});
it('sees through a symlink to the workspace', async () => {
const dock = booby(join(home, 'dock'));
const ws = join(home, 'case');