Skip to content

fix(init): merge .gitignore with user content instead of overwriting (#6)#7

Merged
kevinkod merged 1 commit intomainfrom
fix/gitignore-merge
May 1, 2026
Merged

fix(init): merge .gitignore with user content instead of overwriting (#6)#7
kevinkod merged 1 commit intomainfrom
fix/gitignore-merge

Conversation

@kevinkod
Copy link
Copy Markdown
Contributor

@kevinkod kevinkod commented May 1, 2026

Closes #6.

Summary

Brownfield projects (Vite, Next, Astro, etc.) all ship a .gitignore. Before this change, specflow init --here reported it as a conflict and --force then backed up the user file and wrote specflow's stub — silently losing all framework ignores.

The fix introduces a new mergeable-project-root bundle category and an append-block strategy: specflow's .gitignore is wrapped in fence markers and merged into any existing user file, non-destructively, idempotently, and without --force.

Before / After

 # Logs
 logs
 *.log
 ...
 node_modules
 dist
 .DS_Store
 .vscode/*
+
+# --- Specflow: gitignore ---
+*.specflow.bak
+.specflow/config.yml.local
+# --- End Specflow: gitignore ---

User file fully preserved. No .gitignore.specflow.bak. No --force required.

Architectural shape

  • New CoreCategory value mergeable-project-root (currently only .gitignore uses it).
  • New TemplateFile.mergeBlock?: string field carrying the fence label.
  • DenoFsWriter.detectConflicts skips entries with mergeBlock set.
  • DenoFsWriter.writeBundle reads any pre-existing file and merges via mergeIntoFile() from the new src/domain/merge_block.ts helper.
  • Lock SHA for mergeable files stores the canonical block body (trimmed) so it stays comparable against the body extracted from disk on upgrade reads.
  • All 8 harnesses route mergeable-project-root to the same destination as project-root and stamp mergeBlock: \"gitignore\". No harness-specific divergence.

Test plan

  • deno task test — 316 / 316 pass (was 313 before; +3 new brownfield tests).
  • New integration tests in tests/integration/init_brownfield_gitignore_test.ts:
    • Brownfield: pre-existing Vite-style .gitignore → init succeeds without --force, both user lines and specflow block present, no .bak file created.
    • Idempotency: running init twice with --force does not duplicate the specflow block.
    • Greenfield: fresh init writes the .gitignore wrapped in fence markers (same shape regardless of whether merge was needed).
  • End-to-end smoke against a real npm create vite@latest project — Vite's 24-line .gitignore (logs, node_modules, dist, .DS_Store, .vscode, etc.) preserved verbatim with the specflow block appended.

🤖 Generated with Claude Code

)

Closes #6.

Brownfield projects (Vite, Next, Astro, etc.) all ship a `.gitignore`.
Before this change, `specflow init --here` reported it as a conflict
and `--force` then backed up the user file and wrote specflow's stub —
silently losing all the framework's ignores.

This fix introduces a new `mergeable-project-root` bundle category and
an append-block strategy in the FsWriter:

- Specflow's `.gitignore` content is wrapped in fence markers
  `# --- Specflow: gitignore ---` / `# --- End Specflow: gitignore ---`
- On a brownfield init, the block is appended to the existing user
  file. No backup, no conflict, no `--force` needed.
- Re-running init replaces the existing block in place (idempotent).
- `specflow upgrade` also extracts and re-syncs only the block; user
  lines outside the block are preserved.

Lock SHA for mergeable files stores the canonical block body (no
leading/trailing newlines), comparable against the body extracted
from disk on subsequent reads.

All 8 harnesses route `mergeable-project-root` to the same destination
as `project-root` and stamp `mergeBlock: "gitignore"` on the bundle
entry — no harness-specific divergence.

Reproduction case (verified end-to-end on a fresh
`npm create vite@latest`): user's 24-line Vite `.gitignore` is
preserved verbatim with the specflow block appended at the end.

Tests: 3 new integration tests cover greenfield, brownfield, and
idempotency (316 pass total, 0 fail).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kevinkod kevinkod merged commit d1a6c34 into main May 1, 2026
4 checks passed
@kevinkod kevinkod deleted the fix/gitignore-merge branch May 1, 2026 21:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

init --here destroys user .gitignore on brownfield projects

1 participant