fix: exclude .gsd/ from pre-switch auto-commits to prevent squash merge conflicts (#143)
Pre-switch auto-commits were including .gsd/ planning artifacts (roadmaps, STATE.md) on both sides of a branch switch, causing reliable merge conflicts when squash-merging slices back to main. Now pre-switch auto-commits exclude the entire .gsd/ directory, while post-task auto-commits continue to include them normally. Also restores VALID_BRANCH_NAME export removed in a prior merge conflict resolution. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7a1eac6af3
commit
db2a409d7d
2 changed files with 57 additions and 7 deletions
|
|
@ -28,6 +28,8 @@ export interface GitPreferences {
|
|||
commit_type?: string;
|
||||
}
|
||||
|
||||
export const VALID_BRANCH_NAME = /^[a-zA-Z0-9_\-\/.]+$/;
|
||||
|
||||
export interface CommitOptions {
|
||||
message: string;
|
||||
allowEmpty?: boolean;
|
||||
|
|
@ -123,9 +125,11 @@ export class GitServiceImpl {
|
|||
/**
|
||||
* Smart staging: `git add -A` excluding GSD runtime paths via pathspec.
|
||||
* Falls back to plain `git add -A` if the exclusion pathspec fails.
|
||||
* @param extraExclusions Additional pathspec exclusions beyond RUNTIME_EXCLUSION_PATHS.
|
||||
*/
|
||||
private smartStage(): void {
|
||||
const excludes = RUNTIME_EXCLUSION_PATHS.map(p => `':(exclude)${p}'`);
|
||||
private smartStage(extraExclusions: readonly string[] = []): void {
|
||||
const allExclusions = [...RUNTIME_EXCLUSION_PATHS, ...extraExclusions];
|
||||
const excludes = allExclusions.map(p => `':(exclude)${p}'`);
|
||||
const args = ["add", "-A", "--", ".", ...excludes];
|
||||
try {
|
||||
this.git(args);
|
||||
|
|
@ -157,13 +161,14 @@ export class GitServiceImpl {
|
|||
/**
|
||||
* Auto-commit dirty working tree with a conventional chore message.
|
||||
* Returns the commit message on success, or null if nothing to commit.
|
||||
* @param extraExclusions Additional paths to exclude from staging (e.g. [".gsd/"] for pre-switch commits).
|
||||
*/
|
||||
autoCommit(unitType: string, unitId: string): string | null {
|
||||
autoCommit(unitType: string, unitId: string, extraExclusions: readonly string[] = []): string | null {
|
||||
// Quick check: is there anything dirty at all?
|
||||
const status = this.git(["status", "--short"], { allowFailure: true });
|
||||
if (!status) return null;
|
||||
|
||||
this.smartStage();
|
||||
this.smartStage(extraExclusions);
|
||||
|
||||
// After smart staging, check if anything was actually staged
|
||||
// (all changes might have been runtime files that got excluded)
|
||||
|
|
@ -297,8 +302,9 @@ export class GitServiceImpl {
|
|||
}
|
||||
}
|
||||
|
||||
// Auto-commit dirty state via smart staging before checkout
|
||||
this.autoCommit("pre-switch", current);
|
||||
// Auto-commit dirty state via smart staging before checkout.
|
||||
// Exclude .gsd/ to prevent merge conflicts when both branches modify planning artifacts.
|
||||
this.autoCommit("pre-switch", current, [".gsd/"]);
|
||||
|
||||
this.git(["checkout", branch]);
|
||||
return created;
|
||||
|
|
@ -312,7 +318,8 @@ export class GitServiceImpl {
|
|||
const current = this.getCurrentBranch();
|
||||
if (current === mainBranch) return;
|
||||
|
||||
this.autoCommit("pre-switch", current);
|
||||
// Exclude .gsd/ to prevent merge conflicts when both branches modify planning artifacts.
|
||||
this.autoCommit("pre-switch", current, [".gsd/"]);
|
||||
|
||||
this.git(["checkout", mainBranch]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -453,6 +453,49 @@ async function main(): Promise<void> {
|
|||
rmSync(repo, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// ─── GitServiceImpl: autoCommit with extraExclusions ───────────────────
|
||||
|
||||
console.log("\n=== GitServiceImpl: autoCommit with extraExclusions ===");
|
||||
|
||||
{
|
||||
const repo = initTempRepo();
|
||||
const svc = new GitServiceImpl(repo);
|
||||
|
||||
// Create both a .gsd/ planning file and a regular source file
|
||||
createFile(repo, ".gsd/milestones/M001/M001-ROADMAP.md", "- [x] S01");
|
||||
createFile(repo, "src/feature.ts", "export const y = 2;");
|
||||
|
||||
// Auto-commit with .gsd/ excluded (simulates pre-switch)
|
||||
const msg = svc.autoCommit("pre-switch", "main", [".gsd/"]);
|
||||
assertEq(msg, "chore(main): auto-commit after pre-switch", "pre-switch autoCommit with .gsd/ exclusion commits");
|
||||
|
||||
// Verify .gsd/ file was NOT committed
|
||||
const show = run("git show --stat HEAD", repo);
|
||||
assert(!show.includes("ROADMAP"), ".gsd/ files excluded from pre-switch auto-commit");
|
||||
assert(show.includes("feature.ts"), "non-.gsd/ files included in pre-switch auto-commit");
|
||||
|
||||
rmSync(repo, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// ─── GitServiceImpl: autoCommit extraExclusions — only .gsd/ dirty ────
|
||||
|
||||
console.log("\n=== GitServiceImpl: autoCommit extraExclusions — only .gsd/ dirty ===");
|
||||
|
||||
{
|
||||
const repo = initTempRepo();
|
||||
const svc = new GitServiceImpl(repo);
|
||||
|
||||
// Create only .gsd/ planning files
|
||||
createFile(repo, ".gsd/milestones/M001/M001-ROADMAP.md", "- [x] S01");
|
||||
createFile(repo, ".gsd/STATE.md", "state content");
|
||||
|
||||
// Auto-commit with .gsd/ excluded — nothing else to commit
|
||||
const result = svc.autoCommit("pre-switch", "main", [".gsd/"]);
|
||||
assertEq(result, null, "autoCommit returns null when only .gsd/ files are dirty and excluded");
|
||||
|
||||
rmSync(repo, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// ─── GitServiceImpl: commit returns null when nothing staged ───────────
|
||||
|
||||
console.log("\n=== GitServiceImpl: commit empty ===");
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue