From a07ca77b69ea94132a8a7eef8ce3ad05cce5c2e2 Mon Sep 17 00:00:00 2001 From: esengine <359807859@qq.com> Date: Sun, 18 Oct 2026 08:50:37 -0700 Subject: [PATCH] fix(git): repositories are listed once and kept until the structure changes, reads are settled and not repeated, a kernel without the route keeps the old behaviour --- desktop/frontend-next/perf/git-panel.mjs | 4 +- desktop/frontend-next/perf/harness.tsx | 6 +- desktop/frontend-next/src/actions.ts | 1 + desktop/frontend-next/src/i18n/en.ts | 1 + desktop/frontend-next/src/port/mock_git.ts | 2 +- desktop/frontend-next/src/port/workspace.ts | 3 - .../frontend-next/src/state/gittree.test.ts | 2 +- .../frontend-next/src/ui/WorkbenchChanges.tsx | 1 + desktop/frontend-next/src/ui/WorkbenchGit.tsx | 31 ++-- .../frontend-next/src/ui/WorkbenchPanel.tsx | 8 +- .../src/ui/git-panel.interaction.test.tsx | 2 +- .../src/ui/git-refresh.interaction.test.tsx | 143 ++++++++++++++++++ desktop/frontend-next/src/ui/useGitRepos.ts | 48 ++++-- .../frontend-next/src/ui/useRepoChanges.ts | 54 +++++++ internal/frontend/serve/changes.go | 21 +-- internal/platform/gitrepos/cache.go | 89 +++++++++++ internal/platform/gitrepos/gitrepos.go | 83 +++------- internal/platform/gitrepos/gitrepos_test.go | 82 ++++++++-- internal/platform/gitstatus/commitdetail.go | 11 +- .../platform/gitstatus/commitdetail_test.go | 15 ++ internal/platform/gitstatus/summary.go | 17 +++ internal/session/control/controller_deps.go | 3 + internal/session/control/git_repos.go | 21 ++- 23 files changed, 531 insertions(+), 117 deletions(-) create mode 100644 desktop/frontend-next/src/ui/git-refresh.interaction.test.tsx create mode 100644 desktop/frontend-next/src/ui/useRepoChanges.ts create mode 100644 internal/platform/gitrepos/cache.go diff --git a/desktop/frontend-next/perf/git-panel.mjs b/desktop/frontend-next/perf/git-panel.mjs index 5bfffae893..6fccb973ab 100644 --- a/desktop/frontend-next/perf/git-panel.mjs +++ b/desktop/frontend-next/perf/git-panel.mjs @@ -29,7 +29,9 @@ try { if (!shotDir) return; await page.mouse.move(width - 3, 995); await page.waitForTimeout(250); - await page.locator(".workbench-explorer").first().screenshot({ path: `${shotDir}/${scheme}-${width}-${name}.png` }); + const box = await page.locator(".workbench-explorer").first().boundingBox(); + // At phone width the composer's toolbar is drawn over the foot of the column; the panel's own picture stops above it. + await page.screenshot({ path: `${shotDir}/${scheme}-${width}-${name}.png`, clip: { x: box.x, y: box.y, width: box.width, height: box.height - (width <= 700 ? 150 : 0) } }); }; const one = await open(""); diff --git a/desktop/frontend-next/perf/harness.tsx b/desktop/frontend-next/perf/harness.tsx index 17c8b36430..71a3b2742e 100644 --- a/desktop/frontend-next/perf/harness.tsx +++ b/desktop/frontend-next/perf/harness.tsx @@ -101,9 +101,9 @@ class BenchPort extends MockPort { // ?git=multi is a folder of projects: three repositories, each with changes of its own. if (query.get("git") === "multi") { this.repoRows = [ - { dir: "api", name: "api", branch: "main", detached: false, added: 3, removed: 1, untracked: 1 }, - { dir: "web", name: "web", branch: "feature/login", detached: false, added: 5, removed: 2, untracked: 0 }, - { dir: "tools/release", name: "release", branch: "main", detached: false, added: 0, removed: 0, untracked: 0 }, + { dir: "api", name: "api", branch: "main", detached: false }, + { dir: "web", name: "web", branch: "feature/login", detached: false }, + { dir: "tools/release", name: "release", branch: "main", detached: false }, ]; this.repoChanges = { api: [ diff --git a/desktop/frontend-next/src/actions.ts b/desktop/frontend-next/src/actions.ts index 877b0d5ac7..f76b560077 100644 --- a/desktop/frontend-next/src/actions.ts +++ b/desktop/frontend-next/src/actions.ts @@ -691,6 +691,7 @@ export const ACTIONS: UIAction[] = [ { id: "workbench.discard-confirm", kind: "kernel-mutation", target: "entity", proof: "authority-effect" }, { id: "workbench.discard-cancel", kind: "view", target: "none", proof: "interaction" }, { id: "workbench.repo", kind: "view", target: "entity", proof: "interaction" }, + { id: "workbench.repos-reload", kind: "view", target: "none", proof: "interaction" }, { id: "workbench.commit", kind: "view", target: "entity", proof: "interaction" }, { id: "workbench.commit-file", kind: "view", target: "entity", proof: "interaction" }, { id: "push.preview", kind: "view", target: "none", proof: "interaction" }, diff --git a/desktop/frontend-next/src/i18n/en.ts b/desktop/frontend-next/src/i18n/en.ts index dd4470d12a..9da58416db 100644 --- a/desktop/frontend-next/src/i18n/en.ts +++ b/desktop/frontend-next/src/i18n/en.ts @@ -146,6 +146,7 @@ export const EN: Record = { "确认丢弃": "Confirm discard", "丢弃 {path} 的改动?这个文件会回到上次提交的样子,改动无法恢复。": "Discard the change to {path}? The file goes back to how the last commit has it, and the change cannot be recovered.", "丢弃": "Discard", + "它已暂存的部分和还没暂存的部分都会丢弃。": "Both its staged part and its unstaged part are discarded.", "无法读取这个仓库的提交历史": "Could not read this repository's commit history", "这不是一个提交编号": "That is not a commit id", "这个仓库里没有这个提交": "This repository has no such commit", diff --git a/desktop/frontend-next/src/port/mock_git.ts b/desktop/frontend-next/src/port/mock_git.ts index eceef7a5cb..1422bba101 100644 --- a/desktop/frontend-next/src/port/mock_git.ts +++ b/desktop/frontend-next/src/port/mock_git.ts @@ -10,7 +10,7 @@ const refuse = (status: number, code: string, error: string) => new HttpError(st // write is seen by the next read the way the kernel's would be. export class MockGit extends MockFeedback { // One entry is the workspace being a repository; several are a folder of projects. - repoRows: GitRepoRow[] = [{ dir: "", name: "reasonix", branch: "main", detached: false, added: 2, removed: 1, untracked: 3 }]; + repoRows: GitRepoRow[] = [{ dir: "", name: "reasonix", branch: "main", detached: false }]; // The changes of each repository below the workspace; the workspace's own are MockWorkspace.changeList. repoChanges: Record = {}; diff --git a/desktop/frontend-next/src/port/workspace.ts b/desktop/frontend-next/src/port/workspace.ts index 48909439b5..5569b9d236 100644 --- a/desktop/frontend-next/src/port/workspace.ts +++ b/desktop/frontend-next/src/port/workspace.ts @@ -198,9 +198,6 @@ export interface GitRepoRow { name: string; branch: string; detached: boolean; - added: number; - removed: number; - untracked: number; } // The workspace's repositories; truncated says a search bound ended the look early. diff --git a/desktop/frontend-next/src/state/gittree.test.ts b/desktop/frontend-next/src/state/gittree.test.ts index 856c06b16f..5973845f01 100644 --- a/desktop/frontend-next/src/state/gittree.test.ts +++ b/desktop/frontend-next/src/state/gittree.test.ts @@ -43,7 +43,7 @@ describe("the changes tree", () => { }); describe("which repository a path belongs to", () => { - const row = (dir: string) => ({ dir, name: dir || "ws", branch: "main", detached: false, added: 0, removed: 0, untracked: 0 }); + const row = (dir: string) => ({ dir, name: dir || "ws", branch: "main", detached: false }); it("gives a folder of projects' path to the deepest listed repository", () => { const repos = [row("api"), row("group/tools"), row("group")]; expect(locate(repos, "api/main.go")).toEqual({ repo: "api", rel: "main.go" }); diff --git a/desktop/frontend-next/src/ui/WorkbenchChanges.tsx b/desktop/frontend-next/src/ui/WorkbenchChanges.tsx index a61160d3b5..95a3626294 100644 --- a/desktop/frontend-next/src/ui/WorkbenchChanges.tsx +++ b/desktop/frontend-next/src/ui/WorkbenchChanges.tsx @@ -117,6 +117,7 @@ export function WorkbenchChanges({ port, changes, repo, current, onOpen, onChang {asking === item.path && (

{t("丢弃 {path} 的改动?这个文件会回到上次提交的样子,改动无法恢复。", { path: item.path })}

+ {item.staged && item.unstaged &&

{t("它已暂存的部分和还没暂存的部分都会丢弃。")}

}
diff --git a/desktop/frontend-next/src/ui/WorkbenchGit.tsx b/desktop/frontend-next/src/ui/WorkbenchGit.tsx index 5a63d208ab..085479f792 100644 --- a/desktop/frontend-next/src/ui/WorkbenchGit.tsx +++ b/desktop/frontend-next/src/ui/WorkbenchGit.tsx @@ -1,8 +1,10 @@ -import { useCallback, useEffect, useState } from "react"; +import { useState } from "react"; +import { t } from "../i18n"; import type { AgentPort, WorkspaceChange } from "../port/port"; import type { GitRepoRow } from "../port/workspace"; import { useCommitCard } from "./CommitCard"; import { usePushCard } from "./GitPushCard"; +import { useRepoChanges } from "./useRepoChanges"; import { StudioIcon } from "./StudioIcon"; import { WorkbenchChanges } from "./WorkbenchChanges"; import { WorkbenchHistory } from "./WorkbenchHistory"; @@ -13,6 +15,9 @@ interface Props { // of projects has none of its own: each repository below it reads its own. changes: WorkspaceChange[]; repos: GitRepoRow[]; + // Why the repositories could not be listed, in words, and the way to ask again. + failure?: string; + onRetry?: () => void; // Moves when the agent has written, so a repository the pane does not watch is read again. wrote: number; // The workspace-relative path whose diff is on screen, "" when none is. @@ -28,7 +33,20 @@ const join = (repo: string, path: string) => (repo ? `${repo}/${path}` : path); // stage, discard and commit, the push of the branch, and its history. A workspace // that is one repository shows just that; a folder of projects shows one section per // repository, each acting on its own. -export function WorkbenchGit({ port, changes, repos, wrote, current, onOpen, onChanged, remote }: Props) { +export function WorkbenchGit({ port, changes, repos, failure = "", onRetry, wrote, current, onOpen, onChanged, remote }: Props) { + // A list that could not be read says so, and the workspace is shown as the one + // repository it was before there was a list: the changes the pane holds are real. + if (failure) { + return ( + <> +

+ {failure} + {onRetry && } +

+ + + ); + } if (repos.length === 0) return null; const several = repos.length > 1 || repos[0].dir !== ""; if (!several) { @@ -45,14 +63,7 @@ export function WorkbenchGit({ port, changes, repos, wrote, current, onOpen, onC function Folder({ port, row, wrote, current, onOpen, remote }: { port: AgentPort; row: GitRepoRow; wrote: number; current: string; onOpen: (path: string) => void; remote: boolean }) { const [open, setOpen] = useState(true); - const [changes, setChanges] = useState([]); - const [again, setAgain] = useState(0); - useEffect(() => { - let live = true; - port.changes(row.dir).then((r) => live && setChanges(r.changes), () => live && setChanges([])); - return () => { live = false; }; - }, [port, row.dir, wrote, again]); - const reload = useCallback(() => setAgain((n) => n + 1), []); + const { changes, reload } = useRepoChanges(port, row.dir, open, wrote); return (