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:
@@ -2174,6 +2174,7 @@ export interface GitCommit {
|
||||
hash: string;
|
||||
shortHash: string;
|
||||
message: string;
|
||||
body?: string;
|
||||
author: string;
|
||||
date: string;
|
||||
parents: string[];
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user