mirror of
https://github.com/github/spec-kit.git
synced 2026-08-03 06:26:30 +08:00
* feat(extensions): port git extension scripts to Python Ports git-common, initialize-repo, auto-commit, and create-new-feature-branch to extensions/git/scripts/python/, mirroring the bash/PowerShell twins. Parity tests run each bash script and its Python twin in identical projects and compare output, exit codes, and resulting git state. Fixes #3282 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: match bash error message for whitespace-only descriptions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Handle unreadable git-config.yml and assert stderr parity An unreadable config file raised OSError with a full traceback from _parse_auto_commit_config. Treat it like a missing config: auto-commit stays disabled. Covered by a chmod-000 test (skipped on non-POSIX and as root). _assert_parity now also compares stderr so warning or usage-text regressions between the bash and Python twins fail the suite. All existing parity tests pass with the stricter assertion. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(extensions/git): pass script path to core.get_repo_root for cwd-outside-repo callers Without script_file, core.get_repo_root() falls back to Path.cwd() when SPECIFY_INIT_DIR is unset and no .specify root is found upward — the bash twin instead falls back to the script's install location (.specify/scripts/...). Pass script_file so both twins resolve the same repo_root; TypeError fallback keeps older cores working. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test: exercise SPECIFY_INIT_DIR from outside the project Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(extensions/git): handle UnicodeDecodeError and USER/USERNAME fallback - Catch (OSError, UnicodeDecodeError) when reading git-config.yml in create_new_feature_branch.py, initialize_repo.py, and auto_commit.py so invalid UTF-8 config falls back to defaults instead of crashing with a traceback. - Fall back to USERNAME (then "unknown") when USER is unset when deriving the branch author token, matching the PowerShell twin's Windows-friendly fallback chain. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(extensions/git): platform-aware persist hint and stronger SPECIFY_INIT_DIR test - Add a shared _persist_hint() helper in create_new_feature_branch.py and use it for both the JSON-mode stderr hint and the human-readable stdout hint, so there is a single place emitting the SPECIFY_FEATURE persistence guidance. On Windows (os.name == "nt") it prints PowerShell $env:VAR = "..." syntax; elsewhere it keeps the existing POSIX export VAR=... syntax (parity with the bash twin). - Rework test_specify_init_dir_resolves_target_project so SPECIFY_INIT_DIR is the only thing that can produce the observed result: the script now runs from a separate host_proj (no existing specs, so script/cwd-based discovery would yield 001) while SPECIFY_INIT_DIR points at a different target_proj that already has an existing spec (007-existing, so the override must yield 008). The old version pointed SPECIFY_INIT_DIR at the same project the script was installed in, so it passed even if the env var were ignored. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(extensions): tolerate missing Git executable Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(extensions): quote PowerShell persist hint Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(git): match bash persist hint escaping Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(git): ignore unterminated config record Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(git): handle Windows persist hint parity Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(init): install Python shared scripts Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(git): normalize Windows persistence hints Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
90 lines
2.7 KiB
Python
90 lines
2.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Git extension: initialize_repo.py
|
|
|
|
Initialize a Git repository with an initial commit.
|
|
Python port of ``initialize-repo.sh`` / ``initialize-repo.ps1``.
|
|
Customizable — replace this script to add .gitignore templates,
|
|
default branch config, git-flow, LFS, signing, etc.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
def _find_project_root(start: Path) -> Path | None:
|
|
current = start
|
|
while True:
|
|
if (current / ".specify").is_dir() or (current / ".git").exists():
|
|
return current
|
|
if current.parent == current:
|
|
return None
|
|
current = current.parent
|
|
|
|
|
|
def _read_commit_message(repo_root: Path) -> str:
|
|
"""Read init_commit_message from git-config.yml, mirroring the bash sed pipeline."""
|
|
default = "[Spec Kit] Initial commit"
|
|
config_file = repo_root / ".specify" / "extensions" / "git" / "git-config.yml"
|
|
if not config_file.is_file():
|
|
return default
|
|
try:
|
|
lines = config_file.read_text(encoding="utf-8").splitlines()
|
|
except (OSError, UnicodeDecodeError):
|
|
return default
|
|
for line in lines:
|
|
if line.startswith("init_commit_message:"):
|
|
value = re.sub(r"^init_commit_message:\s*", "", line)
|
|
value = re.sub(r"^[\"']", "", value)
|
|
value = re.sub(r"[\"']*$", "", value)
|
|
if value:
|
|
return value
|
|
return default
|
|
|
|
|
|
def main() -> int:
|
|
script_dir = Path(__file__).resolve().parent
|
|
repo_root = _find_project_root(script_dir) or Path.cwd()
|
|
|
|
commit_msg = _read_commit_message(repo_root)
|
|
|
|
if shutil.which("git") is None:
|
|
print(
|
|
"[specify] Warning: Git not found; skipped repository initialization",
|
|
file=sys.stderr,
|
|
)
|
|
return 0
|
|
|
|
probe = subprocess.run(
|
|
["git", "rev-parse", "--is-inside-work-tree"],
|
|
cwd=repo_root,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if probe.returncode == 0:
|
|
print("[specify] Git repository already initialized; skipping", file=sys.stderr)
|
|
return 0
|
|
|
|
steps = [
|
|
(["git", "init", "-q"], "git init"),
|
|
(["git", "add", "."], "git add"),
|
|
(["git", "commit", "--allow-empty", "-q", "-m", commit_msg], "git commit"),
|
|
]
|
|
for cmd, label in steps:
|
|
result = subprocess.run(cmd, cwd=repo_root, capture_output=True, text=True)
|
|
if result.returncode != 0:
|
|
output = (result.stdout + result.stderr).strip()
|
|
print(f"[specify] Error: {label} failed: {output}", file=sys.stderr)
|
|
return 1
|
|
|
|
print("[OK] Git repository initialized", file=sys.stderr)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|