Skip to content

Extract Git/Branching operations into extensions/git extension (opt-out, auto-enabled)#1941

Draft
Copilot wants to merge 10 commits intomainfrom
copilot/extract-git-branching-operations
Draft

Extract Git/Branching operations into extensions/git extension (opt-out, auto-enabled)#1941
Copilot wants to merge 10 commits intomainfrom
copilot/extract-git-branching-operations

Conversation

Copy link
Contributor

Copilot AI commented Mar 23, 2026

Git branching logic is coupled across ~815+ lines in 10+ files spanning 4 languages, blocking workflow customization (#841, #1382), causing recurring cross-platform bugs (#1066, #1791), and making spec iteration painful (#1191). This PR creates a self-contained extensions/git/ extension and wires it into the init lifecycle as an auto-enabled, opt-out extension for the pre-1.0.0 migration period.

Extension structure (extensions/git/)

  • extension.yml — Manifest with 3 commands (speckit.git.feature, speckit.git.validate, speckit.git.remote), 2 hooks (before_specify, after_implement), config defaults
  • commands/ — AI-agent-mediated command templates with explicit script paths, config resolution order, and graceful degradation when git is absent
  • scripts/bash/ and scripts/powershell/create-new-feature.{sh,ps1} copied from core with extension-aware common.sh/common.ps1 sourcing; git-common.{sh,ps1} extracts check_feature_branch() and has_git()/Test-HasGit
  • config-template.yml — Branch numbering strategy, template, auto-fetch defaults

Auto-install during specify init

  • _locate_bundled_git_extension() resolves from wheel core_pack/extensions/git/ or source checkout
  • _install_bundled_git_extension() calls ExtensionManager.install_from_directory() after init options are saved; no-ops if already installed, swallows errors (non-fatal during migration)
  • pyproject.toml force-include bundles extensions/git into the wheel

specify.md template changes

  • Checks .specify/extensions/.registry JSON for extensions.git.enabled before branch creation
  • Config precedence: extension git-config.ymlinit-options.json → default sequential
  • Script resolution: extension scripts → core {SCRIPT} fallback
  • When git extension is disabled, skips branching and proceeds with spec directory creation only

What stays in core (unchanged)

is_git_repo(), init_git_repo(), --branch-numbering flag, get_current_branch(), SPECIFY_FEATURE env var, find_feature_dir_by_prefix() — these remain until Phase 3 cleanup in a follow-up.

Disable/re-enable

specify extension disable git   # stops all branch operations, spec creation continues
specify extension enable git    # re-enables branching

🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

Copilot AI changed the title [WIP] Extract Git/branching operations into extensions/git extension Extract Git/Branching operations into extensions/git extension (opt-out, auto-enabled) Mar 23, 2026
Copilot AI requested a review from mnriem March 23, 2026 21:34
@mnriem mnriem requested a review from Copilot March 23, 2026 21:38
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR migrates git/branching workflow logic into a bundled extensions/git/ extension and wires it into specify init as an auto-enabled (opt-out) migration step, aiming to decouple branching behavior from core and reduce platform-specific breakages.

Changes:

  • Add a new extensions/git/ extension (manifest, commands, config template, and cross-platform scripts) for feature-branch creation/validation/remote detection.
  • Auto-install the bundled git extension during specify init and bundle it into the wheel via pyproject.toml force-include.
  • Update the /speckit.specify command template to consult extension enablement/config and prefer extension scripts with fallback behavior.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
templates/commands/specify.md Updates specify command flow to check git extension enablement/config and prefer extension scripts.
src/specify_cli/init.py Adds bundled-extension discovery + auto-install during specify init.
pyproject.toml Bundles extensions/git into the wheel’s core_pack.
extensions/git/extension.yml New extension manifest defining commands, hooks, and defaults.
extensions/git/config-template.yml New user-facing config template for git extension settings.
extensions/git/README.md Documentation for git extension usage, config, and behavior.
extensions/git/commands/speckit.git.feature.md Command template for creating a feature branch via extension scripts.
extensions/git/commands/speckit.git.validate.md Command template for validating branch naming conventions.
extensions/git/commands/speckit.git.remote.md Command template for detecting git remote URL safely.
extensions/git/scripts/bash/create-new-feature.sh Bash feature-creation script adapted for extension install locations.
extensions/git/scripts/bash/git-common.sh Bash git-only helper functions extracted for reuse.
extensions/git/scripts/powershell/create-new-feature.ps1 PowerShell feature-creation script adapted for extension install locations.
extensions/git/scripts/powershell/git-common.ps1 PowerShell git-only helper functions extracted for reuse.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@mnriem
Copy link
Collaborator

mnriem commented Mar 23, 2026

@copilot apply changes based on the comments in this thread

- Fix bash common.sh sourcing: check .specify/scripts/bash/ first, then
  scripts/bash/, with explicit error if neither found
- Fix PowerShell common.ps1 sourcing: use $fallbackRoot for reliable
  path resolution, with explicit error if not found
- Remove undocumented branch_template and auto_fetch from extension.yml
  defaults and config-template.yml (scripts don't use them yet)
- Remove unused ExtensionError import in _install_bundled_git_extension
- Remove undocumented SPECKIT_GIT_BRANCH_NUMBERING env var from README
- Fix specify.md: skip step 2 when before_specify hook already executed
- Fix specify.md: add explicit FEATURE_DIR/SPEC_FILE in disabled-git path
- Fix specify.md: add PowerShell path to script resolution
- Add tests for git extension auto-install during specify init

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
Agent-Logs-Url: https://github.com/github/spec-kit/sessions/008835a0-7778-40bb-bdb2-4182b22be315
@mnriem mnriem requested a review from Copilot March 23, 2026 21:58
Copy link
Contributor Author

Copilot AI commented Mar 23, 2026

@copilot apply changes based on the comments in this thread

All 9 review comments addressed in 5e49ec6 and 17810c8:

  1. Bash common.sh sourcing — checks .specify/scripts/bash/scripts/bash/git-common.sh with hard error if none found
  2. PowerShell common.ps1 sourcing — uses $fallbackRoot for reliable resolution with same priority order and error
  3. Removed branch_template/auto_fetch from extension.yml defaults and config-template.yml (not implemented in scripts)
  4. Removed unused ExtensionError import
  5. Added 2 tests for auto-install path (init flow + already-installed no-op)
  6. Fixed double branch creation — step 2 now skips when before_specify hook already executed
  7. Added explicit FEATURE_DIR/SPEC_FILE creation in disabled-git path
  8. Added PowerShell path to script resolution in specify.md
  9. Removed undocumented SPECKIT_GIT_BRANCH_NUMBERING env var from README

All 818 tests pass, CodeQL clean.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • https://api.github.com/repos/github/spec-kit/releases/latest
    • Triggering command: `/usr/bin/python3 python3 -c
      import tempfile, shutil
      from pathlib import Path
      from typer.testing import CliRunner
      from specify_cli import app

with tempfile.TemporaryDirectory() as tmpdir:
project = Path(tmpdir) / 'test-project'
REDACTED = CliRunner()
result = REDACTED.invok` (http block)

  • Triggering command: `/usr/bin/python3 python3 -c
    import tempfile
    from pathlib import Path
    from typer.testing import CliRunner
    from specify_cli import app

with tempfile.TemporaryDirectory() as tmpdir:
project = Path(tmpdir) / 'test-project'
REDACTED = CliRunner()
result = REDACTED.invoke(app, [` (http block)

If you need me to access, download, or install something from one of these locations, you can either:

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 7 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@mnriem
Copy link
Collaborator

mnriem commented Mar 23, 2026

@copilot apply changes based on the comments in this thread

Copilot AI and others added 2 commits March 23, 2026 22:13
1. Log warning on auto-install failure instead of silent catch
2. Fix README install command (--from → --dev)
3. Bash: fail fast if resolve_template/json_escape unavailable after git-common.sh fallback
4. PowerShell: fail fast if Resolve-Template unavailable after git-common.ps1 fallback
5. Fix PowerShell $env:FEATURE_DIR → $FEATURE_DIR in specify.md
6. Fix docstring to reflect already-installed return value

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
Agent-Logs-Url: https://github.com/github/spec-kit/sessions/6bcb7cb8-c7da-49d6-8206-1187766f92e1
Copy link
Contributor Author

Copilot AI commented Mar 23, 2026

@copilot apply changes based on the comments in this thread

All 7 review comments addressed in 4f1b63a and 363b7dc:

  1. Auto-install failure loggingexcept Exception now logs a [dim yellow]Warning: with details via console.print instead of silently returning False
  2. README install command — fixed --from extensions/git/extensions/git --dev
  3. README table syntax — already correct (single pipes), no change needed
  4. Bash fail-fast — added post-source check for resolve_template/json_escape; hard-fails with error listing actual tried paths
  5. PowerShell fail-fast — added post-source check for Resolve-Template; throws with paths tried
  6. PowerShell $env: prefix — fixed $env:FEATURE_DIR/$env:SPEC_FILE$FEATURE_DIR/$SPEC_FILE
  7. Docstring — updated to "Returns True if the extension was installed or already present"

All 818 tests pass, CodeQL clean.

@mnriem mnriem requested a review from Copilot March 23, 2026 22:22
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@mnriem
Copy link
Collaborator

mnriem commented Mar 23, 2026

@copilot apply changes based on the comments in this thread

1. Use `git checkout -q -b` with stdout redirect to prevent status
   messages from contaminating JSON output in create-new-feature.sh
2. Clarify specify.md registry check: only skip branching when
   extensions.git.enabled is present and explicitly false; proceed
   normally in all other cases (missing registry, missing entry, etc.)

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
Agent-Logs-Url: https://github.com/github/spec-kit/sessions/67a6414f-e8ce-4455-95e5-299c72d86624
Copy link
Contributor Author

Copilot AI commented Mar 23, 2026

@copilot apply changes based on the comments in this thread

Both comments addressed in a5466f0:

  1. Quiet git checkoutgit checkout -q -b "$BRANCH_NAME" >/dev/null 2>&1 suppresses the "Switched to a new branch…" status line that was contaminating JSON output
  2. Registry check clarification — specify.md now explicitly states: only skip branching when extensions.git.enabled is present and explicitly false; proceed normally in all other cases (missing registry, missing entry, absent enabled field)

All 818 tests pass.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1202 to +1204
# Skip if already installed (e.g. via preset)
if manager.registry.is_installed("git"):
return True
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The “already installed” check only consults the registry (manager.registry.is_installed("git")). If the registry is stale/corrupted (entry exists but .specify/extensions/git/ was deleted or incomplete), this will incorrectly skip re-install and leave init in a broken state. Consider also verifying the extension directory/manifest exists (e.g., check for .specify/extensions/git/extension.yml) and reinstall if it’s missing.

Suggested change
# Skip if already installed (e.g. via preset)
if manager.registry.is_installed("git"):
return True
# Skip if already installed (e.g. via preset), but only if the
# on-disk extension manifest still exists. This guards against
# stale/corrupted registry entries.
if manager.registry.is_installed("git"):
ext_manifest = project_path / ".specify" / "extensions" / "git" / "extension.yml"
if ext_manifest.is_file():
return True

Copilot uses AI. Check for mistakes.
Comment on lines +122 to +140
def test_git_extension_noop_when_already_installed(self, tmp_path: Path):
"""_install_bundled_git_extension should no-op if git is already installed."""
from specify_cli import _install_bundled_git_extension
from specify_cli.extensions import ExtensionManager

project_dir = tmp_path / "proj"
(project_dir / ".specify").mkdir(parents=True)

# First install
result1 = _install_bundled_git_extension(project_dir)
assert result1 is True

# Second install should also succeed (no-op)
result2 = _install_bundled_git_extension(project_dir)
assert result2 is True

# Only one entry in registry
manager = ExtensionManager(project_dir)
assert manager.registry.is_installed("git")
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If _install_bundled_git_extension() is updated to repair stale registry entries (registry says installed but .specify/extensions/git/ is missing), it would be good to add a regression test for that scenario here to prevent future breakage (e.g., pre-create .registry with a git entry, delete the directory, then assert init reinstalls it).

Copilot uses AI. Check for mistakes.
@mnriem
Copy link
Collaborator

mnriem commented Mar 23, 2026

@copilot apply changes based on the comments in this thread

Copy link
Contributor Author

Copilot AI commented Mar 23, 2026

@copilot apply changes based on the comments in this thread

Both comments addressed in 7396683:

  1. Stale registry detection_install_bundled_git_extension() now verifies .specify/extensions/git/extension.yml exists on disk when the registry says "installed"; if the manifest is missing, removes the stale registry entry and proceeds with reinstall
  2. Regression test — added test_git_extension_reinstalls_when_directory_missing that creates an install, deletes the extension directory (keeping the registry), and asserts reinstall succeeds

All 819 tests pass.

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.

Extract Git/Branching Operations into extensions/git Extension (Opt-Out, Auto-Enabled)

3 participants