fix(git-status): address #537 review (docker workspaces, gone upstream, in-flight reset, docs)

- never inspect a repository at or inside a Docker case workspace (walk-up, scan, diff route): git would run its clean filters on the host
- a branch whose upstream was deleted and pruned reports upstreamGone and falls back to commits on no remote, instead of green
- turning the setting off during a poll releases the in-flight flag
- log.showSignature=false; reword the docs: clean filters still run
- CLAUDE.md frontend load order, changeset names git-diff
- discovery reads a bounded, sorted directory listing; leading-dash paths allowed; diff 500 redacts credentials
- keyboard focus survives the poll re-render; panel stays on screen on narrow viewports; aria-expanded visible on light skins

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JrzFKEdBLwVfu6ev2ZscJS
This commit is contained in:
Devvyn
2026-10-05 11:49:38 +08:00
co-authored by Claude Sonnet 5.5
parent b2423c90ce
commit 0c4bb5169f
11 changed files with 352 additions and 49 deletions
+106 -1
View File
@@ -33,6 +33,18 @@ describe('parsePorcelainV2', () => {
});
});
it('flags an upstream whose remote branch is gone: branch.upstream without branch.ab', () => {
expect(
parsePorcelainV2(['# branch.oid x', '# branch.head feature', '# branch.upstream origin/feature'].join(NUL) + NUL)
).toMatchObject({ upstream: 'origin/feature', upstreamGone: true });
expect(
parsePorcelainV2(
['# branch.oid x', '# branch.head main', '# branch.upstream origin/main', '# branch.ab +0 -0'].join(NUL) + NUL
)
).toMatchObject({ upstreamGone: false });
expect(parsePorcelainV2(['# branch.oid x', '# branch.head feature'].join(NUL) + NUL).upstreamGone).toBe(false);
});
it('reads a detached HEAD and a branch with no upstream (no branch.ab line either)', () => {
expect(parsePorcelainV2(['# branch.oid x', '# branch.head (detached)'].join(NUL) + NUL)).toMatchObject({
branch: null,
@@ -314,6 +326,21 @@ describe('getGitWorkspaceStatus against a real repository', () => {
expect(s.unpushed.map((c) => c.subject)).toEqual(['f2', 'f1']);
});
it('a branch whose upstream was deleted and pruned is NOT reported as everything pushed', async () => {
git(repo, 'checkout', '-q', '-b', 'feature');
write('f1.txt');
commit('f1');
git(repo, 'push', '-q', '-u', 'origin', 'feature');
git(repo, 'push', '-q', 'origin', '--delete', 'feature');
git(repo, 'fetch', '-q', '--prune');
write('f2.txt');
commit('f2');
const s = await getGitWorkspaceStatus(repo);
expect(s).toMatchObject({ branch: 'feature', upstream: 'origin/feature', upstreamGone: true });
expect(s.unpushedCount).toBe(2);
expect(s.unpushed.map((c) => c.subject)).toEqual(['f2', 'f1']);
});
it('a pushed branch is not reported as unpushed once it has an upstream', async () => {
git(repo, 'checkout', '-q', '-b', 'feature');
write('f1.txt');
@@ -668,7 +695,8 @@ describe('isSafeRepoRelativePath', () => {
['a.txt', true],
['src/deep/x.ts', true],
['', false],
['-rf', false],
['-rf', true], // every operand follows `--`, so a leading dash is just a name
['-', true],
['/etc/passwd', false],
['../x', false],
['a/../../x', false],
@@ -725,3 +753,80 @@ describe('getGitFileDiff', () => {
expect(cut.diff.endsWith('x')).toBe(true);
});
});
describe('Docker case workspaces are never inspected', () => {
let top: string;
let home: string;
const repoAt = (p: string): string => {
mkdir(p, { recursive: true });
git(p, 'init', '-q', '-b', 'main');
writeFileSync(join(p, 'f.txt'), '1\n');
git(p, 'add', '-A');
git(p, 'commit', '-q', '-m', 'c');
return p;
};
/** A repository whose clean filter drops a marker file: proof that git ran on it. */
const booby = (p: string): string => {
repoAt(p);
git(p, 'config', 'filter.mark.clean', 'touch RAN; cat');
writeFileSync(join(p, '.gitattributes'), 'f.txt filter=mark\n');
// Same size as the committed '1\n', so git must read the content (running the filter) to see the change.
writeFileSync(join(p, 'f.txt'), 'x\n');
return p;
};
beforeEach(() => {
top = mkdtempSync(join(tmpdir(), 'git-docker-'));
home = join(top, 'home');
mkdir(home, { recursive: true });
clearGitStatusCache();
});
afterEach(() => rmSync(top, { recursive: true, force: true }));
it('control: without the exclusion git does run the repository’s clean filter', async () => {
const ws = join(home, 'case');
booby(join(ws, 'proj'));
await getGitWorkspaceOverview(ws, { home, git: undefined });
expect(existsSync(join(ws, 'proj', 'RAN'))).toBe(true);
});
it('drops a Docker workspace found below the folder, and runs nothing in it', async () => {
const ws = join(home, 'case');
booby(join(ws, 'sandbox'));
repoAt(join(ws, 'plain'));
const o = await getGitWorkspaceOverview(ws, { home, dockerWorkspaces: [join(ws, 'sandbox')] });
expect(o.repos.map((r) => r.path)).toEqual(['plain']);
expect(existsSync(join(ws, 'sandbox', 'RAN'))).toBe(false);
});
it('answers unsupported/docker for a folder at or inside a Docker workspace, before any git runs', async () => {
const dock = booby(join(home, 'dock'));
mkdir(join(dock, 'sub'));
for (const cwd of [dock, join(dock, 'sub')]) {
clearGitStatusCache();
const git = vi.fn(async () => '');
const o = await getGitWorkspaceOverview(cwd, { home, git, dockerWorkspaces: [dock] });
expect(o).toMatchObject({ state: 'unsupported', reason: 'docker', repos: [] });
expect(git).not.toHaveBeenCalled();
}
expect(existsSync(join(dock, 'RAN'))).toBe(false);
});
it('sees through a symlink to the workspace', async () => {
const dock = booby(join(home, 'dock'));
const ws = join(home, 'case');
mkdir(ws, { recursive: true });
symlink(dock, join(ws, 'link'));
const o = await getGitWorkspaceOverview(join(ws, 'link'), { home, dockerWorkspaces: [dock] });
expect(o.state).toBe('unsupported');
expect(existsSync(join(dock, 'RAN'))).toBe(false);
});
it('a folder next to the workspace, and one whose name merely starts the same, are not excluded', async () => {
const dock = join(home, 'dock');
const sibling = repoAt(join(home, 'dock-two'));
mkdir(dock, { recursive: true });
const o = await getGitWorkspaceOverview(sibling, { home, dockerWorkspaces: [dock] });
expect(o.state).toBe('ok');
});
});