Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions scripts/bash/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ get_feature_paths() {

# Use printf '%q' to safely quote values, preventing shell injection
# via crafted branch names or paths containing special characters
if [[ -z "$current_branch" ]]; then
current_branch="${feature_dir%"${feature_dir##*[!\\/]}"}"
current_branch="${current_branch##*[\\/]}"
fi
Comment on lines +154 to +157
printf 'REPO_ROOT=%q\n' "$repo_root"
printf 'CURRENT_BRANCH=%q\n' "$current_branch"
printf 'FEATURE_DIR=%q\n' "$feature_dir"
Expand Down
8 changes: 8 additions & 0 deletions scripts/powershell/common.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,14 @@ function Get-FeaturePathsEnv {
[Console]::Error.WriteLine("ERROR: Feature directory not found. Set SPECIFY_FEATURE_DIRECTORY or run the specify command to create .specify/feature.json.")
exit 1
}

if (-not $currentBranch) {
$normalizedFeatureDir = $featureDir.TrimEnd(
[System.IO.Path]::DirectorySeparatorChar,
[System.IO.Path]::AltDirectorySeparatorChar
)
$currentBranch = Split-Path -Path $normalizedFeatureDir -Leaf
}
Comment on lines +143 to +149

[PSCustomObject]@{
REPO_ROOT = $repoRoot
Expand Down
95 changes: 88 additions & 7 deletions tests/test_check_prerequisites_paths_only.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@
CHECK_PREREQS_PS = PROJECT_ROOT / "scripts" / "powershell" / "check-prerequisites.ps1"

HAS_PWSH = shutil.which("pwsh") is not None
_WINDOWS_POWERSHELL = (shutil.which("powershell.exe") or shutil.which("powershell")) if os.name == "nt" else None
_WINDOWS_POWERSHELL = (
(shutil.which("powershell.exe") or shutil.which("powershell"))
if os.name == "nt"
else None
)


def _install_bash_scripts(repo: Path) -> None:
Expand Down Expand Up @@ -141,6 +145,46 @@ def test_paths_only_text_mode_on_non_spec_branch(prereq_repo: Path) -> None:
assert "FEATURE_DIR:" in result.stdout


@requires_bash
def test_paths_only_falls_back_to_feature_dir_basename(prereq_repo: Path) -> None:
"""--paths-only should emit a non-empty branch name from feature.json."""
feat = prereq_repo / "specs" / "001-my-feature"
feat.mkdir(parents=True, exist_ok=True)
_write_feature_json(prereq_repo)
script = prereq_repo / ".specify" / "scripts" / "bash" / "check-prerequisites.sh"
result = subprocess.run(
["bash", str(script), "--json", "--paths-only"],
cwd=prereq_repo,
capture_output=True,
text=True,
check=False,
env=_clean_env(),
)
Comment on lines +155 to +162
assert result.returncode == 0, result.stderr
data = json.loads(result.stdout)
assert data["BRANCH"] == "001-my-feature"


@requires_bash
def test_paths_only_fallback_handles_windows_style_feature_dir(
prereq_repo: Path,
) -> None:
"""--paths-only should derive BRANCH from backslash-separated feature dirs."""
_write_feature_json(prereq_repo, "specs\\001-my-feature")
Comment on lines +172 to +173
script = prereq_repo / ".specify" / "scripts" / "bash" / "check-prerequisites.sh"
result = subprocess.run(
["bash", str(script), "--json", "--paths-only"],
cwd=prereq_repo,
capture_output=True,
text=True,
check=False,
env=_clean_env(),
)
assert result.returncode == 0, result.stderr
data = json.loads(result.stdout)
assert data["BRANCH"] == "001-my-feature"


@requires_bash
def test_normal_mode_still_validates_branch(prereq_repo: Path) -> None:
"""Without --paths-only, feature directory validation must still fail on main."""
Expand All @@ -160,13 +204,17 @@ def test_normal_mode_still_validates_branch(prereq_repo: Path) -> None:
# ── PowerShell tests ──────────────────────────────────────────────────────


@pytest.mark.skipif(not (HAS_PWSH or _WINDOWS_POWERSHELL), reason="no PowerShell available")
@pytest.mark.skipif(
not (HAS_PWSH or _WINDOWS_POWERSHELL), reason="no PowerShell available"
)
def test_ps_paths_only_succeeds_on_non_spec_branch(prereq_repo: Path) -> None:
"""-PathsOnly must return paths when feature.json pins the feature dir."""
feat = prereq_repo / "specs" / "001-my-feature"
feat.mkdir(parents=True, exist_ok=True)
_write_feature_json(prereq_repo)
script = prereq_repo / ".specify" / "scripts" / "powershell" / "check-prerequisites.ps1"
script = (
prereq_repo / ".specify" / "scripts" / "powershell" / "check-prerequisites.ps1"
)
exe = "pwsh" if HAS_PWSH else _WINDOWS_POWERSHELL
result = subprocess.run(
[exe, "-NoProfile", "-File", str(script), "-Json", "-PathsOnly"],
Expand All @@ -183,7 +231,9 @@ def test_ps_paths_only_succeeds_on_non_spec_branch(prereq_repo: Path) -> None:
assert "FEATURE_DIR" in data


@pytest.mark.skipif(not (HAS_PWSH or _WINDOWS_POWERSHELL), reason="no PowerShell available")
@pytest.mark.skipif(
not (HAS_PWSH or _WINDOWS_POWERSHELL), reason="no PowerShell available"
)
def test_ps_paths_only_succeeds_on_spec_branch(prereq_repo: Path) -> None:
"""-PathsOnly must also work when feature.json and SPECIFY_FEATURE agree."""
subprocess.run(
Expand All @@ -194,7 +244,9 @@ def test_ps_paths_only_succeeds_on_spec_branch(prereq_repo: Path) -> None:
feat = prereq_repo / "specs" / "001-my-feature"
feat.mkdir(parents=True, exist_ok=True)
_write_feature_json(prereq_repo)
script = prereq_repo / ".specify" / "scripts" / "powershell" / "check-prerequisites.ps1"
script = (
prereq_repo / ".specify" / "scripts" / "powershell" / "check-prerequisites.ps1"
)
exe = "pwsh" if HAS_PWSH else _WINDOWS_POWERSHELL
env = _clean_env()
env["SPECIFY_FEATURE"] = "001-my-feature"
Expand All @@ -211,10 +263,39 @@ def test_ps_paths_only_succeeds_on_spec_branch(prereq_repo: Path) -> None:
assert "FEATURE_DIR" in data


@pytest.mark.skipif(not (HAS_PWSH or _WINDOWS_POWERSHELL), reason="no PowerShell available")
@pytest.mark.skipif(
not (HAS_PWSH or _WINDOWS_POWERSHELL), reason="no PowerShell available"
)
def test_ps_paths_only_falls_back_to_feature_dir_basename(prereq_repo: Path) -> None:
"""-PathsOnly should emit a non-empty branch name from feature.json."""
feat = prereq_repo / "specs" / "001-my-feature"
feat.mkdir(parents=True, exist_ok=True)
_write_feature_json(prereq_repo)
script = (
prereq_repo / ".specify" / "scripts" / "powershell" / "check-prerequisites.ps1"
)
exe = "pwsh" if HAS_PWSH else _WINDOWS_POWERSHELL
result = subprocess.run(
[exe, "-NoProfile", "-File", str(script), "-Json", "-PathsOnly"],
cwd=prereq_repo,
capture_output=True,
text=True,
check=False,
env=_clean_env(),
)
assert result.returncode == 0, result.stderr
data = json.loads(result.stdout)
assert data["BRANCH"] == "001-my-feature"


@pytest.mark.skipif(
not (HAS_PWSH or _WINDOWS_POWERSHELL), reason="no PowerShell available"
)
def test_ps_normal_mode_still_validates_branch(prereq_repo: Path) -> None:
"""Without -PathsOnly, feature directory validation must still fail on main."""
script = prereq_repo / ".specify" / "scripts" / "powershell" / "check-prerequisites.ps1"
script = (
prereq_repo / ".specify" / "scripts" / "powershell" / "check-prerequisites.ps1"
)
exe = "pwsh" if HAS_PWSH else _WINDOWS_POWERSHELL
result = subprocess.run(
[exe, "-NoProfile", "-File", str(script), "-Json"],
Expand Down