From 8d98bab274b98ff06410d19bb57431f57ae30673 Mon Sep 17 00:00:00 2001 From: ybx <3046829330@qq.com> Date: Sun, 16 Aug 2026 09:16:17 +0800 Subject: [PATCH] fix(windows): send diff pathspecs the shared core accepts - translate git_diff_file and git_status_diff_stats filePath into the git.diff pathspecs contract instead of the unrelated paths field, and fall back to ["."] so whole-tree stat requests are not rejected - derive diff stat staged flags from the request instead of hardcoding false, and fetch staged and unstaged stats so both groups show counts --- windows/tauri/src-tauri/src/platform.rs | 45 ++++++++++++++- .../src/features/git/api/git-diff-api.ts | 16 ++++-- .../platform/core-result-adapter.diff.test.ts | 57 +++++++++++++++++++ .../tauri/src/platform/core-result-adapter.ts | 3 +- 4 files changed, 112 insertions(+), 9 deletions(-) create mode 100644 windows/tauri/src/platform/core-result-adapter.diff.test.ts diff --git a/windows/tauri/src-tauri/src/platform.rs b/windows/tauri/src-tauri/src/platform.rs index 5bfccd20..a3cdb70b 100644 --- a/windows/tauri/src-tauri/src/platform.rs +++ b/windows/tauri/src-tauri/src/platform.rs @@ -90,8 +90,13 @@ fn translate(command: &str, args: Value) -> Result<(String, Value), String> { "git.write" } "git_diff_file" | "git_status_diff_stats" => { - paths_from_file(&mut payload); - payload.entry("pathspecs").or_insert_with(|| json!([])); + if let Some(path) = payload.remove("filePath") { + payload.insert("pathspecs".into(), Value::Array(vec![path])); + } else { + // The shared core rejects empty pathspecs; a request without a + // file scope means the whole tree, which git spells as `.`. + payload.insert("pathspecs".into(), json!(["."])); + } "git.diff" } "git_ref_diff" => { @@ -436,7 +441,41 @@ mod tests { translate("git_diff_file", json!({ "repoPath": "C:/work" })).unwrap(); assert_eq!(command, "git.diff"); - assert_eq!(payload, json!({ "root": "C:/work", "pathspecs": [] })); + assert_eq!(payload, json!({ "root": "C:/work", "pathspecs": ["."] })); + } + + #[test] + fn translates_diff_file_pathspec() { + let (command, payload) = translate( + "git_diff_file", + json!({ "repoPath": "C:/work", "filePath": "src/main.rs", "staged": true }), + ) + .unwrap(); + + assert_eq!(command, "git.diff"); + assert_eq!( + payload, + json!({ + "root": "C:/work", + "pathspecs": ["src/main.rs"], + "staged": true + }) + ); + } + + #[test] + fn translates_status_diff_stats_whole_tree() { + let (command, payload) = translate( + "git_status_diff_stats", + json!({ "repoPath": "C:/work", "staged": true }), + ) + .unwrap(); + + assert_eq!(command, "git.diff"); + assert_eq!( + payload, + json!({ "root": "C:/work", "pathspecs": ["."], "staged": true }) + ); } #[test] diff --git a/windows/tauri/src/features/git/api/git-diff-api.ts b/windows/tauri/src/features/git/api/git-diff-api.ts index 389c72f4..5ec665ff 100644 --- a/windows/tauri/src/features/git/api/git-diff-api.ts +++ b/windows/tauri/src/features/git/api/git-diff-api.ts @@ -219,14 +219,20 @@ export const getStatusDiffStats = async (repoPath: string): Promise("git_status_diff_stats", { - repoPath: resolvedRepoPath, - }) - .then((stats) => { + const request = Promise.all([ + tauriInvoke("git_status_diff_stats", { + repoPath: resolvedRepoPath, + }), + tauriInvoke("git_status_diff_stats", { + repoPath: resolvedRepoPath, + staged: true, + }), + ]) + .then(([unstagedStats, stagedStats]) => { if (generation !== getRepositoryCacheGeneration(resolvedRepoPath)) { return getStatusDiffStats(resolvedRepoPath); } - return stats; + return [...unstagedStats, ...stagedStats]; }) .catch((error) => { if (!isNotGitRepositoryError(error)) { diff --git a/windows/tauri/src/platform/core-result-adapter.diff.test.ts b/windows/tauri/src/platform/core-result-adapter.diff.test.ts new file mode 100644 index 00000000..cefe734b --- /dev/null +++ b/windows/tauri/src/platform/core-result-adapter.diff.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, test } from "bun:test"; +import { adaptCoreResult } from "./core-result-adapter"; + +const twoFilePatch = `diff --git a/a.txt b/a.txt +index 111..222 100644 +--- a/a.txt ++++ b/a.txt +@@ -1 +1,2 @@ + hello ++world +diff --git a/b.txt b/b.txt +index 333..444 100644 +--- a/b.txt ++++ b/b.txt +@@ -1 +0,0 @@ +-old line +`; + +describe("git diff stats adaptation", () => { + test("maps per-file additions and deletions from the whole tree patch", () => { + const stats = adaptCoreResult( + "git_status_diff_stats", + { repoPath: "C:/work" }, + { patch: twoFilePatch }, + ); + + expect(stats).toEqual([ + { file_path: "a.txt", staged: false, additions: 1, deletions: 0 }, + { file_path: "b.txt", staged: false, additions: 0, deletions: 1 }, + ]); + }); + + test("carries the staged flag from the request into every stat entry", () => { + const stats = adaptCoreResult( + "git_status_diff_stats", + { repoPath: "C:/work", staged: true }, + { patch: twoFilePatch }, + ); + + expect(Array.isArray(stats)).toBe(true); + for (const stat of stats as Array<{ staged: boolean }>) { + expect(stat.staged).toBe(true); + } + }); +}); + +describe("git single-file diff adaptation", () => { + test("returns the parsed diff for the requested file", () => { + const diff = adaptCoreResult( + "git_diff_file", + { repoPath: "C:/work", filePath: "a.txt" }, + { patch: twoFilePatch }, + ); + + expect((diff as { file_path: string }).file_path).toBe("a.txt"); + }); +}); diff --git a/windows/tauri/src/platform/core-result-adapter.ts b/windows/tauri/src/platform/core-result-adapter.ts index dc94de2c..1f86f93e 100644 --- a/windows/tauri/src/platform/core-result-adapter.ts +++ b/windows/tauri/src/platform/core-result-adapter.ts @@ -29,10 +29,11 @@ function adaptDiff(command: string, args: JsonRecord | undefined, value: unknown return "files" in parsed ? parsed.files[0] ?? null : parsed; } if (command === "git_status_diff_stats") { + const staged = Boolean(argumentsRecord.staged); const diffs = "files" in parsed ? parsed.files : [parsed]; return diffs.map((diff) => ({ file_path: diff.file_path, - staged: false, + staged, additions: diff.additions ?? diff.lines.filter((line) => line.line_type === "added").length, deletions: diff.deletions ?? diff.lines.filter((line) => line.line_type === "removed").length, }));