mirror of
https://github.com/aljazceru/claude-code-viewer.git
synced 2026-01-23 07:24:21 +01:00
refactor: move directories
This commit is contained in:
521
src/server/core/git/functions/getDiff.test.ts
Normal file
521
src/server/core/git/functions/getDiff.test.ts
Normal file
@@ -0,0 +1,521 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { compareBranches, getDiff } from "./getDiff";
|
||||
import * as utils from "./utils";
|
||||
|
||||
vi.mock("./utils", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof utils>();
|
||||
return {
|
||||
...actual,
|
||||
executeGitCommand: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("node:fs/promises");
|
||||
|
||||
describe("getDiff", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("正常系", () => {
|
||||
it("2つのブランチ間のdiffを取得できる", async () => {
|
||||
const mockCwd = "/test/repo";
|
||||
const fromRef = "base:main";
|
||||
const toRef = "compare:feature";
|
||||
|
||||
const mockNumstatOutput = `5\t2\tsrc/file1.ts
|
||||
10\t0\tsrc/file2.ts`;
|
||||
|
||||
const mockDiffOutput = `diff --git a/src/file1.ts b/src/file1.ts
|
||||
index abc123..def456 100644
|
||||
--- a/src/file1.ts
|
||||
+++ b/src/file1.ts
|
||||
@@ -1,5 +1,8 @@
|
||||
function hello() {
|
||||
- console.log("old");
|
||||
+ console.log("new");
|
||||
+ console.log("added line 1");
|
||||
+ console.log("added line 2");
|
||||
}
|
||||
diff --git a/src/file2.ts b/src/file2.ts
|
||||
new file mode 100644
|
||||
index 0000000..ghi789
|
||||
--- /dev/null
|
||||
+++ b/src/file2.ts
|
||||
@@ -0,0 +1,10 @@
|
||||
+export const newFile = true;`;
|
||||
|
||||
vi.mocked(utils.executeGitCommand)
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: mockNumstatOutput,
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: mockDiffOutput,
|
||||
});
|
||||
|
||||
const result = await getDiff(mockCwd, fromRef, toRef);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.files).toHaveLength(2);
|
||||
expect(result.data.files[0]?.filePath).toBe("src/file1.ts");
|
||||
expect(result.data.files[0]?.status).toBe("modified");
|
||||
expect(result.data.files[0]?.additions).toBe(5);
|
||||
expect(result.data.files[0]?.deletions).toBe(2);
|
||||
|
||||
expect(result.data.files[1]?.filePath).toBe("src/file2.ts");
|
||||
expect(result.data.files[1]?.status).toBe("added");
|
||||
expect(result.data.files[1]?.additions).toBe(10);
|
||||
expect(result.data.files[1]?.deletions).toBe(0);
|
||||
|
||||
expect(result.data.summary.totalFiles).toBe(2);
|
||||
expect(result.data.summary.totalAdditions).toBe(15);
|
||||
expect(result.data.summary.totalDeletions).toBe(2);
|
||||
}
|
||||
|
||||
expect(utils.executeGitCommand).toHaveBeenCalledWith(
|
||||
["diff", "--numstat", "main", "feature"],
|
||||
mockCwd,
|
||||
);
|
||||
|
||||
expect(utils.executeGitCommand).toHaveBeenCalledWith(
|
||||
["diff", "--unified=5", "main", "feature"],
|
||||
mockCwd,
|
||||
);
|
||||
});
|
||||
|
||||
it("HEADとworking directoryの比較ができる", async () => {
|
||||
const mockCwd = "/test/repo";
|
||||
const fromRef = "base:HEAD";
|
||||
const toRef = "compare:working";
|
||||
|
||||
const mockNumstatOutput = `3\t1\tsrc/modified.ts`;
|
||||
const mockDiffOutput = `diff --git a/src/modified.ts b/src/modified.ts
|
||||
index abc123..def456 100644
|
||||
--- a/src/modified.ts
|
||||
+++ b/src/modified.ts
|
||||
@@ -1,3 +1,5 @@
|
||||
const value = 1;`;
|
||||
|
||||
const mockStatusOutput = `## main
|
||||
M src/modified.ts
|
||||
?? src/untracked.ts`;
|
||||
|
||||
vi.mocked(readFile).mockResolvedValue("line1\nline2\nline3");
|
||||
|
||||
vi.mocked(utils.executeGitCommand)
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: mockNumstatOutput,
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: mockDiffOutput,
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: mockStatusOutput,
|
||||
});
|
||||
|
||||
const result = await getDiff(mockCwd, fromRef, toRef);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
// modified file + untracked file
|
||||
expect(result.data.files.length).toBeGreaterThanOrEqual(1);
|
||||
const modifiedFile = result.data.files.find(
|
||||
(f) => f.filePath === "src/modified.ts",
|
||||
);
|
||||
expect(modifiedFile).toBeDefined();
|
||||
expect(modifiedFile?.status).toBe("modified");
|
||||
}
|
||||
});
|
||||
|
||||
it("同一refの場合は空の結果を返す", async () => {
|
||||
const mockCwd = "/test/repo";
|
||||
const fromRef = "base:main";
|
||||
const toRef = "compare:main";
|
||||
|
||||
const result = await getDiff(mockCwd, fromRef, toRef);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.files).toHaveLength(0);
|
||||
expect(result.data.diffs).toHaveLength(0);
|
||||
expect(result.data.summary.totalFiles).toBe(0);
|
||||
expect(result.data.summary.totalAdditions).toBe(0);
|
||||
expect(result.data.summary.totalDeletions).toBe(0);
|
||||
}
|
||||
|
||||
expect(utils.executeGitCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.skip("削除されたファイルを処理できる", async () => {
|
||||
const mockCwd = "/test/repo";
|
||||
const fromRef = "base:main";
|
||||
const toRef = "compare:feature";
|
||||
|
||||
const mockNumstatOutput = `0\t10\tsrc/deleted.ts`;
|
||||
const mockDiffOutput = `diff --git a/src/deleted.ts b/src/deleted.ts
|
||||
deleted file mode 100644
|
||||
index abc123..0000000 100644
|
||||
--- a/src/deleted.ts
|
||||
+++ /dev/null
|
||||
@@ -1,10 +0,0 @@
|
||||
-deleted line 1
|
||||
-deleted line 2
|
||||
-deleted line 3
|
||||
-deleted line 4
|
||||
-deleted line 5
|
||||
-deleted line 6
|
||||
-deleted line 7
|
||||
-deleted line 8
|
||||
-deleted line 9
|
||||
-deleted line 10`;
|
||||
|
||||
vi.mocked(utils.executeGitCommand)
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: mockNumstatOutput,
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: mockDiffOutput,
|
||||
});
|
||||
|
||||
const result = await getDiff(mockCwd, fromRef, toRef);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.files).toHaveLength(1);
|
||||
expect(result.data.files[0]?.filePath).toBe("src/deleted.ts");
|
||||
expect(result.data.files[0]?.status).toBe("deleted");
|
||||
expect(result.data.files[0]?.additions).toBe(0);
|
||||
expect(result.data.files[0]?.deletions).toBe(10);
|
||||
}
|
||||
});
|
||||
|
||||
it.skip("名前変更されたファイルを処理できる", async () => {
|
||||
const mockCwd = "/test/repo";
|
||||
const fromRef = "base:main";
|
||||
const toRef = "compare:feature";
|
||||
|
||||
const mockNumstatOutput = `0\t0\tnew-name.ts`;
|
||||
const mockDiffOutput = `diff --git a/old-name.ts b/new-name.ts
|
||||
similarity index 100%
|
||||
rename from old-name.ts
|
||||
rename to new-name.ts
|
||||
index abc123..abc123 100644`;
|
||||
|
||||
vi.mocked(utils.executeGitCommand)
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: mockNumstatOutput,
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: mockDiffOutput,
|
||||
});
|
||||
|
||||
const result = await getDiff(mockCwd, fromRef, toRef);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.files).toHaveLength(1);
|
||||
expect(result.data.files[0]?.status).toBe("renamed");
|
||||
expect(result.data.files[0]?.filePath).toBe("new-name.ts");
|
||||
expect(result.data.files[0]?.oldPath).toBe("old-name.ts");
|
||||
}
|
||||
});
|
||||
|
||||
it("空のdiffを処理できる", async () => {
|
||||
const mockCwd = "/test/repo";
|
||||
const fromRef = "base:main";
|
||||
const toRef = "compare:feature";
|
||||
|
||||
const mockNumstatOutput = "";
|
||||
const mockDiffOutput = "";
|
||||
|
||||
vi.mocked(utils.executeGitCommand)
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: mockNumstatOutput,
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: mockDiffOutput,
|
||||
});
|
||||
|
||||
const result = await getDiff(mockCwd, fromRef, toRef);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.files).toHaveLength(0);
|
||||
expect(result.data.diffs).toHaveLength(0);
|
||||
expect(result.data.summary.totalFiles).toBe(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("エラー系", () => {
|
||||
it("ディレクトリが存在しない場合", async () => {
|
||||
const mockCwd = "/nonexistent/repo";
|
||||
const fromRef = "base:main";
|
||||
const toRef = "compare:feature";
|
||||
|
||||
vi.mocked(utils.executeGitCommand).mockResolvedValue({
|
||||
success: false,
|
||||
error: {
|
||||
code: "NOT_A_REPOSITORY",
|
||||
message: `Directory does not exist: ${mockCwd}`,
|
||||
command: "git diff --numstat main feature",
|
||||
},
|
||||
});
|
||||
|
||||
const result = await getDiff(mockCwd, fromRef, toRef);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.code).toBe("NOT_A_REPOSITORY");
|
||||
expect(result.error.message).toContain("Directory does not exist");
|
||||
}
|
||||
});
|
||||
|
||||
it("Gitリポジトリでない場合", async () => {
|
||||
const mockCwd = "/test/not-a-repo";
|
||||
const fromRef = "base:main";
|
||||
const toRef = "compare:feature";
|
||||
|
||||
vi.mocked(utils.executeGitCommand).mockResolvedValue({
|
||||
success: false,
|
||||
error: {
|
||||
code: "NOT_A_REPOSITORY",
|
||||
message: `Not a git repository: ${mockCwd}`,
|
||||
command: "git diff --numstat main feature",
|
||||
},
|
||||
});
|
||||
|
||||
const result = await getDiff(mockCwd, fromRef, toRef);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.code).toBe("NOT_A_REPOSITORY");
|
||||
expect(result.error.message).toContain("Not a git repository");
|
||||
}
|
||||
});
|
||||
|
||||
it("ブランチが見つからない場合", async () => {
|
||||
const mockCwd = "/test/repo";
|
||||
const fromRef = "base:nonexistent";
|
||||
const toRef = "compare:feature";
|
||||
|
||||
vi.mocked(utils.executeGitCommand).mockResolvedValue({
|
||||
success: false,
|
||||
error: {
|
||||
code: "BRANCH_NOT_FOUND",
|
||||
message: "Branch or commit not found",
|
||||
command: "git diff --numstat nonexistent feature",
|
||||
stderr:
|
||||
"fatal: ambiguous argument 'nonexistent': unknown revision or path not in the working tree.",
|
||||
},
|
||||
});
|
||||
|
||||
const result = await getDiff(mockCwd, fromRef, toRef);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.code).toBe("BRANCH_NOT_FOUND");
|
||||
expect(result.error.message).toBe("Branch or commit not found");
|
||||
}
|
||||
});
|
||||
|
||||
it("numstatコマンドが失敗した場合", async () => {
|
||||
const mockCwd = "/test/repo";
|
||||
const fromRef = "base:main";
|
||||
const toRef = "compare:feature";
|
||||
|
||||
vi.mocked(utils.executeGitCommand).mockResolvedValue({
|
||||
success: false,
|
||||
error: {
|
||||
code: "COMMAND_FAILED",
|
||||
message: "Command failed",
|
||||
command: "git diff --numstat main feature",
|
||||
stderr: "fatal: bad revision",
|
||||
},
|
||||
});
|
||||
|
||||
const result = await getDiff(mockCwd, fromRef, toRef);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.code).toBe("COMMAND_FAILED");
|
||||
}
|
||||
});
|
||||
|
||||
it("無効なfromRefの場合", async () => {
|
||||
const mockCwd = "/test/repo";
|
||||
const fromRef = "invalidref";
|
||||
const toRef = "compare:feature";
|
||||
|
||||
await expect(getDiff(mockCwd, fromRef, toRef)).rejects.toThrow(
|
||||
"Invalid ref text",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("エッジケース", () => {
|
||||
it("特殊文字を含むファイル名を処理できる", async () => {
|
||||
const mockCwd = "/test/repo";
|
||||
const fromRef = "base:main";
|
||||
const toRef = "compare:feature";
|
||||
|
||||
const mockNumstatOutput = `5\t2\tsrc/file with spaces.ts
|
||||
3\t1\tsrc/日本語ファイル.ts`;
|
||||
|
||||
const mockDiffOutput = `diff --git a/src/file with spaces.ts b/src/file with spaces.ts
|
||||
index abc123..def456 100644
|
||||
--- a/src/file with spaces.ts
|
||||
+++ b/src/file with spaces.ts
|
||||
@@ -1,3 +1,5 @@
|
||||
content
|
||||
diff --git a/src/日本語ファイル.ts b/src/日本語ファイル.ts
|
||||
index abc123..def456 100644
|
||||
--- a/src/日本語ファイル.ts
|
||||
+++ b/src/日本語ファイル.ts
|
||||
@@ -1,2 +1,3 @@
|
||||
content`;
|
||||
|
||||
vi.mocked(utils.executeGitCommand)
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: mockNumstatOutput,
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: mockDiffOutput,
|
||||
});
|
||||
|
||||
const result = await getDiff(mockCwd, fromRef, toRef);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.files).toHaveLength(2);
|
||||
expect(result.data.files[0]?.filePath).toBe("src/file with spaces.ts");
|
||||
expect(result.data.files[1]?.filePath).toBe("src/日本語ファイル.ts");
|
||||
}
|
||||
});
|
||||
|
||||
it("バイナリファイルの変更を処理できる", async () => {
|
||||
const mockCwd = "/test/repo";
|
||||
const fromRef = "base:main";
|
||||
const toRef = "compare:feature";
|
||||
|
||||
const mockNumstatOutput = `-\t-\timage.png`;
|
||||
const mockDiffOutput = `diff --git a/image.png b/image.png
|
||||
index abc123..def456 100644
|
||||
Binary files a/image.png and b/image.png differ`;
|
||||
|
||||
vi.mocked(utils.executeGitCommand)
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: mockNumstatOutput,
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: mockDiffOutput,
|
||||
});
|
||||
|
||||
const result = await getDiff(mockCwd, fromRef, toRef);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.files).toHaveLength(1);
|
||||
expect(result.data.files[0]?.filePath).toBe("image.png");
|
||||
expect(result.data.files[0]?.additions).toBe(0);
|
||||
expect(result.data.files[0]?.deletions).toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("大量のファイル変更を処理できる", async () => {
|
||||
const mockCwd = "/test/repo";
|
||||
const fromRef = "base:main";
|
||||
const toRef = "compare:feature";
|
||||
|
||||
const mockNumstatOutput = Array.from(
|
||||
{ length: 100 },
|
||||
(_, i) => `1\t1\tfile${i}.ts`,
|
||||
).join("\n");
|
||||
const mockDiffOutput = Array.from(
|
||||
{ length: 100 },
|
||||
(_, i) => `diff --git a/file${i}.ts b/file${i}.ts
|
||||
index abc123..def456 100644
|
||||
--- a/file${i}.ts
|
||||
+++ b/file${i}.ts
|
||||
@@ -1 +1 @@
|
||||
-old
|
||||
+new`,
|
||||
).join("\n");
|
||||
|
||||
vi.mocked(utils.executeGitCommand)
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: mockNumstatOutput,
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: mockDiffOutput,
|
||||
});
|
||||
|
||||
const result = await getDiff(mockCwd, fromRef, toRef);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.files).toHaveLength(100);
|
||||
expect(result.data.summary.totalFiles).toBe(100);
|
||||
expect(result.data.summary.totalAdditions).toBe(100);
|
||||
expect(result.data.summary.totalDeletions).toBe(100);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("compareBranches", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("getDiffのショートハンドとして機能する", async () => {
|
||||
const mockCwd = "/test/repo";
|
||||
const baseBranch = "base:main";
|
||||
const targetBranch = "compare:feature";
|
||||
|
||||
const mockNumstatOutput = `5\t2\tfile.ts`;
|
||||
const mockDiffOutput = `diff --git a/file.ts b/file.ts
|
||||
index abc123..def456 100644
|
||||
--- a/file.ts
|
||||
+++ b/file.ts
|
||||
@@ -1,2 +1,5 @@
|
||||
content`;
|
||||
|
||||
vi.mocked(utils.executeGitCommand)
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: mockNumstatOutput,
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: mockDiffOutput,
|
||||
});
|
||||
|
||||
const result = await compareBranches(mockCwd, baseBranch, targetBranch);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.files).toHaveLength(1);
|
||||
expect(result.data.files[0]?.filePath).toBe("file.ts");
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user