fix: use pathspec exclusions in smartStage to prevent hanging on large repos (#1613)
smartStage() ran `git add -A` on the entire repo then unstaged exclusions, causing indefinite hangs on repos with large untracked artifact trees (57GB+). autoCommitDirtyState() bypassed smartStage() entirely via direct nativeAddAll(). Add nativeAddAllWithExclusions() using `git add -A -- ':!pattern'` syntax so excluded paths are never hashed. Route autoCommitDirtyState() through it with RUNTIME_EXCLUSION_PATHS. Closes #1605 Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2f5323ee97
commit
426e0e839c
3 changed files with 47 additions and 12 deletions
|
|
@ -37,13 +37,13 @@ import {
|
|||
resolveGitHeadPath,
|
||||
nudgeGitBranchCache,
|
||||
} from "./worktree.js";
|
||||
import { MergeConflictError, readIntegrationBranch } from "./git-service.js";
|
||||
import { MergeConflictError, readIntegrationBranch, RUNTIME_EXCLUSION_PATHS } from "./git-service.js";
|
||||
import { parseRoadmap } from "./files.js";
|
||||
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
||||
import {
|
||||
nativeGetCurrentBranch,
|
||||
nativeWorkingTreeStatus,
|
||||
nativeAddAll,
|
||||
nativeAddAllWithExclusions,
|
||||
nativeCommit,
|
||||
nativeCheckoutBranch,
|
||||
nativeMergeSquash,
|
||||
|
|
@ -768,7 +768,7 @@ function autoCommitDirtyState(cwd: string): boolean {
|
|||
try {
|
||||
const status = nativeWorkingTreeStatus(cwd);
|
||||
if (!status) return false;
|
||||
nativeAddAll(cwd);
|
||||
nativeAddAllWithExclusions(cwd, RUNTIME_EXCLUSION_PATHS);
|
||||
const result = nativeCommit(
|
||||
cwd,
|
||||
"chore: auto-commit before milestone merge",
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import {
|
|||
nativeDetectMainBranch,
|
||||
nativeBranchExists,
|
||||
nativeHasChanges,
|
||||
nativeAddAll,
|
||||
nativeAddAllWithExclusions,
|
||||
nativeResetPaths,
|
||||
nativeHasStagedChanges,
|
||||
nativeCommit,
|
||||
|
|
@ -385,7 +385,9 @@ export class GitServiceImpl {
|
|||
this._runtimeFilesCleanedUp = true;
|
||||
}
|
||||
|
||||
// Stage everything, then unstage excluded paths.
|
||||
// Stage everything using pathspec exclusions so excluded paths are never
|
||||
// hashed by git. The old approach of `git add -A` followed by unstaging
|
||||
// hangs indefinitely on repos with large untracked artifact trees (#1605).
|
||||
//
|
||||
// Exclude only RUNTIME paths from staging — not the entire .gsd/ directory.
|
||||
// When .gsd/milestones/ files are already tracked in the index (projects
|
||||
|
|
@ -395,13 +397,9 @@ export class GitServiceImpl {
|
|||
// the second half of a milestone's artifacts are never committed (#1326).
|
||||
//
|
||||
// If .gsd/ IS in .gitignore (the default for external state projects),
|
||||
// git add -A already skips it and the reset is a harmless no-op.
|
||||
nativeAddAll(this.basePath);
|
||||
|
||||
const runtimeExclusions = [...RUNTIME_EXCLUSION_PATHS, ...extraExclusions];
|
||||
for (const exclusion of runtimeExclusions) {
|
||||
try { nativeResetPaths(this.basePath, [exclusion]); } catch { /* path not staged — ignore */ }
|
||||
}
|
||||
// git add -A already skips it and the exclusions are harmless no-ops.
|
||||
const allExclusions = [...RUNTIME_EXCLUSION_PATHS, ...extraExclusions];
|
||||
nativeAddAllWithExclusions(this.basePath, allExclusions);
|
||||
}
|
||||
|
||||
/** Tracks whether runtime file cleanup has run this session. */
|
||||
|
|
|
|||
|
|
@ -671,6 +671,43 @@ export function nativeAddAll(basePath: string): void {
|
|||
gitFileExec(basePath, ["add", "-A"]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stage all files with pathspec exclusions (git add -A -- ':!pattern' ...).
|
||||
* Excluded paths are never hashed by git, preventing hangs on large
|
||||
* untracked artifact trees (57GB+, 11K+ files). See #1605.
|
||||
*
|
||||
* Falls back to plain `git add -A` when no exclusions are provided.
|
||||
* Always uses the CLI path (not libgit2) because libgit2's add_all
|
||||
* does not support pathspec exclusion syntax.
|
||||
*
|
||||
* When excluded paths are already covered by .gitignore, git may exit
|
||||
* with code 1 and an "ignored by .gitignore" warning. This is harmless
|
||||
* (the staging succeeds for all non-ignored files) and is suppressed.
|
||||
*/
|
||||
export function nativeAddAllWithExclusions(basePath: string, exclusions: readonly string[]): void {
|
||||
if (exclusions.length === 0) {
|
||||
nativeAddAll(basePath);
|
||||
return;
|
||||
}
|
||||
const pathspecs = exclusions.map(e => `:!${e}`);
|
||||
try {
|
||||
execFileSync("git", ["add", "-A", "--", ...pathspecs], {
|
||||
cwd: basePath,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
encoding: "utf-8",
|
||||
env: GIT_NO_PROMPT_ENV,
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
// git exits 1 when pathspec exclusions reference paths already covered
|
||||
// by .gitignore. The staging itself succeeds — only suppress that case.
|
||||
const stderr = (err as { stderr?: string })?.stderr ?? "";
|
||||
if (stderr.includes("ignored by one of your .gitignore files")) {
|
||||
return;
|
||||
}
|
||||
throw new GSDError(GSD_GIT_ERROR, `git add -A with exclusions failed in ${basePath}: ${getErrorMessage(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stage specific files.
|
||||
* Native: libgit2 index add.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue