feat(FN-3301): render commit body in expanded git views

Completes the commit body rendering in expanded git views within `GitManagerModal`, including updated component implementation and tests. The dashboard git routes and modal component now properly display full commit body content.

Fusion-Task-Id: FN-3301
This commit is contained in:
Fusion
2026-05-04 22:04:35 -07:00
committed by gsxdsm
parent 7e8ad8b2c3
commit 1655ef578f
5 changed files with 116 additions and 61 deletions

View File

@@ -2174,6 +2174,7 @@ export interface GitCommit {
hash: string;
shortHash: string;
message: string;
body?: string;
author: string;
date: string;
parents: string[];

View File

@@ -1372,6 +1372,7 @@ function CommitsPanel({
</div>
) : commitDiff ? (
<>
{commit.body && <div className="gm-commit-message-full">{commit.body}</div>}
{commitDiff.stat && <pre className="gm-diff-stat">{commitDiff.stat}</pre>}
<pre className="gm-diff-patch">{commitDiff.patch}</pre>
</>
@@ -1591,6 +1592,9 @@ function BranchesPanel({
</div>
) : branchCommitDiff ? (
<>
{(commit.body || commit.message) && (
<div className="gm-commit-message-full">{commit.body || commit.message}</div>
)}
{branchCommitDiff.stat && <pre className="gm-diff-stat">{branchCommitDiff.stat}</pre>}
<pre className="gm-diff-patch">{branchCommitDiff.patch}</pre>
</>
@@ -2381,8 +2385,8 @@ function RemotesPanel({
</div>
) : aheadCommitDiff ? (
<>
{commit.message && (
<div className="gm-commit-message-full">{commit.message}</div>
{(commit.body || commit.message) && (
<div className="gm-commit-message-full">{commit.body || commit.message}</div>
)}
{aheadCommitDiff.stat && <pre className="gm-diff-stat">{aheadCommitDiff.stat}</pre>}
<pre className="gm-diff-patch">{aheadCommitDiff.patch}</pre>
@@ -2468,8 +2472,8 @@ function RemotesPanel({
</div>
) : remoteCommitDiff ? (
<>
{commit.message && (
<div className="gm-commit-message-full">{commit.message}</div>
{(commit.body || commit.message) && (
<div className="gm-commit-message-full">{commit.body || commit.message}</div>
)}
{remoteCommitDiff.stat && <pre className="gm-diff-stat">{remoteCommitDiff.stat}</pre>}
<pre className="gm-diff-patch">{remoteCommitDiff.patch}</pre>

View File

@@ -130,6 +130,7 @@ describe("GitManagerModal", () => {
hash: "abc1234def5678",
shortHash: "abc1234",
message: "Test commit",
body: "Detailed description\n\nMultiple paragraphs.",
author: "User",
date: "2026-01-01T00:00:00Z",
parents: [],
@@ -594,6 +595,57 @@ describe("GitManagerModal", () => {
});
});
it("shows commit body when expanding a commit in commits panel", async () => {
render(
<GitManagerModal isOpen={true} onClose={vi.fn()} tasks={mockTasks} addToast={mockAddToast} />
);
fireEvent.click(screen.getByRole("tab", { name: /commits/i }));
await waitFor(() => {
expect(screen.getByText("Test commit")).toBeInTheDocument();
});
fireEvent.click(screen.getByText("Test commit"));
await waitFor(() => {
const fullMessage = document.querySelector(".gm-commit-message-full");
expect(fullMessage?.textContent).toContain("Detailed description");
expect(fullMessage?.textContent).toContain("Multiple paragraphs.");
});
});
it("does not render full message block for commits without body", async () => {
(fetchGitCommits as any).mockResolvedValue([
{
hash: "abc1234def5678",
shortHash: "abc1234",
message: "Subject only commit",
body: "",
author: "User",
date: "2026-01-01T00:00:00Z",
parents: [],
},
]);
render(
<GitManagerModal isOpen={true} onClose={vi.fn()} tasks={mockTasks} addToast={mockAddToast} />
);
fireEvent.click(screen.getByRole("tab", { name: /commits/i }));
await waitFor(() => {
expect(screen.getByText("Subject only commit")).toBeInTheDocument();
});
fireEvent.click(screen.getByText("Subject only commit"));
await waitFor(() => {
expectLatestCallStartsWith(fetchCommitDiff as any, "abc1234def5678");
});
const expandedDiff = document.querySelector(".gm-commit-diff");
expect(expandedDiff?.querySelector(".gm-commit-message-full")).toBeNull();
});
// ── Branches Panel ─────────────────────────────────────────
it("loads branches and shows current branch", async () => {

View File

@@ -387,6 +387,9 @@ describe("Git Management endpoints", () => {
expect(res.body[0]).toHaveProperty("message");
expect(res.body[0]).toHaveProperty("author");
expect(res.body[0]).toHaveProperty("date");
if ("body" in res.body[0] && res.body[0].body !== undefined) {
expect(typeof res.body[0].body).toBe("string");
}
}
});
@@ -550,6 +553,9 @@ describe("Git Management endpoints", () => {
expect(commit).toHaveProperty("author");
expect(commit).toHaveProperty("date");
expect(commit).toHaveProperty("parents");
if ("body" in commit && commit.body !== undefined) {
expect(typeof commit.body).toBe("string");
}
}
});
@@ -593,6 +599,9 @@ describe("Git Management endpoints", () => {
expect(commit).toHaveProperty("author");
expect(commit).toHaveProperty("date");
expect(commit).toHaveProperty("parents");
if ("body" in commit && commit.body !== undefined) {
expect(typeof commit.body).toBe("string");
}
}
}
});

View File

@@ -156,28 +156,53 @@ export interface GitCommit {
hash: string;
shortHash: string;
message: string;
body?: string;
author: string;
date: string;
parents: string[];
}
export async function getGitCommits(limit = 20, cwd?: string): Promise<GitCommit[]> {
try {
const format = "%H|%h|%s|%an|%aI|%P";
const output = await runGitCommand(["log", `--max-count=${limit}`, `--pretty=format:${format}`], cwd, 10000);
function parseGitCommitsFromLogOutput(output: string): GitCommit[] {
const commits: GitCommit[] = [];
const commits: GitCommit[] = [];
for (const line of output.split("\n")) {
const parts = line.split("|");
if (parts.length < 5) continue;
for (const record of output.split("\0")) {
if (!record) continue;
const [hash, shortHash, message, author, date, parentsStr] = parts;
const parents = parentsStr ? parentsStr.split(" ").filter(Boolean) : [];
const parts = record.split("\x1f");
if (parts.length < 7) continue;
commits.push({ hash, shortHash, message: message || "", author: author || "", date: date || "", parents });
const [hash, shortHash, message, fullMessage, author, date, parentsStr] = parts;
const trimmedFullMessage = fullMessage.trimEnd();
const subjectLine = message || "";
let body = trimmedFullMessage;
if (subjectLine && body.startsWith(subjectLine)) {
body = body.slice(subjectLine.length);
body = body.replace(/^\n+/, "");
}
return commits;
body = body.trim();
const parents = parentsStr ? parentsStr.split(" ").filter(Boolean) : [];
commits.push({
hash,
shortHash,
message: subjectLine,
body: body || undefined,
author: author || "",
date: date || "",
parents,
});
}
return commits;
}
export async function getGitCommits(limit = 20, cwd?: string): Promise<GitCommit[]> {
try {
const format = "%H%x1f%h%x1f%s%x1f%B%x1f%an%x1f%aI%x1f%P";
const output = await runGitCommand(["log", "-z", `--max-count=${limit}`, `--pretty=format:${format}`], cwd, 10000);
return parseGitCommitsFromLogOutput(output);
} catch {
return [];
}
@@ -199,21 +224,9 @@ export function isValidGitRef(ref: string): boolean {
export async function getGitCommitsForBranch(branch: string, limit = 10, cwd?: string): Promise<GitCommit[]> {
try {
const format = "%H|%h|%s|%an|%aI|%P";
const output = await runGitCommand(["log", `--max-count=${limit}`, `--pretty=format:${format}`, branch], cwd, 10000);
const commits: GitCommit[] = [];
for (const line of output.split("\n")) {
const parts = line.split("|");
if (parts.length < 5) continue;
const [hash, shortHash, message, author, date, parentsStr] = parts;
const parents = parentsStr ? parentsStr.split(" ").filter(Boolean) : [];
commits.push({ hash, shortHash, message: message || "", author: author || "", date: date || "", parents });
}
return commits;
const format = "%H%x1f%h%x1f%s%x1f%B%x1f%an%x1f%aI%x1f%P";
const output = await runGitCommand(["log", "-z", `--max-count=${limit}`, `--pretty=format:${format}`, branch], cwd, 10000);
return parseGitCommitsFromLogOutput(output);
} catch {
return [];
}
@@ -227,21 +240,9 @@ export async function getAheadCommits(cwd?: string): Promise<GitCommit[]> {
return [];
}
const format = "%H|%h|%s|%an|%aI|%P";
const output = await runGitCommand(["log", "@{u}..HEAD", `--pretty=format:${format}`], cwd, 10000);
const commits: GitCommit[] = [];
for (const line of output.split("\n")) {
const parts = line.split("|");
if (parts.length < 5) continue;
const [hash, shortHash, message, author, date, parentsStr] = parts;
const parents = parentsStr ? parentsStr.split(" ").filter(Boolean) : [];
commits.push({ hash, shortHash, message: message || "", author: author || "", date: date || "", parents });
}
return commits;
const format = "%H%x1f%h%x1f%s%x1f%B%x1f%an%x1f%aI%x1f%P";
const output = await runGitCommand(["log", "-z", "@{u}..HEAD", `--pretty=format:${format}`], cwd, 10000);
return parseGitCommitsFromLogOutput(output);
} catch {
return [];
}
@@ -259,22 +260,10 @@ export async function getRemoteCommits(remoteRef: string, limit = 10, cwd?: stri
return [];
}
const format = "%H|%h|%s|%an|%aI|%P";
const format = "%H%x1f%h%x1f%s%x1f%B%x1f%an%x1f%aI%x1f%P";
const safeLimit = Math.min(Math.max(1, limit), 50);
const output = await runGitCommand(["log", `--max-count=${safeLimit}`, `--pretty=format:${format}`, remoteRef], cwd, 10000);
const commits: GitCommit[] = [];
for (const line of output.split("\n")) {
const parts = line.split("|");
if (parts.length < 5) continue;
const [hash, shortHash, message, author, date, parentsStr] = parts;
const parents = parentsStr ? parentsStr.split(" ").filter(Boolean) : [];
commits.push({ hash, shortHash, message: message || "", author: author || "", date: date || "", parents });
}
return commits;
const output = await runGitCommand(["log", "-z", `--max-count=${safeLimit}`, `--pretty=format:${format}`, remoteRef], cwd, 10000);
return parseGitCommitsFromLogOutput(output);
} catch {
return [];
}