Git commit history (#1534)
* feat(app): add fileView preference for changes panel * feat(app): add buildDiffTree util for changed-files tree * feat(app): add changed-files tree directory row * feat(app): changed-files tree view mode in changes panel (#117) * feat(protocol): add checkout.commits.list RPC + commitsList feature flag * feat(server): list branch commits ahead of base with on-remote flags * feat(server): handle checkout.commits.list RPC and advertise capability * feat(client): checkout.commits.list method + useCommitsQuery hook * feat(app): per-commit inline view with local-vs-remote markers (#117) * feat(protocol,server): per-commit file diff RPC (checkout.commits.file_diff) * feat(app): open per-commit file diff on click (#117) * refactor: surface baseRef + commits loading/error, drop dead depth field * fix(app): flip commit local/remote dot — local hollow, remote filled * feat(app): list/tree view for expanded commit file list (#117) * fix(app): syntax-highlight per-commit file diff to match Changes view * feat(app): draggable resize between commits and diff sections * refactor(app): dedupe wrap-text helpers into diff-highlighted-text * fix(app): clean up commits resize drag on unmount + a11y label * fix(app): collapse commits section by default * fix(app): render per-commit file diff with the shared Changes line renderer * perf(app): memoize shared diff line row; drop redundant DiffLineView wrapper * refactor(app): extract shared DiffFileBody; render commit file diff through it * fix(app): hide inline-comment affordance in per-commit diffs (no reviewActions) * feat(app): move commits to a resizable bottom drawer in the Changes panel * feat(review): commitSha scoping for per-commit review drafts + attachment * feat(app): per-commit inline comments wired through to the composer (#117) * refactor(app): own commit file-diff open state in CommitFileList (drop reset effect) * refactor(app): colocate diff-render cluster under git/diff-file-body/ * feat(app): add diff tab target kind (working/commit diff tabs) * feat(app): useDiffFiles hook unifying working + commit diff targets * feat(app): diff tab panel rendering working/commit diffs * feat(app): open a commit diff tab on commit click; drop per-commit drawer/file list * feat(app): open/scroll working diff tab on changed-file click * fix(app): keep commit diff tabs ephemeral; align working diff whitespace * feat(app): collapsible file sections in the diff tab, collapsed by default * feat(app): make the on-remote commit dot more subtle Dims the filled green remote dot (row + legend) to ~0.55 opacity so the local-only ring stays the state that draws the eye. * Reshape commit diffs around the existing Changes view * Fix diff tab migration complexity after rebase * Address diff tab review findings * Clean up diff tab review interfaces * Collapse commit diffs into the existing view * Load commits when expanded --------- Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
import { execFileSync } from "child_process";
|
||||
import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "fs";
|
||||
import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { listCheckoutCommits } from "./checkout-git.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
for (const dir of tempDirs.splice(0)) {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
function makeTempDir(): string {
|
||||
const dir = realpathSync.native(mkdtempSync(join(tmpdir(), "checkout-commits-test-")));
|
||||
tempDirs.push(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
function git(args: string[], cwd?: string): void {
|
||||
execFileSync("git", args, cwd ? { cwd } : {});
|
||||
}
|
||||
|
||||
function commit(repoDir: string, message: string): void {
|
||||
git(["-c", "commit.gpgsign=false", "commit", "-m", message], repoDir);
|
||||
}
|
||||
|
||||
function commitFile(repoDir: string, name: string, content: string, message: string): void {
|
||||
writeFileSync(join(repoDir, name), content);
|
||||
git(["add", "."], repoDir);
|
||||
commit(repoDir, message);
|
||||
}
|
||||
|
||||
function initRepoOnMain(): { repoDir: string; tempDir: string } {
|
||||
const tempDir = makeTempDir();
|
||||
const repoDir = join(tempDir, "repo");
|
||||
mkdirSync(repoDir, { recursive: true });
|
||||
git(["init", "-b", "main"], repoDir);
|
||||
git(["config", "user.email", "test@test.com"], repoDir);
|
||||
git(["config", "user.name", "Test User"], repoDir);
|
||||
commitFile(repoDir, "README.md", "base\n", "initial");
|
||||
return { repoDir, tempDir };
|
||||
}
|
||||
|
||||
function addBareRemote(repoDir: string, tempDir: string): string {
|
||||
const remoteDir = join(tempDir, "remote.git");
|
||||
git(["init", "--bare", "-b", "main", remoteDir]);
|
||||
git(["remote", "add", "origin", remoteDir], repoDir);
|
||||
return remoteDir;
|
||||
}
|
||||
|
||||
describe("listCheckoutCommits", () => {
|
||||
it("lists commits ahead of base newest-first with on-remote flags and file stats", async () => {
|
||||
const { repoDir, tempDir } = initRepoOnMain();
|
||||
git(["checkout", "-b", "feature"], repoDir);
|
||||
commitFile(repoDir, "foo.txt", "a\nb\nc\n", "Add foo");
|
||||
|
||||
// Push feature (containing only commit A) to the remote, then add B locally.
|
||||
addBareRemote(repoDir, tempDir);
|
||||
git(["push", "-u", "origin", "feature"], repoDir);
|
||||
commitFile(repoDir, "bar.txt", "x\n", "Add bar");
|
||||
|
||||
const { baseRef, commits } = await listCheckoutCommits({ cwd: repoDir });
|
||||
|
||||
expect(baseRef).toBe("main");
|
||||
expect(commits).toHaveLength(2);
|
||||
expect(commits[0]?.subject).toBe("Add bar");
|
||||
expect(commits[1]?.subject).toBe("Add foo");
|
||||
|
||||
expect(commits[0]?.isOnRemote).toBe(false);
|
||||
expect(commits[1]?.isOnRemote).toBe(true);
|
||||
|
||||
expect(commits[0]?.files).toEqual([
|
||||
{ path: "bar.txt", additions: 1, deletions: 0, status: "added" },
|
||||
]);
|
||||
expect(commits[1]?.files).toEqual([
|
||||
{ path: "foo.txt", additions: 3, deletions: 0, status: "added" },
|
||||
]);
|
||||
|
||||
expect(commits[0]?.authorName).toBe("Test User");
|
||||
expect(commits[0]?.sha).toHaveLength(40);
|
||||
expect((commits[0]?.shortSha.length ?? 0) > 0).toBe(true);
|
||||
expect(Number.isNaN(new Date(commits[0]?.authorDate ?? "").getTime())).toBe(false);
|
||||
});
|
||||
|
||||
it("returns [] when there are no commits ahead of base", async () => {
|
||||
const { repoDir } = initRepoOnMain();
|
||||
const { baseRef, commits } = await listCheckoutCommits({ cwd: repoDir });
|
||||
expect(baseRef).toBeNull();
|
||||
expect(commits).toEqual([]);
|
||||
});
|
||||
|
||||
it("marks all commits local-only when there is no remote", async () => {
|
||||
const { repoDir } = initRepoOnMain();
|
||||
git(["checkout", "-b", "feature"], repoDir);
|
||||
commitFile(repoDir, "foo.txt", "a\n", "Add foo");
|
||||
commitFile(repoDir, "bar.txt", "b\n", "Add bar");
|
||||
|
||||
const { baseRef, commits } = await listCheckoutCommits({ cwd: repoDir });
|
||||
|
||||
expect(baseRef).toBe("main");
|
||||
expect(commits).toHaveLength(2);
|
||||
expect(commits.every((c) => c.isOnRemote === false)).toBe(true);
|
||||
});
|
||||
|
||||
it("classifies renamed files with status renamed and correct destination path", async () => {
|
||||
const { repoDir } = initRepoOnMain();
|
||||
git(["checkout", "-b", "feature"], repoDir);
|
||||
commitFile(repoDir, "original.txt", "content\n", "Add original");
|
||||
git(["mv", "original.txt", "renamed.txt"], repoDir);
|
||||
commit(repoDir, "Rename file");
|
||||
|
||||
const { commits } = await listCheckoutCommits({ cwd: repoDir });
|
||||
|
||||
expect(commits[0]?.files).toEqual([
|
||||
{ path: "renamed.txt", additions: 0, deletions: 0, status: "renamed" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("derives status for modified and deleted files", async () => {
|
||||
const { repoDir } = initRepoOnMain();
|
||||
git(["checkout", "-b", "feature"], repoDir);
|
||||
commitFile(repoDir, "README.md", "base\nmore\n", "Edit readme");
|
||||
git(["rm", "README.md"], repoDir);
|
||||
commit(repoDir, "Delete readme");
|
||||
|
||||
const { commits } = await listCheckoutCommits({ cwd: repoDir });
|
||||
|
||||
expect(commits[0]?.files).toEqual([
|
||||
{ path: "README.md", additions: 0, deletions: 2, status: "deleted" },
|
||||
]);
|
||||
expect(commits[1]?.files).toEqual([
|
||||
{ path: "README.md", additions: 1, deletions: 0, status: "modified" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user