feat(FN-803): add ahead/behind commit lists to Git Manager Remotes tab

- Add backend API routes for remote commit lists (local-ahead, remote-ahead, all commits by branch)
- Surface ahead/behind commit counts and expandable commit lists in Remotes tab UI
- Add expand/collapse rows showing commit hash, message, author, and relative date
- Style commit list rows with alternating backgrounds and monospace hashes
- Add comprehensive tests for API routes and GitManagerModal component
- Update README documentation for Git Manager Remotes tab
This commit is contained in:
gsxdsm
2026-04-03 22:02:57 -07:00
parent f1f98ba718
commit acfeea4fd3
9 changed files with 833 additions and 6 deletions

View File

@@ -909,6 +909,8 @@ import {
fetchGitStatus,
fetchGitCommits,
fetchCommitDiff,
fetchAheadCommits,
fetchRemoteCommits,
fetchGitBranches,
fetchGitWorktrees,
createBranch,
@@ -994,6 +996,66 @@ describe("Git Management API", () => {
});
});
describe("fetchAheadCommits", () => {
it("returns commits ahead of upstream", async () => {
const commits = [
{ hash: "abc123", shortHash: "abc", message: "Fix bug", author: "User", date: "2026-01-01", parents: [] },
];
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, commits));
const result = await fetchAheadCommits();
expect(result).toEqual(commits);
expect(globalThis.fetch).toHaveBeenCalledWith("/api/git/commits/ahead", {
headers: { "Content-Type": "application/json" },
});
});
it("returns empty array when no upstream", async () => {
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, []));
const result = await fetchAheadCommits();
expect(result).toEqual([]);
});
});
describe("fetchRemoteCommits", () => {
it("fetches commits for a remote with default params", async () => {
const commits = [
{ hash: "def456", shortHash: "def", message: "Remote commit", author: "User", date: "2026-01-01", parents: [] },
];
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, commits));
const result = await fetchRemoteCommits("origin");
expect(result).toEqual(commits);
expect(globalThis.fetch).toHaveBeenCalledWith("/api/git/remotes/origin/commits", {
headers: { "Content-Type": "application/json" },
});
});
it("includes ref and limit in query", async () => {
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, []));
await fetchRemoteCommits("origin", "main", 5);
expect(globalThis.fetch).toHaveBeenCalledWith("/api/git/remotes/origin/commits?ref=main&limit=5", {
headers: { "Content-Type": "application/json" },
});
});
it("encodes remote name in URL", async () => {
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, []));
await fetchRemoteCommits("my-remote");
expect(globalThis.fetch).toHaveBeenCalledWith("/api/git/remotes/my-remote/commits", {
headers: { "Content-Type": "application/json" },
});
});
});
describe("fetchGitBranches", () => {
it("returns branches array", async () => {
const branches = [{ name: "main", isCurrent: true, remote: "origin/main" }];