diff --git a/src/features/vcs/PushChanges.tsx b/src/features/vcs/PushChanges.tsx index ad6753cc..fefa408f 100644 --- a/src/features/vcs/PushChanges.tsx +++ b/src/features/vcs/PushChanges.tsx @@ -1,19 +1,29 @@ /** * Push dialog: the files changed by the outgoing commits (the selected commit's, or all of * them), with the selected file's diff beside the tree. The diff stays inside the dialog, - * which is modal. + * which is modal. A single commit also shows its message, author and time. */ import { useEffect, useMemo, useState } from "react"; import type { ChangedFile, Commit, RevisionChange } from "@/bindings"; import { call, commands } from "@/platform/ipc"; import { tx } from "@/platform/i18n"; -import { Button, Spinner, T } from "@/ui"; +import { Button, IconButton, Spinner, T } from "@/ui"; import { ComparisonTree } from "./ComparisonTree"; import { buildFileTree } from "./fileTree"; -import { toLogChanges, type LogChange } from "./logChanges"; -import { KIND_COLOR, fileCount } from "./model"; +import { flatFileNodes, toLogChanges, type LogChange } from "./logChanges"; +import { KIND_COLOR, dirName, fileCount } from "./model"; import styles from "./common.module.css"; +const GROUP_KEY = "openidea.vcs.push.groupByDirectory"; + +function readGroupByDirectory(): boolean { + try { + return localStorage.getItem(GROUP_KEY) !== "false"; + } catch { + return true; + } +} + /** Identifies what `PushChanges` shows; render it with this `key` so a new selection starts afresh. */ export const pushChangesKey = (repo: string | null, commits: readonly Commit[]) => `${repo ?? ""}:${commits.map((c) => c.id).join(" ")}`; @@ -34,6 +44,7 @@ export function PushChanges({ const [changes, setChanges] = useState(null); const [error, setError] = useState(null); const [current, setCurrent] = useState(null); + const [byDirectory, setByDirectory] = useState(readGroupByDirectory); useEffect(() => { if (!commits.length) return; @@ -50,7 +61,18 @@ export function PushChanges({ }; }, [repo, commits]); - const tree = useMemo(() => (changes ? buildFileTree(changes, (f) => f.path) : []), [changes]); + const tree = useMemo( + () => (!changes ? [] : byDirectory ? buildFileTree(changes, (f) => f.path) : flatFileNodes(changes, (f) => f.path)), + [changes, byDirectory], + ); + const toggleGrouping = () => { + setByDirectory(!byDirectory); + try { + localStorage.setItem(GROUP_KEY, String(!byDirectory)); + } catch { + /* kept for this dialog only */ + } + }; const count = commits.length; if (!count) return null; return ( @@ -65,7 +87,16 @@ export function PushChanges({ Show All Commits )} + + {count === 1 && } {error ? (
{error}
) : !changes ? ( @@ -80,9 +111,31 @@ export function PushChanges({ diffFor={(c) => c.diff} getColor={(c) => KIND_COLOR[c.kind === "untracked" ? "added" : c.kind]} ariaLabel="Changed files" - renderDetails={(n) => (n.file ? null : {n.count})} + renderDetails={(n) => + n.file ? ( + byDirectory ? null : ( + {dirName(n.file.path)} + ) + ) : ( + {n.count} + ) + } /> )} ); } + +/** The selected commit's full message, revision, author and time. */ +function PushCommitDetails({ commit }: { commit: Commit }) { + const date = commit.timeMs ? new Date(commit.timeMs).toLocaleString() : null; + return ( +
+
{commit.message.trimEnd() || commit.summary}
+
+ {commit.shortId} {commit.author} <{commit.email}> + {date ? " " + tx`on ${date}` : ""} +
+
+ ); +} diff --git a/src/features/vcs/__tests__/pushChanges.test.tsx b/src/features/vcs/__tests__/pushChanges.test.tsx index 1cdbf94b..b400a332 100644 --- a/src/features/vcs/__tests__/pushChanges.test.tsx +++ b/src/features/vcs/__tests__/pushChanges.test.tsx @@ -12,7 +12,16 @@ vi.mock("@/platform/ipc", async (importOriginal) => { import { PushChanges } from "../PushChanges"; const commit = (id: string, parent: string): Commit => - ({ id, shortId: id.slice(0, 7), parents: [parent], summary: id, author: "me", timeMs: 0 }) as unknown as Commit; + ({ + id, + shortId: id.slice(0, 7), + parents: [parent], + summary: id, + message: id, + author: "me", + email: "me@example.com", + timeMs: 0, + }) as unknown as Commit; const newer = commit("bbbbbbbbbb", "aaaaaaaaaa"); const older = commit("aaaaaaaaaa", "9999999999"); @@ -41,4 +50,36 @@ describe("Push dialog changes", () => { fireEvent.click(screen.getByRole("button", { name: "Show All Commits" })); expect(showAll).toHaveBeenCalled(); }); + + it("shows the selected commit's message and author, and lists files flat on request", async () => { + localStorage.clear(); + files.mockResolvedValue({ + status: "ok", + data: [{ path: "src/orders/service.py", oldPath: null, kind: "added" }], + }); + const one = { ...older, message: "Add order service\n\nIntroduces the service.\n", timeMs: 1_700_000_000_000 }; + render(); + const details = screen.getByRole("region", { name: "Commit details" }); + expect(details.textContent).toContain("Add order service\n\nIntroduces the service."); + expect(details.textContent).toContain("aaaaaaa me on "); + expect(await screen.findByText("src/orders")).toBeTruthy(); + expect(screen.getAllByRole("treeitem")).toHaveLength(2); + const grouping = screen.getByRole("button", { name: "Group by Directory" }); + expect(grouping.getAttribute("aria-pressed")).toBe("true"); + fireEvent.click(grouping); + expect(grouping.getAttribute("aria-pressed")).toBe("false"); + // Flat: one row, the file, with its directory beside the name. + const rows = screen.getAllByRole("treeitem"); + expect(rows).toHaveLength(1); + expect(rows[0]!.textContent).toContain("service.py"); + expect(rows[0]!.textContent).toContain("src/orders"); + expect(localStorage.getItem("openidea.vcs.push.groupByDirectory")).toBe("false"); + }); + + it("shows no commit details for several commits", async () => { + combined.mockResolvedValue({ status: "ok", data: [] }); + render(); + expect(await screen.findByText(/0 files in 2 commits/)).toBeTruthy(); + expect(screen.queryByRole("region", { name: "Commit details" })).toBeNull(); + }); }); diff --git a/src/features/vcs/common.module.css b/src/features/vcs/common.module.css index d8464a18..cadf6f9d 100644 --- a/src/features/vcs/common.module.css +++ b/src/features/vcs/common.module.css @@ -143,6 +143,27 @@ padding: 0 14px 6px; color: var(--fg-subtle); } +.pushGrouping { + margin-left: auto; +} +.pushCommitDetails { + display: flex; + flex-direction: column; + gap: 4px; + max-height: 96px; + margin: 0 14px 8px; + padding: 6px 8px; + overflow: auto; + border: 1px solid var(--border); + border-radius: var(--radius-small); + -webkit-user-select: text; + user-select: text; +} +.pushCommitMessage { + color: var(--fg-strong); + white-space: pre-wrap; + overflow-wrap: anywhere; +} .todoList { margin: 8px 0 0; padding-left: 18px; diff --git a/src/platform/locales/text/zh.json b/src/platform/locales/text/zh.json index 076cec07..2ce88371 100644 --- a/src/platform/locales/text/zh.json +++ b/src/platform/locales/text/zh.json @@ -477,6 +477,7 @@ "Comment": "评论", "Comment:": "注释:", "Commit": "提交", + "Commit details": "提交详细信息", "Commit Anyway": "仍然提交", "Commit Changelist…": "提交更改列表…", "Commit Checks": "提交检查",