mirror of
https://github.com/github/spec-kit.git
synced 2026-08-15 14:02:04 +02:00
feat(init): scaffold managed .specify/.gitignore (#4000)
* feat(init): scaffold managed .specify/.gitignore Write a manifest-tracked `.specify/.gitignore` during shared-infra install so machine-local Spec Kit state stays out of version control while everything else under `.specify/` remains shareable: - `feature.json` — the current-feature pointer, rewritten on every feature switch (per-checkout state, not something to share). - `extensions/*/local-config.yml` — per-machine extension config overrides. The file is routed through the same overwrite/skip/preserve policy as shared templates: `--force` refreshes it, user edits are preserved on re-init, and uninstall removes it via the manifest. Addresses github/spec-kit#2304. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 98faefd1-9fc8-48fc-bd25-d4f3ccbb2ab9 * docs: correct .specify/.gitignore uninstall claim The file is tracked in the shared-infra manifest (speckit.manifest.json), not the per-integration manifest that `specify integration uninstall` loads. Shared infrastructure is deliberately preserved on uninstall (see test_uninstall_preserves_shared_infra), so `.specify/.gitignore` is left in place rather than removed. Reword the code comment and core.md note to state the actual behavior; keep the true benefits (force-refresh and preserve-on-edit). Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 98faefd1-9fc8-48fc-bd25-d4f3ccbb2ab9 * revert: drop manual CHANGELOG.md edit CHANGELOG.md is auto-generated; do not hand-edit it. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 98faefd1-9fc8-48fc-bd25-d4f3ccbb2ab9 * test: add .specify/.gitignore to integration file inventories The complete-file-inventory tests assert an exact match of every file produced by `specify init`. Now that shared infra scaffolds a managed `.specify/.gitignore`, add it to the expected inventories so the exact-match assertions pass on both sh and ps script types. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous) --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 98faefd1-9fc8-48fc-bd25-d4f3ccbb2ab9
This commit is contained in:
co-authored by
Copilot App
parent
3dff6f1d50
commit
40037b1aca
@@ -16,6 +16,22 @@ from .integrations.manifest import IntegrationManifest
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Managed ``.specify/.gitignore``. Keeps machine-local Spec Kit state out of
|
||||
# version control while leaving shareable project files (specs, constitution,
|
||||
# templates, scripts, extension config) tracked. Patterns are relative to the
|
||||
# ``.specify/`` directory the file lives in.
|
||||
SPECIFY_GITIGNORE_CONTENT = """\
|
||||
# Machine-local Spec Kit state — not meant to be shared.
|
||||
# Managed by the Specify CLI; safe to edit (your changes are preserved on refresh).
|
||||
|
||||
# Local pointer to the current feature directory. Rewritten every time you
|
||||
# switch features, so it is per-checkout state rather than something to share.
|
||||
feature.json
|
||||
|
||||
# Per-machine extension config overrides.
|
||||
extensions/*/local-config.yml
|
||||
"""
|
||||
|
||||
# Matches a SHA-256 digest in its normalized form: exactly 64 hexadecimal
|
||||
# characters. Callers lowercase the declared value before matching (see
|
||||
# ``expected_hex = raw.lower()`` below), so an uppercase digest is accepted and
|
||||
@@ -608,6 +624,36 @@ def install_shared_infra(
|
||||
)
|
||||
planned_templates.append((dst, rel, content))
|
||||
|
||||
# Managed ``.specify/.gitignore`` — keeps machine-local state (the
|
||||
# ``feature.json`` pointer and per-machine ``local-config.yml`` overrides)
|
||||
# out of git while leaving everything else shareable. Routed through the
|
||||
# same overwrite/skip/preserve policy as templates so ``--force`` refreshes
|
||||
# it and user edits are preserved. Like every other shared-infra file it is
|
||||
# tracked in ``speckit.manifest.json`` (not the per-integration manifest) and
|
||||
# is therefore intentionally left in place by ``integration uninstall``.
|
||||
specify_dir = project_path / ".specify"
|
||||
if _ensure_or_bucket_dir(specify_dir):
|
||||
gitignore_dst = specify_dir / ".gitignore"
|
||||
gitignore_rel = gitignore_dst.relative_to(project_path).as_posix()
|
||||
seen_rels.add(gitignore_rel)
|
||||
if _safe_dest_or_bucket(gitignore_dst, gitignore_rel):
|
||||
write, bucket = _decide_overwrite(gitignore_rel, gitignore_dst)
|
||||
if write:
|
||||
planned_templates.append(
|
||||
(gitignore_dst, gitignore_rel, SPECIFY_GITIGNORE_CONTENT)
|
||||
)
|
||||
elif bucket == "preserved":
|
||||
preserved_user_files.append(gitignore_rel)
|
||||
else:
|
||||
skipped_files.append(gitignore_rel)
|
||||
if gitignore_dst.is_file() and gitignore_rel not in prior_hashes:
|
||||
try:
|
||||
manifest.record_existing(gitignore_rel, recovered=True)
|
||||
except (OSError, ValueError) as exc:
|
||||
console.print(
|
||||
f"[yellow]⚠[/yellow] could not record {gitignore_rel} in manifest: {exc}"
|
||||
)
|
||||
|
||||
for dst_path, rel, content, mode in planned_copies:
|
||||
if not _ensure_or_bucket_dir(dst_path.parent):
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user