Skip to content

Commit 485c8d3

Browse files
mnriemCopilot
andcommitted
refactor: remove all git operations from core
- Remove is_git_repo() and init_git_repo() dead code from _utils.py - Remove --branch-numbering from init command - Remove git from 'specify check' (now extension-only) - Update docs: git is optional prerequisite, check command description - Fix tests to reflect no-git-in-core reality (fallback to main) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 87665f5 commit 485c8d3

13 files changed

Lines changed: 67 additions & 193 deletions

docs/installation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
- AI coding agent: [Claude Code](https://www.anthropic.com/claude-code), [GitHub Copilot](https://code.visualstudio.com/), [Codebuddy CLI](https://www.codebuddy.ai/cli), [Gemini CLI](https://github.com/google-gemini/gemini-cli), or [Pi Coding Agent](https://pi.dev)
77
- [uv](https://docs.astral.sh/uv/) for package management (recommended) or [pipx](https://pipx.pypa.io/) for persistent installation
88
- [Python 3.11+](https://www.python.org/downloads/)
9-
- [Git](https://git-scm.com/downloads)
9+
- [Git](https://git-scm.com/downloads) _(optional — required only when the git extension is enabled)_
1010

1111
## Installation
1212

docs/reference/core.md

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,11 @@ specify init [<project_name>]
1717
| `--force` | Force merge/overwrite when initializing in an existing directory |
1818
| `--ignore-agent-tools` | Skip checks for AI coding agent CLI tools |
1919
| `--preset <id>` | Install a preset during initialization |
20-
| `--branch-numbering` | Branch numbering strategy: `sequential` (default) or `timestamp` |
2120

2221
Creates a new Spec Kit project with the necessary directory structure, templates, scripts, and AI coding agent integration files.
2322

2423
> [!NOTE]
25-
> The git extension is opt-in during `specify init`. To add it after init, run `specify extension add git`.
24+
> Git repository initialization and branching are managed by the **git extension**, which is not installed by default. Run `specify extension add git` after init to enable git workflows.
2625
2726
Use `<project_name>` to create a new directory, or `--here` (or `.`) to initialize in the current directory. If the directory already has files, use `--force` to merge without confirmation.
2827

@@ -45,9 +44,6 @@ specify init my-project --integration copilot --script ps
4544

4645
# Install a preset during initialization
4746
specify init my-project --integration copilot --preset compliance
48-
49-
# Use timestamp-based branch numbering (useful for distributed teams)
50-
specify init my-project --integration copilot --branch-numbering timestamp
5147
```
5248

5349
### Environment Variables
@@ -62,7 +58,7 @@ specify init my-project --integration copilot --branch-numbering timestamp
6258
specify check
6359
```
6460

65-
Checks that required tools are available on your system: `git` and any CLI-based AI coding agents. IDE-based agents are skipped since they don't require a CLI tool.
61+
Checks that CLI-based AI coding agents are available on your system. IDE-based agents are skipped since they don't require a CLI tool.
6662

6763
This command stays offline. If a command behaves like an older Spec Kit version or an expected CLI feature is missing, run `specify self check` to check whether your local CLI is behind the latest release.
6864

src/specify_cli/__init__.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,6 @@
6969
_display_project_path,
7070
check_tool as check_tool,
7171
handle_vscode_settings as handle_vscode_settings,
72-
init_git_repo as init_git_repo,
73-
is_git_repo as is_git_repo,
7472
merge_json_files as merge_json_files,
7573
run_command as run_command,
7674
)
@@ -453,9 +451,6 @@ def check():
453451

454452
tracker = StepTracker("Check Available Tools")
455453

456-
tracker.add("git", "Git version control")
457-
git_ok = check_tool("git", tracker=tracker)
458-
459454
agent_results = {}
460455
for agent_key, agent_config in AGENT_CONFIG.items():
461456
if agent_key == "generic":
@@ -483,9 +478,6 @@ def check():
483478

484479
console.print("\n[bold green]Specify CLI is ready to use![/bold green]")
485480

486-
if not git_ok:
487-
console.print("[dim]Tip: Install git for repository management[/dim]")
488-
489481
if not any(agent_results.values()):
490482
console.print("[dim]Tip: Install a coding agent for the best experience[/dim]")
491483

src/specify_cli/_utils.py

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -77,51 +77,6 @@ def check_tool(tool: str, tracker=None) -> bool:
7777
return found
7878

7979

80-
def is_git_repo(path: Path | None = None) -> bool:
81-
"""Check if the specified path is inside a git repository."""
82-
if path is None:
83-
path = Path.cwd()
84-
85-
if not path.is_dir():
86-
return False
87-
88-
try:
89-
subprocess.run(
90-
["git", "rev-parse", "--is-inside-work-tree"],
91-
check=True,
92-
capture_output=True,
93-
cwd=path,
94-
)
95-
return True
96-
except (subprocess.CalledProcessError, FileNotFoundError):
97-
return False
98-
99-
100-
def init_git_repo(project_path: Path, quiet: bool = False) -> tuple[bool, str | None]:
101-
"""Initialize a git repository in the specified path."""
102-
try:
103-
original_cwd = Path.cwd()
104-
os.chdir(project_path)
105-
if not quiet:
106-
console.print("[cyan]Initializing git repository...[/cyan]")
107-
subprocess.run(["git", "init"], check=True, capture_output=True, text=True)
108-
subprocess.run(["git", "add", "."], check=True, capture_output=True, text=True)
109-
subprocess.run(["git", "commit", "-m", "Initial commit from Specify template"], check=True, capture_output=True, text=True)
110-
if not quiet:
111-
console.print("[green]✓[/green] Git repository initialized")
112-
return True, None
113-
except subprocess.CalledProcessError as e:
114-
error_msg = f"Command: {' '.join(e.cmd)}\nExit code: {e.returncode}"
115-
if e.stderr:
116-
error_msg += f"\nError: {e.stderr.strip()}"
117-
elif e.stdout:
118-
error_msg += f"\nOutput: {e.stdout.strip()}"
119-
if not quiet:
120-
console.print(f"[red]Error initializing git repository:[/red] {e}")
121-
return False, error_msg
122-
finally:
123-
os.chdir(original_cwd)
124-
12580

12681
def handle_vscode_settings(sub_item, dest_file, rel_path, verbose=False, tracker=None) -> None:
12782
"""Handle merging or copying of .vscode/settings.json files.

src/specify_cli/commands/init.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@ def init(
7878
github_token: str = typer.Option(None, "--github-token", help="Deprecated (no-op). Previously: GitHub token for API requests.", hidden=True),
7979
offline: bool = typer.Option(False, "--offline", help="Deprecated (no-op). All scaffolding now uses bundled assets.", hidden=True),
8080
preset: str = typer.Option(None, "--preset", help="Install a preset during initialization (by preset ID)"),
81-
branch_numbering: str = typer.Option(None, "--branch-numbering", help="Branch numbering strategy: 'sequential' (001, 002, …, 1000, … — expands past 999 automatically) or 'timestamp' (YYYYMMDD-HHMMSS)"),
8281
integration: str = typer.Option(None, "--integration", help="AI coding agent integration to use (e.g. --integration copilot). See 'specify check' for available integrations."),
8382
integration_options: str = typer.Option(None, "--integration-options", help='Options for the integration (e.g. --integration-options="--commands-dir .myagent/cmds")'),
8483
):
@@ -151,10 +150,7 @@ def init(
151150
console.print("[red]Error:[/red] Must specify either a project name, use '.' for current directory, or use --here flag")
152151
raise typer.Exit(1)
153152

154-
BRANCH_NUMBERING_CHOICES = {"sequential", "timestamp"}
155-
if branch_numbering and branch_numbering not in BRANCH_NUMBERING_CHOICES:
156-
console.print(f"[red]Error:[/red] Invalid --branch-numbering value '{branch_numbering}'. Choose from: {', '.join(sorted(BRANCH_NUMBERING_CHOICES))}")
157-
raise typer.Exit(1)
153+
158154

159155
dir_existed_before = False
160156
if here:
@@ -383,7 +379,6 @@ def init(
383379
init_opts = {
384380
"ai": selected_ai,
385381
"integration": resolved_integration.key,
386-
"branch_numbering": branch_numbering or "sequential",
387382
"here": here,
388383
"script": selected_script,
389384
"speckit_version": get_speckit_version(),

tests/integrations/test_integration_base_toml.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,7 @@ def test_init_options_includes_context_file(self, tmp_path):
467467
os.chdir(project)
468468
result = CliRunner().invoke(app, [
469469
"init", "--here", "--integration", self.KEY, "--script", "sh",
470+
"--ignore-agent-tools",
470471
], catch_exceptions=False)
471472
finally:
472473
os.chdir(old_cwd)

tests/integrations/test_integration_base_yaml.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,7 @@ def test_init_options_includes_context_file(self, tmp_path):
346346
os.chdir(project)
347347
result = CliRunner().invoke(app, [
348348
"init", "--here", "--integration", self.KEY, "--script", "sh",
349+
"--ignore-agent-tools",
349350
], catch_exceptions=False)
350351
finally:
351352
os.chdir(old_cwd)

tests/integrations/test_integration_subcommand.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -960,7 +960,7 @@ def test_switch_does_not_register_disabled_extensions(self, tmp_path):
960960
def test_switch_refreshes_managed_shared_script_refs(self, tmp_path):
961961
"""Switching refreshes managed shared scripts to the target command style."""
962962
project = _init_project(tmp_path, "claude")
963-
shared_script = project / ".specify" / "scripts" / "bash" / "common.sh"
963+
shared_script = project / ".specify" / "scripts" / "bash" / "setup-tasks.sh"
964964
assert shared_script.exists()
965965
shared_content = shared_script.read_text(encoding="utf-8")
966966
assert "/speckit-plan" in shared_content
@@ -986,7 +986,7 @@ def test_switch_refreshes_stale_managed_shared_infra(self, tmp_path):
986986
import hashlib
987987

988988
project = _init_project(tmp_path, "claude")
989-
shared_script = project / ".specify" / "scripts" / "bash" / "common.sh"
989+
shared_script = project / ".specify" / "scripts" / "bash" / "setup-tasks.sh"
990990
assert "/speckit-plan" in shared_script.read_text(encoding="utf-8")
991991

992992
# Simulate a stale vendored script: write truncated content as bytes
@@ -998,7 +998,7 @@ def test_switch_refreshes_stale_managed_shared_infra(self, tmp_path):
998998

999999
manifest_path = project / ".specify" / "integrations" / "speckit.manifest.json"
10001000
manifest_data = json.loads(manifest_path.read_text(encoding="utf-8"))
1001-
manifest_data["files"][".specify/scripts/bash/common.sh"] = (
1001+
manifest_data["files"][".specify/scripts/bash/setup-tasks.sh"] = (
10021002
hashlib.sha256(stale_bytes).hexdigest()
10031003
)
10041004
manifest_path.write_text(json.dumps(manifest_data), encoding="utf-8")
@@ -1047,7 +1047,7 @@ def test_switch_preserves_user_customized_shared_infra(self, tmp_path):
10471047
def test_switch_refresh_shared_infra_overwrites_customizations(self, tmp_path):
10481048
"""--refresh-shared-infra explicitly overwrites user customizations on switch."""
10491049
project = _init_project(tmp_path, "claude")
1050-
shared_script = project / ".specify" / "scripts" / "bash" / "common.sh"
1050+
shared_script = project / ".specify" / "scripts" / "bash" / "setup-tasks.sh"
10511051
assert "/speckit-plan" in shared_script.read_text(encoding="utf-8")
10521052
rendered_bytes = shared_script.read_bytes()
10531053

tests/test_branch_numbering.py

Lines changed: 12 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,24 @@
11
"""
2-
Unit tests for branch numbering options (sequential vs timestamp).
2+
Unit tests verifying --branch-numbering removal (v0.10.0).
33
4-
Tests cover:
5-
- Persisting branch_numbering in init-options.json
6-
- Default value when branch_numbering is None
7-
- Validation of branch_numbering values
4+
Branch numbering is now managed entirely by the git extension's config.
5+
The --branch-numbering flag was removed from `specify init`.
86
"""
97

10-
import json
118
from pathlib import Path
129

13-
from specify_cli import save_init_options
1410

11+
class TestBranchNumberingFlagRemoved:
12+
"""--branch-numbering flag was removed in v0.10.0."""
1513

16-
class TestSaveBranchNumbering:
17-
"""Tests for save_init_options with branch_numbering."""
18-
19-
def test_save_branch_numbering_timestamp(self, tmp_path: Path):
20-
opts = {"branch_numbering": "timestamp", "ai": "claude"}
21-
save_init_options(tmp_path, opts)
22-
23-
saved = json.loads((tmp_path / ".specify/init-options.json").read_text())
24-
assert saved["branch_numbering"] == "timestamp"
25-
26-
def test_save_branch_numbering_sequential(self, tmp_path: Path):
27-
opts = {"branch_numbering": "sequential", "ai": "claude"}
28-
save_init_options(tmp_path, opts)
29-
30-
saved = json.loads((tmp_path / ".specify/init-options.json").read_text())
31-
assert saved["branch_numbering"] == "sequential"
32-
33-
def test_branch_numbering_defaults_to_sequential(self, tmp_path: Path):
34-
from typer.testing import CliRunner
35-
from specify_cli import app
36-
37-
project_dir = tmp_path / "proj"
38-
runner = CliRunner()
39-
result = runner.invoke(app, ["init", str(project_dir), "--integration", "claude", "--ignore-agent-tools", "--script", "sh"])
40-
assert result.exit_code == 0
41-
42-
saved = json.loads((project_dir / ".specify/init-options.json").read_text())
43-
assert saved["branch_numbering"] == "sequential"
44-
45-
46-
class TestBranchNumberingValidation:
47-
"""Tests for branch_numbering CLI validation via CliRunner."""
48-
49-
def test_invalid_branch_numbering_rejected(self, tmp_path: Path):
50-
from typer.testing import CliRunner
51-
from specify_cli import app
52-
53-
runner = CliRunner()
54-
result = runner.invoke(app, ["init", str(tmp_path / "proj"), "--integration", "claude", "--branch-numbering", "foobar", "--ignore-agent-tools"])
55-
assert result.exit_code == 1
56-
assert "Invalid --branch-numbering" in result.output
57-
58-
def test_valid_branch_numbering_sequential(self, tmp_path: Path):
59-
from typer.testing import CliRunner
60-
from specify_cli import app
61-
62-
runner = CliRunner()
63-
result = runner.invoke(app, ["init", str(tmp_path / "proj"), "--integration", "claude", "--branch-numbering", "sequential", "--ignore-agent-tools", "--script", "sh"])
64-
assert result.exit_code == 0
65-
assert "Invalid --branch-numbering" not in (result.output or "")
66-
67-
def test_valid_branch_numbering_timestamp(self, tmp_path: Path):
14+
def test_branch_numbering_flag_is_rejected(self, tmp_path: Path):
6815
from typer.testing import CliRunner
6916
from specify_cli import app
7017

7118
runner = CliRunner()
72-
result = runner.invoke(app, ["init", str(tmp_path / "proj"), "--integration", "claude", "--branch-numbering", "timestamp", "--ignore-agent-tools", "--script", "sh"])
73-
assert result.exit_code == 0
74-
assert "Invalid --branch-numbering" not in (result.output or "")
19+
result = runner.invoke(app, [
20+
"init", str(tmp_path / "proj"), "--integration", "claude",
21+
"--branch-numbering", "sequential", "--ignore-agent-tools",
22+
])
23+
assert result.exit_code != 0, "--branch-numbering should be rejected"
24+
assert "No such option" in result.output or "no such option" in result.output.lower()

tests/test_check_prerequisites_paths_only.py

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -88,20 +88,17 @@ def test_paths_only_succeeds_on_non_spec_branch(prereq_repo: Path) -> None:
8888

8989
@requires_bash
9090
def test_paths_only_succeeds_on_spec_branch(prereq_repo: Path) -> None:
91-
"""--paths-only must also work on a properly named spec branch."""
92-
subprocess.run(
93-
["git", "checkout", "-q", "-b", "001-my-feature"],
94-
cwd=prereq_repo,
95-
check=True,
96-
)
91+
"""--paths-only must also work when SPECIFY_FEATURE is set to a feature name."""
9792
script = prereq_repo / ".specify" / "scripts" / "bash" / "check-prerequisites.sh"
93+
env = _clean_env()
94+
env["SPECIFY_FEATURE"] = "001-my-feature"
9895
result = subprocess.run(
9996
["bash", str(script), "--json", "--paths-only"],
10097
cwd=prereq_repo,
10198
capture_output=True,
10299
text=True,
103100
check=False,
104-
env=_clean_env(),
101+
env=env,
105102
)
106103
assert result.returncode == 0, result.stderr
107104
data = json.loads(result.stdout)
@@ -128,7 +125,7 @@ def test_paths_only_text_mode_on_non_spec_branch(prereq_repo: Path) -> None:
128125

129126
@requires_bash
130127
def test_normal_mode_still_validates_branch(prereq_repo: Path) -> None:
131-
"""Without --paths-only, branch validation must still fail on main."""
128+
"""Without --paths-only, feature directory validation must still fail on main."""
132129
script = prereq_repo / ".specify" / "scripts" / "bash" / "check-prerequisites.sh"
133130
result = subprocess.run(
134131
["bash", str(script), "--json"],
@@ -139,7 +136,7 @@ def test_normal_mode_still_validates_branch(prereq_repo: Path) -> None:
139136
env=_clean_env(),
140137
)
141138
assert result.returncode != 0
142-
assert "Not on a feature branch" in result.stderr
139+
assert "Feature directory not found" in result.stderr
143140

144141

145142
# ── PowerShell tests ──────────────────────────────────────────────────────
@@ -190,7 +187,7 @@ def test_ps_paths_only_succeeds_on_spec_branch(prereq_repo: Path) -> None:
190187

191188
@pytest.mark.skipif(not (HAS_PWSH or _POWERSHELL), reason="no PowerShell available")
192189
def test_ps_normal_mode_still_validates_branch(prereq_repo: Path) -> None:
193-
"""Without -PathsOnly, branch validation must still fail on main."""
190+
"""Without -PathsOnly, feature directory validation must still fail on main."""
194191
script = prereq_repo / ".specify" / "scripts" / "powershell" / "check-prerequisites.ps1"
195192
exe = "pwsh" if HAS_PWSH else _POWERSHELL
196193
result = subprocess.run(
@@ -202,4 +199,5 @@ def test_ps_normal_mode_still_validates_branch(prereq_repo: Path) -> None:
202199
env=_clean_env(),
203200
)
204201
assert result.returncode != 0
205-
assert "Not on a feature branch" in result.stderr
202+
combined = result.stdout + result.stderr
203+
assert "Feature directory not found" in combined

0 commit comments

Comments
 (0)