Skip to content

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

@mnriem

Description

@mnriem

Summary

Extract all Git-related operations (repository detection, branch creation, branch numbering, branch validation, remote fetching, SPECIFY_FEATURE env var management) from core into a self-contained Spec Kit extension at extensions/git/. The extension auto-enables by default during the migration period (opt-out) to preserve backward compatibility. Before the 1.0.0 release, the extension will transition to opt-in — users will explicitly install it via specify extension add git or a preset that includes it.

Scope note: The --no-git flag on specify init is out of scope for this issue and will be handled separately.

Problem

Git branching logic is deeply interwoven across ~815+ lines in 10+ files spanning 4 languages (Python, Bash, PowerShell, Markdown). Analysis of upstream issues reveals this coupling causes problems across five distinct categories — not just maintenance burden, but workflow lock-in, blocked extensibility, spec iteration friction, and contributor barriers. See the original problem analysis in the collapsed section below for the full breakdown.

Delivered Solution

The extraction was implemented incrementally via a hook-based architecture rather than the originally planned "rip and replace" approach. This preserves backward compatibility while cleanly separating git operations from core.

Stage 1 — PR #1941 (merged)

Created extensions/git/ with the extension manifest, commands, hooks, and scripts:

extensions/git/
    extension.yml
    git-config.yml                    # Branch numbering config
    commands/
        speckit.git.feature.md        # Branch creation
        speckit.git.validate.md       # Branch validation
        speckit.git.commit.md         # Auto-commit
        speckit.git.remote.md         # Remote URL detection
        speckit.git.initialize.md     # Git repo initialization
    scripts/
        bash/
            create-new-feature.sh     # Moved from scripts/bash/
            git-common.sh             # Git-specific subset of common.sh
            initialize-repo.sh
        powershell/
            create-new-feature.ps1    # Moved from scripts/powershell/
            git-common.ps1            # Git-specific subset of common.ps1
            initialize-repo.ps1

Key deliverables:

  • Extension manifest with commands, hooks, and config.defaults
  • speckit.git.feature creates branches with sequential or timestamp numbering
  • speckit.git.validate validates feature branch naming conventions
  • speckit.git.remote detects Git remote URL for GitHub integration
  • speckit.git.commit auto-commits at hook points (optional)
  • speckit.git.initialize initializes Git repos
  • before_specify, before_constitution, and all other lifecycle hooks registered
  • Auto-install during specify init (unless --no-git)
  • Graceful degradation when git unavailable
  • Cross-platform bash and PowerShell scripts
  • Extension commands include explicit script paths (no {SCRIPT} placeholder reliance)
  • Extension reads branch_numbering from its own config (.specify/extensions/git/git-config.yml)

Stage 2 — PR #2117 (open)

Enhanced the extension and CLI with:

  • GIT_BRANCH_NAME env var override for exact branch naming (parallels SPECIFY_FEATURE_DIRECTORY)
  • --force flag for specify init <dir> into existing directories
  • Test coverage: TestGitExtensionAutoInstall (3 tests), TestFeatureDirectoryResolution (4 tests)
  • All 1,195 tests passing, 0 failures

What Moved Out of Core

From Core To Extension Status
Branch creation logic speckit.git.feature command + scripts ✅ Done
Branch validation speckit.git.validate command ✅ Done
Remote URL detection speckit.git.remote command ✅ Done
create-new-feature.sh extensions/git/scripts/bash/ ✅ Done
create-new-feature.ps1 extensions/git/scripts/powershell/ ✅ Done
check_feature_branch() Duplicated to git-common.sh ✅ Done (core copy retained for backward compat)
--branch-numbering config git-config.yml in extension ✅ Done (CLI flag retained for backward compat)
Branch creation section of specify.md Hook-based via before_specify ✅ Done

What Stays in Core (by design)

Component Reason
--no-git flag Out of scope; handled separately
get_current_branch() in common.sh/.ps1 Called by all downstream commands; not git-specific
SPECIFY_FEATURE env var check Core feature detection, not git-specific
specs/ directory scanning fallback Non-git feature detection
find_feature_dir_by_prefix() Spec directory resolution (works without git)
check_feature_branch() in core common.sh Called by setup-plan.sh and check-prerequisites.sh; removing would break scripts when git extension is disabled
--branch-numbering CLI flag Backward compatibility; writes to init-options.json which the extension reads as fallback

Extension Enable/Disable Behavior

The specify extension disable git and specify extension enable git commands already exist and work correctly. When disabled:

  • before_specify and other hooks don't fire (hook executor checks enabled status)
  • Spec directories are still created (core behavior, independent of git)
  • Identical to HAS_GIT=false behavior in scripts

Remaining Work (pre-1.0.0)

  • Deprecate --branch-numbering CLI flag on init() with a warning pointing to .specify/extensions/git/git-config.yml (breaking change deferred to 1.0.0)
  • Add explicit test for specify extension disable git → verify no branch operations occur
  • Transition extension from auto-install (opt-out) to opt-in at 1.0.0 (remove auto-install code from init())

Acceptance Criteria

  • extensions/git/extension.yml manifest with commands, hooks, and config.defaults
  • speckit.git.feature command creates branches with sequential or timestamp numbering
  • speckit.git.validate command validates feature branch naming conventions
  • speckit.git.remote command detects Git remote URL for GitHub integration
  • before_specify hook registered; specify.md template conditionally runs branching via hooks
  • Extension commands include explicit script paths (no reliance on {SCRIPT} placeholder)
  • Extension reads branch_numbering from its own config (.specify/extensions/git/git-config.yml)
  • Extension degrades gracefully when git is not installed (warnings, no errors)
  • specify extension disable git stops all branch operations without breaking spec creation
  • specify extension enable git re-enables branch operations
  • Auto-install logic added to specify init for bundled git extension
  • All existing branching tests pass when run against the extension (1,195/1,195)
  • Cross-platform: Bash and PowerShell scripts both in extension
  • get_current_branch() remains intact in core common.sh/.ps1
  • SPECIFY_FEATURE environment variable continues to work (stays in core)
  • specify.md template conditionally includes branching based on hook system
  • No regression in --no-git behavior
  • GIT_BRANCH_NAME env var override for exact branch naming
  • --branch-numbering CLI flag deprecated (pre-1.0.0)
  • Auto-install transitions to opt-in (at 1.0.0)

Related Issues & PRs

# Title Status Relevance Signal
#841 Support disabling automatic branch creation via parameter open Resolved: specify extension disable git 25 👍
#1382 Custom namespacing and branch naming conventions open Unblocked: GIT_BRANCH_NAME env var + extension config 18 👍
#1066 Branch numbering repeats 001- prefix open Fix scope narrowed to extension 13 👍
#1921 Make branch creation configurable (branchStrategy) open Resolved by extension config 1 👍
#1901 Allow agent-namespaced branches (copilot/**) open Enabled via GIT_BRANCH_NAME
#1165 Different commands produce/expect different branch name syntax open Validation now in extension
#1191 Can't easily update or refine existing specs open Hook-based branching is optional 107 👍
#1941 PR: Git extension stage 1 merged Phase 1+2 delivery
#2117 PR: Git extension stage 2 open GIT_BRANCH_NAME, --force, tests

Original Problem Analysis

The scope of the coupling

File Language Git Lines Operations
src/specify_cli/__init__.py Python ~80 is_git_repo() (L634), init_git_repo() (L654), --branch-numbering validation (L1826), git init orchestration (L2100-2135), branch_numbering in init-options (L2146)
scripts/bash/create-new-feature.sh Bash ~130 git branch -a, git fetch --all --prune, git checkout -b, git branch --list, branch number extraction, name generation, 244-byte validation
scripts/bash/common.sh Bash ~95 get_repo_root(), get_current_branch(), has_git(), check_feature_branch(), find_feature_dir_by_prefix(), SPECIFY_FEATURE env var handling
scripts/powershell/create-new-feature.ps1 PowerShell ~120 PowerShell equivalents of all bash git operations
scripts/powershell/common.ps1 PowerShell ~75 PowerShell equivalents of all common.sh git operations
templates/commands/specify.md Markdown ~35 Branch creation instructions, init-options.json reading for branch_numbering, script invocation with --timestamp/--short-name
templates/commands/implement.md Markdown 1 git rev-parse --git-dir for .gitignore creation
templates/commands/taskstoissues.md Markdown 5 git config --get remote.origin.url for GitHub issue creation
tests/test_branch_numbering.py Python 69 6 tests for branch numbering persistence and validation
tests/test_timestamp_branches.py Python 280 15 tests covering timestamp/sequential branching end-to-end

Impact of this coupling:

  1. Workflow lock-in — users who don't want branching can't adopt spec-kit (Feature Request: Support disabling automatic branch creation via parameter #841, 25 reactions; [Feature]: Make branch creation configurable #1921)
  2. Blocked extensibility — users who want different branching need core surgery ([Feature request] Custom namespacing and branch naming conventions #1382, 18 reactions; [Feature]: Allow agent-namespaced branches (claude/**, copilot/**, etc.) to bypass numeric prefix requirement #1901)
  3. Spec iteration friction — mandatory branching makes spec refinement expensive (Spec-Driven Editing Flow: Can't Easily Update or Refine Existing Specs #1191, 107 reactions)
  4. Maintenance burden — every branching enhancement touches 4+ files across 3 languages
  5. Contributor barrier — PRs are large and complex because branching touches everything

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions