fix(gitignore): write sf runtime patterns to .git/info/exclude, not .gitignore
ensureGitignore was re-adding `.sf`, `.sf-id`, `.bg-shell/` to the project's
.gitignore on every sf run, causing two issues:
1. Working-tree churn — every invocation dirtied .gitignore, forcing a
commit just to silence "uncommitted changes" warnings. Pattern flagged
by user: "is this the right way with its own every run".
2. False-positive duplicate-add — the literal-string check
(`existingLines.has(".sf")`) didn't recognize user-equivalent patterns
like `/.sf` (root-only) or `.sf/` (with trailing slash), so an explicit
user entry got duplicated by the auto-add on next run.
Fix: move sf-specific runtime patterns to `.git/info/exclude` via new
`ensureGitInfoExclude()`. That file is per-clone (not committed), so
re-writing is invisible to git status. The project's `.gitignore` stays
human-curated and sf doesn't opinionate on it.
`ensureGitignore()` now calls `ensureGitInfoExclude()` first so callers
don't need to update — backwards compatible. Generic OS/IDE/lang patterns
(.DS_Store, node_modules/, target/, etc.) stay in BASELINE_PATTERNS for
.gitignore since those genuinely belong in version control.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
6031106d93
commit
a2b709f669
1 changed files with 65 additions and 4 deletions
|
|
@ -46,11 +46,22 @@ const SF_RUNTIME_PATTERNS = [
|
|||
".sf/milestones/**/continue.md",
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* SF-specific runtime exclusion patterns. These live in .git/info/exclude
|
||||
* (per-clone, never committed) instead of .gitignore so that:
|
||||
* - Re-running sf doesn't dirty the working tree on every invocation
|
||||
* - The project's .gitignore stays human-curated (sf doesn't own it)
|
||||
* - User-equivalent patterns like `/.sf` (root-only) coexist without
|
||||
* triggering naive duplicate-add since we don't touch .gitignore at all
|
||||
* for these.
|
||||
*
|
||||
* Migrated out of BASELINE_PATTERNS on 2026-04-29.
|
||||
*/
|
||||
const SF_RUNTIME_EXCLUSION_PATTERNS = [".sf", ".sf-id", ".bg-shell/"] as const;
|
||||
|
||||
const BASELINE_PATTERNS = [
|
||||
// ── SF state directory (symlink to external storage) ──
|
||||
".sf",
|
||||
".sf-id",
|
||||
".bg-shell/",
|
||||
// SF-specific patterns now live in SF_RUNTIME_EXCLUSION_PATTERNS, applied
|
||||
// to .git/info/exclude via ensureGitInfoExclude() — see comment above.
|
||||
|
||||
// ── OS junk ──
|
||||
".DS_Store",
|
||||
|
|
@ -181,10 +192,60 @@ export function hasGitTrackedGsdFiles(basePath: string): boolean {
|
|||
* is excluded to prevent data loss. Only the `.sf` pattern is affected —
|
||||
* all other baseline patterns are still applied normally.
|
||||
*/
|
||||
/**
|
||||
* Write sf-specific runtime exclusion patterns (`.sf`, `.sf-id`, `.bg-shell/`)
|
||||
* to `.git/info/exclude` — per-clone, never committed, never causes
|
||||
* working-tree churn. Idempotent: only writes when something is missing.
|
||||
*
|
||||
* This replaces the old behavior of appending those patterns to `.gitignore`,
|
||||
* which caused two pain points:
|
||||
* 1. Every sf run dirtied the working tree because the literal-string
|
||||
* duplicate-check (`existingLines.has(".sf")`) didn't recognize
|
||||
* user-equivalent patterns like `/.sf`.
|
||||
* 2. sf was opinionating on a file that should be human-curated.
|
||||
*
|
||||
* Returns true if the file was modified, false if nothing needed adding or
|
||||
* the directory isn't a git repo (no `.git/info`).
|
||||
*/
|
||||
export function ensureGitInfoExclude(basePath: string): boolean {
|
||||
const gitInfoDir = join(basePath, ".git", "info");
|
||||
if (!existsSync(gitInfoDir)) return false;
|
||||
|
||||
const excludePath = join(gitInfoDir, "exclude");
|
||||
const existing = existsSync(excludePath)
|
||||
? readFileSync(excludePath, "utf-8")
|
||||
: "";
|
||||
|
||||
const existingLines = new Set(
|
||||
existing
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
.filter((l) => l && !l.startsWith("#")),
|
||||
);
|
||||
const missing = SF_RUNTIME_EXCLUSION_PATTERNS.filter(
|
||||
(p) => !existingLines.has(p),
|
||||
);
|
||||
if (missing.length === 0) return false;
|
||||
|
||||
const block = [
|
||||
"",
|
||||
"# ── SF runtime exclusion (managed by sf, per-clone) ──",
|
||||
...missing,
|
||||
"",
|
||||
].join("\n");
|
||||
const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
|
||||
writeFileSync(excludePath, existing + prefix + block, "utf-8");
|
||||
return true;
|
||||
}
|
||||
|
||||
export function ensureGitignore(
|
||||
basePath: string,
|
||||
options?: { manageGitignore?: boolean },
|
||||
): boolean {
|
||||
// SF-specific runtime patterns go to .git/info/exclude regardless of
|
||||
// manage_gitignore — it's per-clone and never affects committed files.
|
||||
ensureGitInfoExclude(basePath);
|
||||
|
||||
// If manage_gitignore is explicitly false, do not touch .gitignore at all
|
||||
if (options?.manageGitignore === false) return false;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue