Skip to content
Draft
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
2 changes: 1 addition & 1 deletion electron/src/ipc/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ function listFilesGit(cwd: string): Promise<string[]> {
execFile(
"git",
["ls-files", "--cached", "--others", "--exclude-standard"],
{ cwd, maxBuffer: 10 * 1024 * 1024 },
{ cwd, maxBuffer: 50 * 1024 * 1024, timeout: 30000 }, // 50MB buffer, 30s timeout
(err, stdout) => {
if (err) return reject(err);
resolve(stdout.split("\n").filter((f) => f.trim()).sort());
Expand Down
38 changes: 38 additions & 0 deletions electron/src/ipc/git.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ipcMain } from "electron";
import path from "path";
import fs from "fs";
import { execFile } from "child_process";
import { gitExec, ALWAYS_SKIP } from "../lib/git-exec";
import { captureEvent } from "../lib/posthog";
import { reportError } from "../lib/error-utils";
Expand Down Expand Up @@ -69,6 +70,34 @@ function validateRef(ref: string): void {
}
}

/**
* Get approximate size of .git directory in bytes.
* Returns -1 if unable to determine size.
*/
async function getGitDirSize(repoPath: string): Promise<number> {
try {
const gitDir = path.join(repoPath, ".git");
const stat = await fs.promises.stat(gitDir);
if (!stat.isDirectory()) return -1;

// Use du command for faster size estimation (sampling approach)
// For very large repos, we just need an order of magnitude, not exact size
return new Promise<number>((resolve) => {
execFile("du", ["-sk", gitDir], { timeout: 5000 }, (err, stdout) => {
if (err) {
resolve(-1);
return;
}
// du -sk returns size in KB as first token
const sizeKB = parseInt(stdout.split(/\s+/)[0], 10);
resolve(sizeKB * 1024); // Convert to bytes
});
});
} catch {
return -1;
}
}

export function register(): void {
ipcMain.handle("git:discover-repos", async (_event, projectPath: string) => {
const normalizedProjectPath = normalizePath(projectPath);
Expand Down Expand Up @@ -470,4 +499,13 @@ export function register(): void {
return { error: reportError("GIT_LOG_ERR", err) };
}
});

ipcMain.handle("git:get-git-dir-size", async (_event, cwd: string) => {
try {
const sizeBytes = await getGitDirSize(cwd);
return { sizeBytes };
} catch (err) {
return { error: reportError("GIT_GET_GIT_DIR_SIZE_ERR", err), sizeBytes: -1 };
}
});
}
7 changes: 5 additions & 2 deletions electron/src/lib/git-exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ export const ALWAYS_SKIP = new Set([
".gradle", ".idea", ".vs", ".vscode", "target", "out", "bin", "obj",
]);

export function gitExec(args: string[], cwd: string): Promise<string> {
export function gitExec(args: string[], cwd: string, options?: { timeout?: number; maxBuffer?: number }): Promise<string> {
return new Promise((resolve, reject) => {
execFile("git", args, { cwd, maxBuffer: 5 * 1024 * 1024 }, (err, stdout, stderr) => {
const timeout = options?.timeout ?? 30000; // 30s default timeout
const maxBuffer = options?.maxBuffer ?? 50 * 1024 * 1024; // 50MB default buffer (up from 5MB)

execFile("git", args, { cwd, maxBuffer, timeout }, (err, stdout, stderr) => {
if (err) return reject(new Error(stderr?.trim() || err.message));
resolve(stdout);
});
Expand Down
1 change: 1 addition & 0 deletions electron/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ contextBridge.exposeInMainWorld("claude", {
diffFile: (cwd: string, file: string, staged: boolean) => ipcRenderer.invoke("git:diff-file", { cwd, file, staged }),
diffStat: (cwd: string) => ipcRenderer.invoke("git:diff-stat", cwd) as Promise<{ additions: number; deletions: number }>,
log: (cwd: string, count?: number) => ipcRenderer.invoke("git:log", { cwd, count }),
getGitDirSize: (cwd: string) => ipcRenderer.invoke("git:get-git-dir-size", cwd) as Promise<{ sizeBytes: number; error?: string }>,
generateCommitMessage: (cwd: string, engine?: string, sessionId?: string) =>
ipcRenderer.invoke("git:generate-commit-message", { cwd, engine, sessionId }),
},
Expand Down
52 changes: 48 additions & 4 deletions src/hooks/useGitStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export interface RepoState {
branches: GitBranch[];
log: GitLogEntry[];
diffStat: DiffStat;
isLargeRepo?: boolean; // Track if repo is large (affects polling)
lastRefreshDuration?: number; // Track how long refresh takes
}

interface UseGitStatusOptions {
Expand All @@ -32,7 +34,35 @@ export function useGitStatus({ projectPath }: UseGitStatusOptions) {
}
(async () => {
const discovered = await window.claude.git.discoverRepos(projectPath);
setRepoStates(discovered.map((repo) => ({ repo, status: null, branches: [], log: [], diffStat: { additions: 0, deletions: 0 } })));

// Check .git directory sizes to detect very large repos
const statesWithSizeCheck = await Promise.all(
discovered.map(async (repo) => {
const { sizeBytes } = await window.claude.git.getGitDirSize(repo.path);
const gitSizeGB = sizeBytes > 0 ? sizeBytes / (1024 * 1024 * 1024) : 0;
const isVeryLarge = gitSizeGB > 10; // 10GB+ .git is very large

// Log warning for very large repos
if (isVeryLarge) {
console.warn(
`[useGitStatus] Detected very large .git directory (${gitSizeGB.toFixed(1)}GB) at ${repo.path}. ` +
`Git operations may be slow. Consider using git worktrees or shallow clones to improve performance.`
);
}

return {
repo,
status: null,
branches: [],
log: [],
diffStat: { additions: 0, deletions: 0 },
isLargeRepo: isVeryLarge,
lastRefreshDuration: 0,
};
})
);

setRepoStates(statesWithSizeCheck);
})();
}, [projectPath]);

Expand All @@ -43,18 +73,23 @@ export function useGitStatus({ projectPath }: UseGitStatusOptions) {
try {
const updated = await Promise.all(
states.map(async (rs) => {
const startTime = performance.now();
const [statusResult, branchesResult, logResult, diffStatResult] = await Promise.all([
window.claude.git.status(rs.repo.path),
window.claude.git.branches(rs.repo.path),
window.claude.git.log(rs.repo.path, 30),
window.claude.git.diffStat(rs.repo.path),
]);
const duration = performance.now() - startTime;
const isLargeRepo = duration > 5000; // Mark as large if refresh takes >5s
return {
repo: rs.repo,
status: (!("error" in statusResult) || !statusResult.error) ? statusResult as GitStatus : rs.status,
branches: Array.isArray(branchesResult) ? branchesResult : rs.branches,
log: Array.isArray(logResult) ? logResult : rs.log,
diffStat: diffStatResult ?? rs.diffStat,
isLargeRepo,
lastRefreshDuration: duration,
};
}),
);
Expand All @@ -69,12 +104,15 @@ export function useGitStatus({ projectPath }: UseGitStatusOptions) {
const idx = states.findIndex((rs) => rs.repo.path === repoPath);
if (idx === -1) return;
const rs = states[idx];
const startTime = performance.now();
const [statusResult, branchesResult, logResult, diffStatResult] = await Promise.all([
window.claude.git.status(rs.repo.path),
window.claude.git.branches(rs.repo.path),
window.claude.git.log(rs.repo.path, 30),
window.claude.git.diffStat(rs.repo.path),
]);
const duration = performance.now() - startTime;
const isLargeRepo = duration > 5000;
setRepoStates((prev) => {
const next = [...prev];
next[idx] = {
Expand All @@ -83,19 +121,25 @@ export function useGitStatus({ projectPath }: UseGitStatusOptions) {
branches: Array.isArray(branchesResult) ? branchesResult : rs.branches,
log: Array.isArray(logResult) ? logResult : rs.log,
diffStat: diffStatResult ?? rs.diffStat,
isLargeRepo,
lastRefreshDuration: duration,
};
return next;
});
}, []);

// Poll all repos every 3s
// Poll repos with adaptive interval (3s for normal repos, 15s for large repos)
useEffect(() => {
if (repoStates.length === 0) return;
refreshAll();

// Determine polling interval based on whether we have any large repos
const hasLargeRepo = repoStates.some((rs) => rs.isLargeRepo);
const pollInterval = hasLargeRepo ? 15000 : 3000; // 15s for large repos, 3s for normal

const interval = setInterval(() => {
if (!document.hidden) refreshAll();
}, 3000);
}, pollInterval);

const onVisibilityChange = () => {
if (!document.hidden) refreshAll();
Expand All @@ -106,7 +150,7 @@ export function useGitStatus({ projectPath }: UseGitStatusOptions) {
clearInterval(interval);
document.removeEventListener("visibilitychange", onVisibilityChange);
};
}, [repoStates.length, refreshAll]);
}, [repoStates.length, refreshAll, repoStates.some((rs) => rs.isLargeRepo)]);

// Per-repo action creators
const stage = useCallback(
Expand Down