diff --git a/.gitignore b/.gitignore index 17539c04..10c9e6a0 100644 --- a/.gitignore +++ b/.gitignore @@ -65,6 +65,4 @@ memory/handoff-inbox/ .brigade/runs/ .brigade/scrub-cache/ .brigade/work/ -.solo-mise/logs/ -.solo-mise/scrub-cache/ # <<< brigade gitignore block <<< diff --git a/CHANGELOG.md b/CHANGELOG.md index b0f81143..dab45961 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Handoff write failures now preserve final run artifacts, print the final answer, return nonzero, and mark `run.json` as `handoff-failed`. - Dogfood runs default to prompt-level read-only plus Codex's `danger-full-access` sandbox setting for trusted-workspace use so repo inspection works on hosts where native read-only sandboxing blocks shell inspection; `--native-read-only-sandbox` opts into stricter native enforcement. +### Fixed +- `brigade init` now collapses mixed current and legacy managed `.gitignore` blocks into one regenerated Brigade block. + ## [0.6.0] - 2026-05-24 ### Added diff --git a/src/brigade/install.py b/src/brigade/install.py index bb954484..5a693088 100644 --- a/src/brigade/install.py +++ b/src/brigade/install.py @@ -84,13 +84,8 @@ def apply_gitignore(target: Path, selection: Selection) -> str: (GITIGNORE_BEGIN, GITIGNORE_END), (LEGACY_GITIGNORE_BEGIN, LEGACY_GITIGNORE_END), ) - for begin, end in markers: - if begin not in existing or end not in existing: - continue - prefix, _, rest = existing.partition(begin) - _, _, suffix = rest.partition(end) - # Strip a trailing newline from prefix and a leading newline from suffix to avoid drift. - new_text = prefix.rstrip("\n") + ("\n\n" if prefix.strip() else "") + block + suffix.lstrip("\n") + new_text, replaced = _replace_managed_gitignore_blocks(existing, block, markers) + if replaced: gi.write_text(new_text) return "updated" sep = "" if existing.endswith("\n") else "\n" @@ -98,6 +93,43 @@ def apply_gitignore(target: Path, selection: Selection) -> str: return "updated" +def _replace_managed_gitignore_blocks( + existing: str, + block: str, + markers: tuple[tuple[str, str], ...], +) -> tuple[str, bool]: + """Replace all complete known managed blocks with one regenerated block.""" + output: list[str] = [] + cursor = 0 + inserted = False + replaced = False + while True: + next_block: tuple[int, int] | None = None + for begin, end in markers: + start = existing.find(begin, cursor) + if start == -1: + continue + end_start = existing.find(end, start + len(begin)) + if end_start == -1: + continue + stop = end_start + len(end) + if next_block is None or start < next_block[0]: + next_block = (start, stop) + if next_block is None: + break + start, stop = next_block + output.append(existing[cursor:start]) + if not inserted: + output.append(block) + inserted = True + cursor = stop + replaced = True + if not replaced: + return existing, False + output.append(existing[cursor:]) + return "".join(output), True + + def resolve_manifests(selection: Selection) -> Tuple[List[dict], List[str], List[str]]: """Return (files, dirs, post_install_notes) for a Selection. diff --git a/tests/test_gitignore.py b/tests/test_gitignore.py index f1fefeaf..d6a518a3 100644 --- a/tests/test_gitignore.py +++ b/tests/test_gitignore.py @@ -96,6 +96,48 @@ def test_init_replaces_legacy_solo_mise_gitignore_block(tmp_target: Path): assert gi.count(install_mod.GITIGNORE_BEGIN) == 1 +def test_init_collapses_current_and_legacy_gitignore_blocks(tmp_target: Path): + tmp_target.mkdir() + (tmp_target / ".gitignore").write_text( + "\n".join( + [ + "# user rules", + "*.log", + "", + install_mod.GITIGNORE_BEGIN, + "STALE_CURRENT_LINE", + install_mod.GITIGNORE_END, + "", + "# between blocks", + ".cache/", + "", + install_mod.LEGACY_GITIGNORE_BEGIN, + "STALE_LEGACY_LINE", + install_mod.LEGACY_GITIGNORE_END, + "", + "# after block", + ".local-cache/", + "", + ] + ) + ) + + rc = install_selection(tmp_target, _repo_selection()) + + assert rc == 0 + gi = _read_gi(tmp_target) + assert "# user rules" in gi + assert "# between blocks" in gi + assert ".cache/" in gi + assert ".local-cache/" in gi + assert "STALE_CURRENT_LINE" not in gi + assert "STALE_LEGACY_LINE" not in gi + assert install_mod.LEGACY_GITIGNORE_BEGIN not in gi + assert install_mod.LEGACY_GITIGNORE_END not in gi + assert gi.count(install_mod.GITIGNORE_BEGIN) == 1 + assert gi.count(install_mod.GITIGNORE_END) == 1 + + def test_init_preserves_user_edits_outside_block(tmp_target: Path): tmp_target.mkdir() pre = "node_modules/\n# user rules\n*.swp\n"