mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
feat(vcs): show the selected commit's details in the Push dialog
Selecting one outgoing commit in the Push dialog now shows its full message, revision, author and time above its changed files. A Group by Directory toggle switches the files between a directory tree and a flat list (each file with its directory beside the name), and the choice is remembered. Fixes #1373
This commit is contained in:
@@ -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<LogChange[] | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [current, setCurrent] = useState<LogChange | null>(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({
|
||||
<T>Show All Commits</T>
|
||||
</Button>
|
||||
)}
|
||||
<IconButton
|
||||
icon="folder"
|
||||
size="small"
|
||||
title={tx("Group by Directory")}
|
||||
pressed={byDirectory}
|
||||
onClick={toggleGrouping}
|
||||
className={styles.pushGrouping}
|
||||
/>
|
||||
</div>
|
||||
{count === 1 && <PushCommitDetails commit={commits[0]!} />}
|
||||
{error ? (
|
||||
<div className={styles.error}>{error}</div>
|
||||
) : !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 : <span className={styles.fileDir}>{n.count}</span>)}
|
||||
renderDetails={(n) =>
|
||||
n.file ? (
|
||||
byDirectory ? null : (
|
||||
<span className={styles.fileDir}>{dirName(n.file.path)}</span>
|
||||
)
|
||||
) : (
|
||||
<span className={styles.fileDir}>{n.count}</span>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 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 (
|
||||
<section className={styles.pushCommitDetails} aria-label={tx("Commit details")}>
|
||||
<div className={styles.pushCommitMessage}>{commit.message.trimEnd() || commit.summary}</div>
|
||||
<div className={styles.fileDir}>
|
||||
{commit.shortId} {commit.author} <{commit.email}>
|
||||
{date ? " " + tx`on ${date}` : ""}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(<PushChanges repo="/r" commits={[one]} />);
|
||||
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 <me@example.com> 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(<PushChanges repo="/r" commits={[newer, older]} />);
|
||||
expect(await screen.findByText(/0 files in 2 commits/)).toBeTruthy();
|
||||
expect(screen.queryByRole("region", { name: "Commit details" })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -477,6 +477,7 @@
|
||||
"Comment": "评论",
|
||||
"Comment:": "注释:",
|
||||
"Commit": "提交",
|
||||
"Commit details": "提交详细信息",
|
||||
"Commit Anyway": "仍然提交",
|
||||
"Commit Changelist…": "提交更改列表…",
|
||||
"Commit Checks": "提交检查",
|
||||
|
||||
Reference in New Issue
Block a user