Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
45 changes: 42 additions & 3 deletions windows/tauri/src-tauri/src/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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" => {
Expand Down Expand Up @@ -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]
Expand Down
18 changes: 17 additions & 1 deletion windows/tauri/src/features/editor/stores/buffer-pane-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { usePaneStore } from "@/features/panes/stores/pane.store";
import type { PaneGroup } from "@/features/panes/types/pane.types";
import type { PaneContent } from "@/features/panes/types/pane-content.types";
import { ensureBufferInPane } from "@/features/panes/utils/pane-buffer-actions";
import { resolveWritablePaneForBuffer } from "@/features/panes/utils/pane-routing";
import {
resolveMainPaneForExternalOpen,
resolveWritablePaneForBuffer,
} from "@/features/panes/utils/pane-routing";
import { createPaneBeside } from "@/features/panes/utils/pane-split-actions";

const getPaneState = (workspaceId?: string) =>
Expand All @@ -29,6 +32,19 @@ export const getWritablePaneForBuffer = (
return newPaneId ? paneStore.actions.getPaneById(newPaneId) : activePane;
};

export const activateMainEditorPane = (workspaceId?: string): PaneGroup | null => {
const paneStore = getPaneState(workspaceId);
const targetPane = resolveMainPaneForExternalOpen({
activePaneId: paneStore.activePaneId,
mostRecentActivePaneIds: paneStore.mostRecentActivePaneIds,
root: paneStore.root,
});
if (targetPane && targetPane.id !== paneStore.activePaneId) {
paneStore.actions.setActivePane(targetPane.id);
}
return targetPane;
};

export const syncBufferToPane = (bufferId: string, workspaceId?: string) => {
const targetPane = getWritablePaneForBuffer(bufferId, workspaceId);
if (!targetPane) return;
Expand Down
19 changes: 12 additions & 7 deletions windows/tauri/src/features/git/api/git-commits-api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { invoke as tauriInvoke } from "@/platform/tauri-core";
import type { GitCommit } from "../types/git.types";
import type { GitCommit, GitHistorySnapshot } from "../types/git.types";
import { emitGitChanged } from "../events/git-events";
import { runGitRead } from "../runtime/git-read-coordinator";
import {
Expand All @@ -24,24 +24,29 @@ export const commitChanges = async (repoPath: string, message: string): Promise<
}
};

export const getGitLog = async (repoPath: string, limit = 50, skip = 0): Promise<GitCommit[]> => {
export const getGitHistory = async (
repoPath: string,
limit = 50,
): Promise<GitHistorySnapshot | null> => {
try {
const resolvedRepoPath = await resolveRepositoryPath(repoPath);
if (!resolvedRepoPath) {
return [];
return null;
}

return await runGitRead(resolvedRepoPath, `log:${limit}:${skip}`, () =>
tauriInvoke<GitCommit[]>("git_log", {
return await runGitRead(resolvedRepoPath, `log:${limit}`, () =>
tauriInvoke<GitHistorySnapshot>("git_log", {
repoPath: resolvedRepoPath,
limit,
skip,
}),
);
} catch (error) {
if (!isNotGitRepositoryError(error)) {
console.error("Failed to get git log:", error);
}
return [];
return null;
}
};

export const getGitLog = async (repoPath: string, limit = 50): Promise<GitCommit[]> =>
(await getGitHistory(repoPath, limit))?.commits ?? [];
16 changes: 11 additions & 5 deletions windows/tauri/src/features/git/api/git-diff-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,14 +219,20 @@ export const getStatusDiffStats = async (repoPath: string): Promise<GitDiffStat[
}

const generation = getRepositoryCacheGeneration(resolvedRepoPath);
const request = tauriInvoke<GitDiffStat[]>("git_status_diff_stats", {
repoPath: resolvedRepoPath,
})
.then((stats) => {
const request = Promise.all([
tauriInvoke<GitDiffStat[]>("git_status_diff_stats", {
repoPath: resolvedRepoPath,
}),
tauriInvoke<GitDiffStat[]>("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)) {
Expand Down
45 changes: 45 additions & 0 deletions windows/tauri/src/features/git/api/git-repo-api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { beforeEach, describe, expect, mock, test } from "bun:test";

const invoke = mock(async (_command: string, _args?: unknown): Promise<string | null> => null);

mock.module("@/platform/tauri-core", () => ({ invoke }));

const { clearRepositoryDiscoveryCache, resolveRepositoryForFile } = await import("./git-repo-api");

beforeEach(() => {
invoke.mockReset();
clearRepositoryDiscoveryCache();
});

describe("resolveRepositoryForFile", () => {
test("discovers the repository from the file's directory", async () => {
invoke.mockResolvedValue("D:/work/project");

const result = await resolveRepositoryForFile("D:/work/project", "src/main.ts");

expect(invoke).toHaveBeenCalledWith("git_discover_repo", {
path: "D:/work/project/src",
});
expect(result).toEqual({
repoPath: "D:/work/project",
filePath: "src/main.ts",
});
});

test("keeps absolute file paths relative to the discovered repository", async () => {
invoke.mockResolvedValue("D:/work/project");

const result = await resolveRepositoryForFile(
"D:/work",
"D:\\work\\project\\src\\main.ts",
);

expect(invoke).toHaveBeenCalledWith("git_discover_repo", {
path: "D:/work/project/src",
});
expect(result).toEqual({
repoPath: "D:/work/project",
filePath: "src/main.ts",
});
});
});
12 changes: 11 additions & 1 deletion windows/tauri/src/features/git/api/git-repo-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,16 @@ function joinPath(basePath: string, childPath: string): string {
return normalizePath(`${base}/${child}`);
}

function parentPath(path: string): string {
const normalized = normalizePath(path);
const separatorIndex = normalized.lastIndexOf("/");
if (separatorIndex < 0) return normalized;
if (separatorIndex === 2 && /^[A-Za-z]:\//.test(normalized)) {
return normalized.slice(0, separatorIndex + 1);
}
return separatorIndex === 0 ? "/" : normalized.slice(0, separatorIndex);
}

function toRelativePath(from: string, to: string): string {
const normalizedFrom = normalizePath(from);
const normalizedTo = normalizePath(to);
Expand Down Expand Up @@ -205,7 +215,7 @@ export async function resolveRepositoryForFile(
filePath: string,
): Promise<{ repoPath: string; filePath: string } | null> {
const absoluteFilePath = isAbsolutePath(filePath) ? filePath : joinPath(repoPath, filePath);
const discoveredRepo = await discoverRepo(absoluteFilePath);
const discoveredRepo = await discoverRepo(parentPath(absoluteFilePath));

if (!discoveredRepo) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { calculateLineHeight, splitLines } from "@/features/editor/utils/lines";
import { useZoomStore } from "@/features/window/stores/zoom.store";
import { useUIState } from "@/features/window/stores/ui-state.store";
import { useFileSystemStore } from "@/features/file-system/stores/file-system.store";
import { useGitDiffPreferencesStore } from "@/features/git/stores/git-diff-preferences.store";
import {
buildSearchRegex,
findAllMatches,
Expand Down Expand Up @@ -650,7 +651,8 @@ const GitDiffEditorStack = memo(function GitDiffEditorStack({
const rootFolderPath = useFileSystemStore((state) => state.rootFolderPath);
const isFindVisible = useUIState((state) => state.isFindVisible);
const setIsFindVisible = useUIState((state) => state.setIsFindVisible);
const [viewMode, setViewMode] = useState<"unified" | "split">("unified");
const viewMode = useGitDiffPreferencesStore.use.viewMode();
const setViewMode = useGitDiffPreferencesStore.use.actions().setViewMode;
const [showWhitespace, setShowWhitespace] = useState(false);
const [isFileTreeVisible, setIsFileTreeVisible] = useState(true);
const [fileNavigatorViewMode, setFileNavigatorViewMode] = useState<FileNavigatorViewMode>("tree");
Expand Down Expand Up @@ -1034,8 +1036,12 @@ const GitDiffEditorStack = memo(function GitDiffEditorStack({
{multiDiff.title || "Uncommitted Changes"}
</span>
<span className="truncate">{indexedFileLabel}</span>
<span className="shrink-0 text-git-added">+{multiDiff.totalAdditions}</span>
<span className="shrink-0 text-git-deleted">-{multiDiff.totalDeletions}</span>
{multiDiff.totalAdditions > 0 ? (
<span className="shrink-0 text-git-added">+{multiDiff.totalAdditions}</span>
) : null}
{multiDiff.totalDeletions > 0 ? (
<span className="shrink-0 text-git-deleted">-{multiDiff.totalDeletions}</span>
) : null}
{isIndexingDiffs ? <span>{indexingLabel}</span> : null}
</div>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import Breadcrumb, {
import { useBufferStore } from "@/features/editor/stores/buffer.store";
import { cn } from "@/utils/cn";
import type { DiffHeaderProps } from "../../types/git-diff.types";
import { getFileStatus } from "../../utils/git-diff-helpers";
import { countDiffStats, getFileStatus } from "../../utils/git-diff-helpers";

const DiffHeader = memo(
({
Expand Down Expand Up @@ -45,12 +45,7 @@ const DiffHeader = memo(
const renderStats = () => {
if (!diff) return null;

let additions = 0;
let deletions = 0;
for (const l of diff.lines) {
if (l.line_type === "added") additions++;
else if (l.line_type === "removed") deletions++;
}
const { additions, deletions } = countDiffStats([diff]);

return (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { createGitHunk, parseDiffHunkRange } from "../../utils/git-diff-helpers"
const DiffHunkHeader = memo(
({
hunk,
stats,
hiddenLineCount,
isCollapsed,
onToggleCollapse,
Expand Down Expand Up @@ -66,11 +67,13 @@ const DiffHunkHeader = memo(
[rootFolderPath, filePath, hunk, isStaged, onStageHunk, onUnstageHunk],
);

let additions = 0;
let deletions = 0;
for (const l of hunk.lines) {
if (l.line_type === "added") additions++;
else if (l.line_type === "removed") deletions++;
let additions = stats?.additions ?? 0;
let deletions = stats?.deletions ?? 0;
if (!stats) {
for (const l of hunk.lines) {
if (l.line_type === "added") additions++;
else if (l.line_type === "removed") deletions++;
}
}

const headerInfo = parseDiffHunkRange(hunk.header.content);
Expand Down
Loading
Loading