Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,4 @@ memory/handoff-inbox/
.brigade/runs/
.brigade/scrub-cache/
.brigade/work/
.solo-mise/logs/
.solo-mise/scrub-cache/
# <<< brigade gitignore block <<<
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
46 changes: 39 additions & 7 deletions src/brigade/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,20 +84,52 @@ 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"
gi.write_text(existing + sep + "\n" + block)
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.

Expand Down
42 changes: 42 additions & 0 deletions tests/test_gitignore.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading