fix: auto-resolve build artifact conflicts in milestone merge (#2777)
* fix: auto-resolve build artifact conflicts in milestone merge The binary conflict classification in mergeMilestoneToMain only auto-resolved .gsd/ prefixed files. Machine-generated build artifacts like .tsbuildinfo, .pyc, __pycache__/, .DS_Store, and .map files were treated as real code conflicts, blocking auto-merge unnecessarily. Extract an isSafeToAutoResolve helper that checks both the .gsd/ prefix and a SAFE_AUTO_RESOLVE_PATTERNS regex list. Matched files are resolved with --theirs, same as .gsd/ state files. Closes #2761 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add tests for build artifact auto-resolve patterns Extract isSafeToAutoResolve and SAFE_AUTO_RESOLVE_PATTERNS to module-level exports for testability. Add unit tests covering .gsd/ state files, build artifacts (.tsbuildinfo, .pyc, __pycache__, .DS_Store, .map), and rejection of real source files (.ts, .js, .py, .json, .md). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
46016adf9a
commit
f96a26b3c9
2 changed files with 112 additions and 13 deletions
|
|
@ -157,6 +157,25 @@ function clearProjectRootStateFiles(basePath: string, milestoneId: string): void
|
|||
}
|
||||
}
|
||||
|
||||
// ─── Build Artifact Auto-Resolve ─────────────────────────────────────────────
|
||||
|
||||
/** Patterns for machine-generated build artifacts that can be safely
|
||||
* auto-resolved by accepting --theirs during merge. These files are
|
||||
* regenerable and never contain meaningful manual edits. */
|
||||
export const SAFE_AUTO_RESOLVE_PATTERNS: RegExp[] = [
|
||||
/\.tsbuildinfo$/,
|
||||
/\.pyc$/,
|
||||
/\/__pycache__\//,
|
||||
/\.DS_Store$/,
|
||||
/\.map$/,
|
||||
];
|
||||
|
||||
/** Returns true if the file path is safe to auto-resolve during merge.
|
||||
* Covers `.gsd/` state files and common build artifacts. */
|
||||
export const isSafeToAutoResolve = (filePath: string): boolean =>
|
||||
filePath.startsWith(".gsd/") ||
|
||||
SAFE_AUTO_RESOLVE_PATTERNS.some((re) => re.test(filePath));
|
||||
|
||||
// ─── Dispatch-Level Sync (project root ↔ worktree) ──────────────────────────
|
||||
|
||||
/**
|
||||
|
|
@ -1408,30 +1427,30 @@ export function mergeMilestoneToMain(
|
|||
: nativeConflictFiles(originalBasePath_);
|
||||
|
||||
if (conflictedFiles.length > 0) {
|
||||
// Separate .gsd/ state file conflicts from real code conflicts.
|
||||
// GSD state files (STATE.md, auto.lock, etc.)
|
||||
// diverge between branches during normal operation — always prefer the
|
||||
// milestone branch version since it has the latest execution state.
|
||||
const gsdConflicts = conflictedFiles.filter((f) => f.startsWith(".gsd/"));
|
||||
// Separate auto-resolvable conflicts (GSD state files + build artifacts)
|
||||
// from real code conflicts. GSD state files diverge between branches
|
||||
// during normal operation. Build artifacts are machine-generated and
|
||||
// regenerable. Both are safe to accept from the milestone branch.
|
||||
const autoResolvable = conflictedFiles.filter(isSafeToAutoResolve);
|
||||
const codeConflicts = conflictedFiles.filter(
|
||||
(f) => !f.startsWith(".gsd/"),
|
||||
(f) => !isSafeToAutoResolve(f),
|
||||
);
|
||||
|
||||
// Auto-resolve .gsd/ conflicts by accepting the milestone branch version
|
||||
if (gsdConflicts.length > 0) {
|
||||
for (const gsdFile of gsdConflicts) {
|
||||
// Auto-resolve safe conflicts by accepting the milestone branch version
|
||||
if (autoResolvable.length > 0) {
|
||||
for (const safeFile of autoResolvable) {
|
||||
try {
|
||||
nativeCheckoutTheirs(originalBasePath_, [gsdFile]);
|
||||
nativeAddPaths(originalBasePath_, [gsdFile]);
|
||||
nativeCheckoutTheirs(originalBasePath_, [safeFile]);
|
||||
nativeAddPaths(originalBasePath_, [safeFile]);
|
||||
} catch {
|
||||
// If checkout --theirs fails, try removing the file from the merge
|
||||
// (it's a runtime file that shouldn't be committed anyway)
|
||||
nativeRmForce(originalBasePath_, [gsdFile]);
|
||||
nativeRmForce(originalBasePath_, [safeFile]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there are still non-.gsd conflicts, escalate
|
||||
// If there are still real code conflicts, escalate
|
||||
if (codeConflicts.length > 0) {
|
||||
// Pop stash before throwing so local work is not lost (#2151).
|
||||
if (stashed) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
/**
|
||||
* auto-worktree-auto-resolve.test.ts — Unit tests for isSafeToAutoResolve.
|
||||
*
|
||||
* Covers: .gsd/ state files, build artifacts (.tsbuildinfo, .pyc, __pycache__,
|
||||
* .DS_Store, .map), and rejection of real source files.
|
||||
*/
|
||||
|
||||
import { describe, test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
isSafeToAutoResolve,
|
||||
SAFE_AUTO_RESOLVE_PATTERNS,
|
||||
} from "../auto-worktree.ts";
|
||||
|
||||
describe("isSafeToAutoResolve", () => {
|
||||
// ─── .gsd/ state files ───────────────────────────────────────────────────
|
||||
test("returns true for .gsd/ prefixed paths", () => {
|
||||
assert.ok(isSafeToAutoResolve(".gsd/STATE.md"));
|
||||
assert.ok(isSafeToAutoResolve(".gsd/milestones/M001/CONTEXT.md"));
|
||||
assert.ok(isSafeToAutoResolve(".gsd/gsd.db"));
|
||||
});
|
||||
|
||||
// ─── Build artifact patterns ─────────────────────────────────────────────
|
||||
test("returns true for .tsbuildinfo files", () => {
|
||||
assert.ok(isSafeToAutoResolve("tsconfig.tsbuildinfo"));
|
||||
assert.ok(isSafeToAutoResolve("dist/tsconfig.tsbuildinfo"));
|
||||
});
|
||||
|
||||
test("returns true for .pyc files", () => {
|
||||
assert.ok(isSafeToAutoResolve("module.pyc"));
|
||||
assert.ok(isSafeToAutoResolve("src/utils/helpers.pyc"));
|
||||
});
|
||||
|
||||
test("returns true for __pycache__/ paths", () => {
|
||||
assert.ok(isSafeToAutoResolve("src/__pycache__/module.cpython-311.pyc"));
|
||||
assert.ok(isSafeToAutoResolve("lib/__pycache__/foo.py"));
|
||||
});
|
||||
|
||||
test("returns true for .DS_Store files", () => {
|
||||
assert.ok(isSafeToAutoResolve(".DS_Store"));
|
||||
assert.ok(isSafeToAutoResolve("src/.DS_Store"));
|
||||
});
|
||||
|
||||
test("returns true for .map source map files", () => {
|
||||
assert.ok(isSafeToAutoResolve("dist/index.js.map"));
|
||||
assert.ok(isSafeToAutoResolve("out/bundle.css.map"));
|
||||
});
|
||||
|
||||
// ─── Real source files (should NOT be auto-resolved) ─────────────────────
|
||||
test("returns false for .ts source files", () => {
|
||||
assert.ok(!isSafeToAutoResolve("src/index.ts"));
|
||||
assert.ok(!isSafeToAutoResolve("lib/utils.ts"));
|
||||
});
|
||||
|
||||
test("returns false for .js source files", () => {
|
||||
assert.ok(!isSafeToAutoResolve("src/index.js"));
|
||||
assert.ok(!isSafeToAutoResolve("lib/helpers.js"));
|
||||
});
|
||||
|
||||
test("returns false for .py source files", () => {
|
||||
assert.ok(!isSafeToAutoResolve("src/main.py"));
|
||||
assert.ok(!isSafeToAutoResolve("scripts/deploy.py"));
|
||||
});
|
||||
|
||||
test("returns false for config and data files", () => {
|
||||
assert.ok(!isSafeToAutoResolve("package.json"));
|
||||
assert.ok(!isSafeToAutoResolve("tsconfig.json"));
|
||||
assert.ok(!isSafeToAutoResolve("README.md"));
|
||||
});
|
||||
|
||||
// ─── SAFE_AUTO_RESOLVE_PATTERNS export ────────────────────────────────────
|
||||
test("SAFE_AUTO_RESOLVE_PATTERNS is a non-empty array of RegExp", () => {
|
||||
assert.ok(Array.isArray(SAFE_AUTO_RESOLVE_PATTERNS));
|
||||
assert.ok(SAFE_AUTO_RESOLVE_PATTERNS.length > 0);
|
||||
for (const pattern of SAFE_AUTO_RESOLVE_PATTERNS) {
|
||||
assert.ok(pattern instanceof RegExp);
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue