mirror of
https://github.com/github/spec-kit.git
synced 2026-03-24 06:13:08 +00:00
fix: address all PR review comments for git extension
- 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
This commit is contained in:
committed by
GitHub
parent
312c37be25
commit
5e49ec6936
@@ -32,20 +32,6 @@ Configuration is stored in `.specify/extensions/git/git-config.yml`:
|
|||||||
```yaml
|
```yaml
|
||||||
# Branch numbering strategy: "sequential" or "timestamp"
|
# Branch numbering strategy: "sequential" or "timestamp"
|
||||||
branch_numbering: sequential
|
branch_numbering: sequential
|
||||||
|
|
||||||
# Branch name template
|
|
||||||
branch_template: "{number}-{short_name}"
|
|
||||||
|
|
||||||
# Whether to fetch remotes before computing next branch number
|
|
||||||
auto_fetch: true
|
|
||||||
```
|
|
||||||
|
|
||||||
### Environment Variable Override
|
|
||||||
|
|
||||||
Set `SPECKIT_GIT_BRANCH_NUMBERING` to override the `branch_numbering` config value:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export SPECKIT_GIT_BRANCH_NUMBERING=timestamp
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|||||||
@@ -3,10 +3,3 @@
|
|||||||
|
|
||||||
# Branch numbering strategy: "sequential" (001, 002, ...) or "timestamp" (YYYYMMDD-HHMMSS)
|
# Branch numbering strategy: "sequential" (001, 002, ...) or "timestamp" (YYYYMMDD-HHMMSS)
|
||||||
branch_numbering: sequential
|
branch_numbering: sequential
|
||||||
|
|
||||||
# Branch name template (used with sequential numbering)
|
|
||||||
# Available placeholders: {number}, {short_name}
|
|
||||||
branch_template: "{number}-{short_name}"
|
|
||||||
|
|
||||||
# Whether to run `git fetch --all --prune` before computing the next branch number
|
|
||||||
auto_fetch: true
|
|
||||||
|
|||||||
@@ -45,5 +45,3 @@ tags:
|
|||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
branch_numbering: sequential
|
branch_numbering: sequential
|
||||||
branch_template: "{number}-{short_name}"
|
|
||||||
auto_fetch: true
|
|
||||||
|
|||||||
@@ -176,21 +176,39 @@ clean_branch_name() {
|
|||||||
# were initialised with --no-git.
|
# were initialised with --no-git.
|
||||||
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
# Source common.sh: try the core scripts directory first (standard layout),
|
# Source common.sh using the following priority:
|
||||||
# then fall back to the extension's sibling copy.
|
# 1. common.sh next to this script (source checkout layout)
|
||||||
|
# 2. .specify/scripts/bash/common.sh under the project root (installed project)
|
||||||
|
# 3. scripts/bash/common.sh under the project root (source checkout fallback)
|
||||||
|
# 4. git-common.sh next to this script (minimal fallback)
|
||||||
|
_common_loaded=false
|
||||||
|
|
||||||
if [ -f "$SCRIPT_DIR/common.sh" ]; then
|
if [ -f "$SCRIPT_DIR/common.sh" ]; then
|
||||||
source "$SCRIPT_DIR/common.sh"
|
source "$SCRIPT_DIR/common.sh"
|
||||||
|
_common_loaded=true
|
||||||
else
|
else
|
||||||
# When running from an extension install (.specify/extensions/git/scripts/bash/),
|
# When running from an extension install (.specify/extensions/git/scripts/bash/),
|
||||||
# resolve common.sh from the project's core scripts directory.
|
# resolve to .specify/ (4 levels up), then to the project root (5 levels up).
|
||||||
_ext_repo_root="$(cd "$SCRIPT_DIR/../../../../.." 2>/dev/null && pwd)"
|
_dot_specify="$(cd "$SCRIPT_DIR/../../../.." 2>/dev/null && pwd)"
|
||||||
if [ -f "$_ext_repo_root/scripts/bash/common.sh" ]; then
|
_project_root="$(cd "$SCRIPT_DIR/../../../../.." 2>/dev/null && pwd)"
|
||||||
source "$_ext_repo_root/scripts/bash/common.sh"
|
|
||||||
|
if [ -n "$_dot_specify" ] && [ -f "$_dot_specify/scripts/bash/common.sh" ]; then
|
||||||
|
source "$_dot_specify/scripts/bash/common.sh"
|
||||||
|
_common_loaded=true
|
||||||
|
elif [ -n "$_project_root" ] && [ -f "$_project_root/scripts/bash/common.sh" ]; then
|
||||||
|
source "$_project_root/scripts/bash/common.sh"
|
||||||
|
_common_loaded=true
|
||||||
elif [ -f "$SCRIPT_DIR/git-common.sh" ]; then
|
elif [ -f "$SCRIPT_DIR/git-common.sh" ]; then
|
||||||
source "$SCRIPT_DIR/git-common.sh"
|
source "$SCRIPT_DIR/git-common.sh"
|
||||||
|
_common_loaded=true
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ "$_common_loaded" != "true" ]; then
|
||||||
|
echo "Error: Could not locate common.sh or git-common.sh. Please ensure the Specify core scripts are installed." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
if git rev-parse --show-toplevel >/dev/null 2>&1; then
|
if git rev-parse --show-toplevel >/dev/null 2>&1; then
|
||||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||||
HAS_GIT=true
|
HAS_GIT=true
|
||||||
|
|||||||
@@ -146,20 +146,37 @@ if (-not $fallbackRoot) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Load common functions (includes Resolve-Template).
|
# Load common functions (includes Resolve-Template).
|
||||||
# Try the core scripts directory first (standard layout), then fall back
|
# Search locations in priority order:
|
||||||
# to the extension's sibling copy.
|
# 1. common.ps1 next to this script (source checkout layout)
|
||||||
|
# 2. .specify/scripts/powershell/common.ps1 under the project root (installed project)
|
||||||
|
# 3. scripts/powershell/common.ps1 under the project root (source checkout fallback)
|
||||||
|
# 4. git-common.ps1 next to this script (minimal fallback)
|
||||||
|
$commonLoaded = $false
|
||||||
|
|
||||||
if (Test-Path "$PSScriptRoot/common.ps1") {
|
if (Test-Path "$PSScriptRoot/common.ps1") {
|
||||||
. "$PSScriptRoot/common.ps1"
|
. "$PSScriptRoot/common.ps1"
|
||||||
|
$commonLoaded = $true
|
||||||
} else {
|
} else {
|
||||||
# When running from an extension install (.specify/extensions/git/scripts/powershell/),
|
$coreCommonCandidates = @()
|
||||||
# resolve common.ps1 from the project's core scripts directory.
|
|
||||||
$extRepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "../../../../..") -ErrorAction SilentlyContinue)
|
if ($fallbackRoot) {
|
||||||
$coreCommon = if ($extRepoRoot) { Join-Path $extRepoRoot "scripts/powershell/common.ps1" } else { "" }
|
$coreCommonCandidates += (Join-Path $fallbackRoot ".specify/scripts/powershell/common.ps1")
|
||||||
if ($coreCommon -and (Test-Path $coreCommon)) {
|
$coreCommonCandidates += (Join-Path $fallbackRoot "scripts/powershell/common.ps1")
|
||||||
. $coreCommon
|
|
||||||
} elseif (Test-Path "$PSScriptRoot/git-common.ps1") {
|
|
||||||
. "$PSScriptRoot/git-common.ps1"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$coreCommonCandidates += "$PSScriptRoot/git-common.ps1"
|
||||||
|
|
||||||
|
foreach ($candidate in $coreCommonCandidates) {
|
||||||
|
if ($candidate -and (Test-Path $candidate)) {
|
||||||
|
. $candidate
|
||||||
|
$commonLoaded = $true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $commonLoaded) {
|
||||||
|
throw "Unable to locate common script file. Please ensure the Specify core scripts are installed."
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1195,7 +1195,7 @@ def _install_bundled_git_extension(project_path: Path) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from .extensions import ExtensionManager, ExtensionError
|
from .extensions import ExtensionManager
|
||||||
manager = ExtensionManager(project_path)
|
manager = ExtensionManager(project_path)
|
||||||
|
|
||||||
# Skip if already installed (e.g. via preset)
|
# Skip if already installed (e.g. via preset)
|
||||||
|
|||||||
@@ -73,14 +73,28 @@ Given that feature description, do this:
|
|||||||
- "Create a dashboard for analytics" → "analytics-dashboard"
|
- "Create a dashboard for analytics" → "analytics-dashboard"
|
||||||
- "Fix payment processing timeout bug" → "fix-payment-timeout"
|
- "Fix payment processing timeout bug" → "fix-payment-timeout"
|
||||||
|
|
||||||
2. **Create the feature branch** by running the script with `--short-name` (and `--json`). In sequential mode, do NOT pass `--number` — the script auto-detects the next available number. In timestamp mode, the script generates a `YYYYMMDD-HHMMSS` prefix automatically:
|
2. **Create the feature branch** (unless already handled by a `before_specify` hook — see Pre-Execution Checks above). If a mandatory `before_specify` hook for `speckit.git.feature` already executed and created the branch, **skip this step entirely** and use the branch/spec information from the hook result. Otherwise:
|
||||||
|
|
||||||
**Git extension check**: Before running the branch creation script, check if the git extension is enabled:
|
**Git extension check**: Before running the branch creation script, check if the git extension is enabled:
|
||||||
- Check if `.specify/extensions/.registry` exists (a single JSON file tracking all extensions)
|
- Check if `.specify/extensions/.registry` exists (a single JSON file tracking all extensions)
|
||||||
- If it exists, read the JSON and look for `extensions.git`; verify its `"enabled"` field is `true` (or not explicitly `false`)
|
- If it exists, read the JSON and look for `extensions.git`; verify its `"enabled"` field is `true` (or not explicitly `false`)
|
||||||
- If the git extension is **disabled** (explicitly `"enabled": false`), **skip branch creation entirely** — proceed directly to step 3 using a spec directory name derived from the short name (e.g., `specs/<short-name>/`)
|
- If the git extension is **disabled** (explicitly `"enabled": false`), **skip branch creation entirely** — do **not** run the branch creation script. Instead:
|
||||||
|
- Derive a spec directory name from the short name, e.g. `specs/<short-name>/`
|
||||||
|
- Explicitly set the following variables so later steps can use them:
|
||||||
|
- `FEATURE_DIR="specs/<short-name>"`
|
||||||
|
- `SPEC_FILE="$FEATURE_DIR/spec.md"`
|
||||||
|
- Ensure the directory and spec file exist:
|
||||||
|
- Bash:
|
||||||
|
- `mkdir -p "$FEATURE_DIR"`
|
||||||
|
- `: > "$SPEC_FILE"`
|
||||||
|
- PowerShell:
|
||||||
|
- `New-Item -ItemType Directory -Path $env:FEATURE_DIR -Force | Out-Null`
|
||||||
|
- `New-Item -ItemType File -Path $env:SPEC_FILE -Force | Out-Null`
|
||||||
|
- Then proceed directly to step 3 using `FEATURE_DIR` and `SPEC_FILE`
|
||||||
- If the registry file does not exist, proceed with branch creation using the default behavior (backward compatibility)
|
- If the registry file does not exist, proceed with branch creation using the default behavior (backward compatibility)
|
||||||
|
|
||||||
|
Run the script with `--short-name` (and `--json`). In sequential mode, do NOT pass `--number` — the script auto-detects the next available number. In timestamp mode, the script generates a `YYYYMMDD-HHMMSS` prefix automatically:
|
||||||
|
|
||||||
**Branch numbering mode**: Before running the script, determine the branch numbering strategy:
|
**Branch numbering mode**: Before running the script, determine the branch numbering strategy:
|
||||||
1. Check `.specify/extensions/git/git-config.yml` for `branch_numbering` value (extension config takes precedence)
|
1. Check `.specify/extensions/git/git-config.yml` for `branch_numbering` value (extension config takes precedence)
|
||||||
2. If not found, check `.specify/init-options.json` for `branch_numbering` value (backward compatibility)
|
2. If not found, check `.specify/init-options.json` for `branch_numbering` value (backward compatibility)
|
||||||
@@ -89,8 +103,8 @@ Given that feature description, do this:
|
|||||||
- If `"sequential"` or absent, do not add any extra flag (default behavior)
|
- If `"sequential"` or absent, do not add any extra flag (default behavior)
|
||||||
|
|
||||||
**Script resolution**: Use the extension's bundled scripts when available, falling back to core scripts:
|
**Script resolution**: Use the extension's bundled scripts when available, falling back to core scripts:
|
||||||
- If `.specify/extensions/git/scripts/bash/create-new-feature.sh` exists, use it
|
- **Bash**: If `.specify/extensions/git/scripts/bash/create-new-feature.sh` exists, use it; otherwise, fall back to `{SCRIPT}`
|
||||||
- Otherwise, fall back to `{SCRIPT}`
|
- **PowerShell**: If `.specify/extensions/git/scripts/powershell/create-new-feature.ps1` exists, use it; otherwise, fall back to `{SCRIPT}`
|
||||||
|
|
||||||
- Bash example: `{SCRIPT} --json --short-name "user-auth" "Add user authentication"`
|
- Bash example: `{SCRIPT} --json --short-name "user-auth" "Add user authentication"`
|
||||||
- Bash (timestamp): `{SCRIPT} --json --timestamp --short-name "user-auth" "Add user authentication"`
|
- Bash (timestamp): `{SCRIPT} --json --timestamp --short-name "user-auth" "Add user authentication"`
|
||||||
|
|||||||
@@ -87,3 +87,54 @@ class TestBranchNumberingValidation:
|
|||||||
result = runner.invoke(app, ["init", str(tmp_path / "proj"), "--ai", "claude", "--branch-numbering", "timestamp", "--ignore-agent-tools"])
|
result = runner.invoke(app, ["init", str(tmp_path / "proj"), "--ai", "claude", "--branch-numbering", "timestamp", "--ignore-agent-tools"])
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert "Invalid --branch-numbering" not in (result.output or "")
|
assert "Invalid --branch-numbering" not in (result.output or "")
|
||||||
|
|
||||||
|
|
||||||
|
class TestGitExtensionAutoInstall:
|
||||||
|
"""Tests for bundled git extension auto-install during specify init."""
|
||||||
|
|
||||||
|
def test_git_extension_installed_during_init(self, tmp_path: Path, monkeypatch):
|
||||||
|
"""verify that `specify init` auto-installs the bundled git extension."""
|
||||||
|
from typer.testing import CliRunner
|
||||||
|
from specify_cli import app
|
||||||
|
|
||||||
|
def _fake_download(project_path, *args, **kwargs):
|
||||||
|
Path(project_path).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
monkeypatch.setattr("specify_cli.download_and_extract_template", _fake_download)
|
||||||
|
|
||||||
|
project_dir = tmp_path / "proj"
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(app, ["init", str(project_dir), "--ai", "claude", "--ignore-agent-tools"])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
# Extension files should exist
|
||||||
|
ext_dir = project_dir / ".specify" / "extensions" / "git"
|
||||||
|
assert ext_dir.is_dir(), "git extension directory not created"
|
||||||
|
assert (ext_dir / "extension.yml").is_file(), "extension.yml not installed"
|
||||||
|
|
||||||
|
# Registry should contain the git extension
|
||||||
|
registry_file = project_dir / ".specify" / "extensions" / ".registry"
|
||||||
|
assert registry_file.is_file(), "extension registry not created"
|
||||||
|
registry = json.loads(registry_file.read_text())
|
||||||
|
assert "git" in registry.get("extensions", {}), "git not in registry"
|
||||||
|
assert registry["extensions"]["git"]["enabled"] is True
|
||||||
|
|
||||||
|
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")
|
||||||
|
|||||||
Reference in New Issue
Block a user