From 53db06c806c16737d111226f7ecf7f5d4c9b7c32 Mon Sep 17 00:00:00 2001 From: Peter Hoburg Date: Sun, 2 Nov 2025 20:47:00 -0500 Subject: [PATCH 1/7] Added SPECIFY_USE_CURRENT_BRANCH env var to use the current branch. --- AGENTS.md | 9 +++++++++ CHANGELOG.md | 15 +++++++++++++++ README.md | 1 + scripts/bash/common.sh | 13 +++++++++++++ scripts/powershell/common.ps1 | 17 ++++++++++++++++- 5 files changed, 54 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index d34efb673..67a6d3087 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,6 +13,15 @@ The toolkit supports multiple AI coding assistants, allowing teams to use their ## General practices - Any changes to `__init__.py` for the Specify CLI require a version rev in `pyproject.toml` and addition of entries to `CHANGELOG.md`. +- Environment variables that affect script behavior should be documented in both `README.md` and `CHANGELOG.md`. + +### Environment Variables + +The Spec Kit workflow recognizes the following environment variables: + +- **`SPECIFY_FEATURE`**: Override feature detection. Set to a specific feature directory name (e.g., `001-photo-albums`) to work on that feature regardless of git branch or directory scan results. This has the highest priority in feature detection. + +- **`SPECIFY_USE_CURRENT_BRANCH`**: Use the current git branch name as the feature identifier without creating a new branch. Useful when working on existing branches that don't follow the `###-name` convention. Works with any branch name. Priority: below `SPECIFY_FEATURE`, above default git detection. ## Adding New Agent Support diff --git a/CHANGELOG.md b/CHANGELOG.md index ea6729145..a09953d91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,21 @@ All notable changes to the Specify CLI and templates are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- **Git Branch Integration with `SPECIFY_USE_CURRENT_BRANCH`**: New environment variable to use the current git branch name as the feature identifier without creating a new branch + - Enables working on existing branches that don't follow the `###-name` convention + - Handles edge cases: detached HEAD state, non-git repositories, git command failures + - Maintains existing priority chain: `SPECIFY_FEATURE` > `SPECIFY_USE_CURRENT_BRANCH` > git branch > directory scan > "main" + - Single git command execution (no redundancy) + - Examples: + - `export SPECIFY_USE_CURRENT_BRANCH=1` (bash) + - `$env:SPECIFY_USE_CURRENT_BRANCH="1"` (PowerShell) + - Works with any branch naming pattern (feature/, bugfix/, hotfix/, main, master, develop, etc.) + - Available in both bash and PowerShell scripts + ## [0.0.20] - 2025-10-14 ### Added diff --git a/README.md b/README.md index 1c7dda215..1dc006869 100644 --- a/README.md +++ b/README.md @@ -252,6 +252,7 @@ Additional commands for enhanced quality and validation: | Variable | Description | |------------------|------------------------------------------------------------------------------------------------| | `SPECIFY_FEATURE` | Override feature detection for non-Git repositories. Set to the feature directory name (e.g., `001-photo-albums`) to work on a specific feature when not using Git branches.
**Must be set in the context of the agent you're working with prior to using `/speckit.plan` or follow-up commands. | +| `SPECIFY_USE_CURRENT_BRANCH` | Use the current git branch name as the feature identifier without creating a new branch. Useful when working on existing branches that don't follow the `###-name` convention.
**Example:** `export SPECIFY_USE_CURRENT_BRANCH=1` (bash) or `$env:SPECIFY_USE_CURRENT_BRANCH="1"` (PowerShell)
**Note:** `SPECIFY_FEATURE` takes precedence if both are set. | ## 📚 Core Philosophy diff --git a/scripts/bash/common.sh b/scripts/bash/common.sh index 6931eccc8..8419b706b 100644 --- a/scripts/bash/common.sh +++ b/scripts/bash/common.sh @@ -20,6 +20,19 @@ get_current_branch() { return fi + # Check if SPECIFY_USE_CURRENT_BRANCH is set to use current git branch + # without creating a new feature branch + if [[ -n "${SPECIFY_USE_CURRENT_BRANCH:-}" ]]; then + local current_git_branch + current_git_branch=$(git rev-parse --abbrev-ref HEAD 2>&1) + if [[ $? -eq 0 && "$current_git_branch" != "HEAD" ]]; then + # Valid branch (not detached HEAD) - use it + echo "$current_git_branch" + return + fi + # Detached HEAD or git command failed - fall through to normal behavior + fi + # Then check git if available if git rev-parse --abbrev-ref HEAD >/dev/null 2>&1; then git rev-parse --abbrev-ref HEAD diff --git a/scripts/powershell/common.ps1 b/scripts/powershell/common.ps1 index b0be27354..581a5a193 100644 --- a/scripts/powershell/common.ps1 +++ b/scripts/powershell/common.ps1 @@ -20,7 +20,22 @@ function Get-CurrentBranch { if ($env:SPECIFY_FEATURE) { return $env:SPECIFY_FEATURE } - + + # Check if SPECIFY_USE_CURRENT_BRANCH is set to use current git branch + # without creating a new feature branch + if ($env:SPECIFY_USE_CURRENT_BRANCH) { + try { + $currentGitBranch = git rev-parse --abbrev-ref HEAD 2>&1 + if ($LASTEXITCODE -eq 0 -and $currentGitBranch -ne 'HEAD') { + # Valid branch (not detached HEAD) - use it + return $currentGitBranch + } + # Detached HEAD - fall through to normal behavior + } catch { + # Git command failed, fall through + } + } + # Then check git if available try { $result = git rev-parse --abbrev-ref HEAD 2>$null From 9532f0ce537ccc5d82dcb25696cc70f30d5eba45 Mon Sep 17 00:00:00 2001 From: Peter Hoburg Date: Tue, 4 Nov 2025 12:11:49 -0500 Subject: [PATCH 2/7] git branch in common --- scripts/bash/common.sh | 5 +++++ scripts/powershell/common.ps1 | 9 +++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/scripts/bash/common.sh b/scripts/bash/common.sh index 8419b706b..10f539872 100644 --- a/scripts/bash/common.sh +++ b/scripts/bash/common.sh @@ -85,6 +85,11 @@ check_feature_branch() { return 0 fi + # If SPECIFY_USE_CURRENT_BRANCH is set, skip pattern validation + if [[ -n "${SPECIFY_USE_CURRENT_BRANCH:-}" ]]; then + return 0 + fi + if [[ ! "$branch" =~ ^[0-9]{3}- ]]; then echo "ERROR: Not on a feature branch. Current branch: $branch" >&2 echo "Feature branches should be named like: 001-feature-name" >&2 diff --git a/scripts/powershell/common.ps1 b/scripts/powershell/common.ps1 index 581a5a193..a64bf492f 100644 --- a/scripts/powershell/common.ps1 +++ b/scripts/powershell/common.ps1 @@ -87,13 +87,18 @@ function Test-FeatureBranch { [string]$Branch, [bool]$HasGit = $true ) - + # For non-git repos, we can't enforce branch naming but still provide output if (-not $HasGit) { Write-Warning "[specify] Warning: Git repository not detected; skipped branch validation" return $true } - + + # If SPECIFY_USE_CURRENT_BRANCH is set, skip pattern validation + if ($env:SPECIFY_USE_CURRENT_BRANCH) { + return $true + } + if ($Branch -notmatch '^[0-9]{3}-') { Write-Output "ERROR: Not on a feature branch. Current branch: $Branch" Write-Output "Feature branches should be named like: 001-feature-name" From c18344ca29b370aa99b87b8112705b77ef936e61 Mon Sep 17 00:00:00 2001 From: Peter Hoburg Date: Tue, 4 Nov 2025 12:25:17 -0500 Subject: [PATCH 3/7] Fixed create-new-feature --- scripts/bash/create-new-feature.sh | 69 +++++++++++-------- scripts/powershell/create-new-feature.ps1 | 80 +++++++++++++++-------- 2 files changed, 94 insertions(+), 55 deletions(-) diff --git a/scripts/bash/create-new-feature.sh b/scripts/bash/create-new-feature.sh index 86d9ecf83..1b2535219 100644 --- a/scripts/bash/create-new-feature.sh +++ b/scripts/bash/create-new-feature.sh @@ -210,34 +210,51 @@ if [ -z "$BRANCH_NUMBER" ]; then fi fi -FEATURE_NUM=$(printf "%03d" "$BRANCH_NUMBER") -BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}" +# Check if SPECIFY_USE_CURRENT_BRANCH is set +if [[ -n "${SPECIFY_USE_CURRENT_BRANCH:-}" ]]; then + if [ "$HAS_GIT" = true ]; then + # Use current branch name + BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD 2>&1) + if [[ $? -ne 0 || "$BRANCH_NAME" == "HEAD" ]]; then + >&2 echo "[specify] Error: Cannot determine current branch name" + exit 1 + fi + >&2 echo "[specify] Using current branch: $BRANCH_NAME" + else + >&2 echo "[specify] Error: SPECIFY_USE_CURRENT_BRANCH requires a git repository" + exit 1 + fi +else + # Normal mode: generate new branch name and create it + FEATURE_NUM=$(printf "%03d" "$BRANCH_NUMBER") + BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}" -# GitHub enforces a 244-byte limit on branch names -# Validate and truncate if necessary -MAX_BRANCH_LENGTH=244 -if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then - # Calculate how much we need to trim from suffix - # Account for: feature number (3) + hyphen (1) = 4 chars - MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - 4)) - - # Truncate suffix at word boundary if possible - TRUNCATED_SUFFIX=$(echo "$BRANCH_SUFFIX" | cut -c1-$MAX_SUFFIX_LENGTH) - # Remove trailing hyphen if truncation created one - TRUNCATED_SUFFIX=$(echo "$TRUNCATED_SUFFIX" | sed 's/-$//') - - ORIGINAL_BRANCH_NAME="$BRANCH_NAME" - BRANCH_NAME="${FEATURE_NUM}-${TRUNCATED_SUFFIX}" - - >&2 echo "[specify] Warning: Branch name exceeded GitHub's 244-byte limit" - >&2 echo "[specify] Original: $ORIGINAL_BRANCH_NAME (${#ORIGINAL_BRANCH_NAME} bytes)" - >&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)" -fi + # GitHub enforces a 244-byte limit on branch names + # Validate and truncate if necessary + MAX_BRANCH_LENGTH=244 + if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then + # Calculate how much we need to trim from suffix + # Account for: feature number (3) + hyphen (1) = 4 chars + MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - 4)) -if [ "$HAS_GIT" = true ]; then - git checkout -b "$BRANCH_NAME" -else - >&2 echo "[specify] Warning: Git repository not detected; skipped branch creation for $BRANCH_NAME" + # Truncate suffix at word boundary if possible + TRUNCATED_SUFFIX=$(echo "$BRANCH_SUFFIX" | cut -c1-$MAX_SUFFIX_LENGTH) + # Remove trailing hyphen if truncation created one + TRUNCATED_SUFFIX=$(echo "$TRUNCATED_SUFFIX" | sed 's/-$//') + + ORIGINAL_BRANCH_NAME="$BRANCH_NAME" + BRANCH_NAME="${FEATURE_NUM}-${TRUNCATED_SUFFIX}" + + >&2 echo "[specify] Warning: Branch name exceeded GitHub's 244-byte limit" + >&2 echo "[specify] Original: $ORIGINAL_BRANCH_NAME (${#ORIGINAL_BRANCH_NAME} bytes)" + >&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)" + fi + + if [ "$HAS_GIT" = true ]; then + git checkout -b "$BRANCH_NAME" + else + >&2 echo "[specify] Warning: Git repository not detected; skipped branch creation for $BRANCH_NAME" + fi fi FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME" diff --git a/scripts/powershell/create-new-feature.ps1 b/scripts/powershell/create-new-feature.ps1 index 4daa6d2c0..6f92c2b18 100644 --- a/scripts/powershell/create-new-feature.ps1 +++ b/scripts/powershell/create-new-feature.ps1 @@ -224,38 +224,60 @@ if ($Number -eq 0) { } } -$featureNum = ('{0:000}' -f $Number) -$branchName = "$featureNum-$branchSuffix" +# Check if SPECIFY_USE_CURRENT_BRANCH is set +if ($env:SPECIFY_USE_CURRENT_BRANCH) { + if ($hasGit) { + # Use current branch name + try { + $branchName = git rev-parse --abbrev-ref HEAD 2>&1 + if ($LASTEXITCODE -ne 0 -or $branchName -eq 'HEAD') { + Write-Error "[specify] Error: Cannot determine current branch name" + exit 1 + } + Write-Warning "[specify] Using current branch: $branchName" + } catch { + Write-Error "[specify] Error: Cannot determine current branch name" + exit 1 + } + } else { + Write-Error "[specify] Error: SPECIFY_USE_CURRENT_BRANCH requires a git repository" + exit 1 + } +} else { + # Normal mode: generate new branch name and create it + $featureNum = ('{0:000}' -f $Number) + $branchName = "$featureNum-$branchSuffix" -# GitHub enforces a 244-byte limit on branch names -# Validate and truncate if necessary -$maxBranchLength = 244 -if ($branchName.Length -gt $maxBranchLength) { - # Calculate how much we need to trim from suffix - # Account for: feature number (3) + hyphen (1) = 4 chars - $maxSuffixLength = $maxBranchLength - 4 - - # Truncate suffix - $truncatedSuffix = $branchSuffix.Substring(0, [Math]::Min($branchSuffix.Length, $maxSuffixLength)) - # Remove trailing hyphen if truncation created one - $truncatedSuffix = $truncatedSuffix -replace '-$', '' - - $originalBranchName = $branchName - $branchName = "$featureNum-$truncatedSuffix" - - Write-Warning "[specify] Branch name exceeded GitHub's 244-byte limit" - Write-Warning "[specify] Original: $originalBranchName ($($originalBranchName.Length) bytes)" - Write-Warning "[specify] Truncated to: $branchName ($($branchName.Length) bytes)" -} + # GitHub enforces a 244-byte limit on branch names + # Validate and truncate if necessary + $maxBranchLength = 244 + if ($branchName.Length -gt $maxBranchLength) { + # Calculate how much we need to trim from suffix + # Account for: feature number (3) + hyphen (1) = 4 chars + $maxSuffixLength = $maxBranchLength - 4 -if ($hasGit) { - try { - git checkout -b $branchName | Out-Null - } catch { - Write-Warning "Failed to create git branch: $branchName" + # Truncate suffix + $truncatedSuffix = $branchSuffix.Substring(0, [Math]::Min($branchSuffix.Length, $maxSuffixLength)) + # Remove trailing hyphen if truncation created one + $truncatedSuffix = $truncatedSuffix -replace '-$', '' + + $originalBranchName = $branchName + $branchName = "$featureNum-$truncatedSuffix" + + Write-Warning "[specify] Branch name exceeded GitHub's 244-byte limit" + Write-Warning "[specify] Original: $originalBranchName ($($originalBranchName.Length) bytes)" + Write-Warning "[specify] Truncated to: $branchName ($($branchName.Length) bytes)" + } + + if ($hasGit) { + try { + git checkout -b $branchName | Out-Null + } catch { + Write-Warning "Failed to create git branch: $branchName" + } + } else { + Write-Warning "[specify] Warning: Git repository not detected; skipped branch creation for $branchName" } -} else { - Write-Warning "[specify] Warning: Git repository not detected; skipped branch creation for $branchName" } $featureDir = Join-Path $specsDir $branchName From 8e3409bd4d2aae0e7c84b6d581a227fb6cdecfd7 Mon Sep 17 00:00:00 2001 From: Peter Hoburg Date: Tue, 4 Nov 2025 15:45:10 -0500 Subject: [PATCH 4/7] Addressed GH bot feedback. --- scripts/bash/common.sh | 11 ++++++----- scripts/bash/create-new-feature.sh | 6 ++++-- scripts/powershell/common.ps1 | 14 +++++--------- scripts/powershell/create-new-feature.ps1 | 11 +++-------- 4 files changed, 18 insertions(+), 24 deletions(-) diff --git a/scripts/bash/common.sh b/scripts/bash/common.sh index 10f539872..747336a4a 100644 --- a/scripts/bash/common.sh +++ b/scripts/bash/common.sh @@ -24,11 +24,12 @@ get_current_branch() { # without creating a new feature branch if [[ -n "${SPECIFY_USE_CURRENT_BRANCH:-}" ]]; then local current_git_branch - current_git_branch=$(git rev-parse --abbrev-ref HEAD 2>&1) - if [[ $? -eq 0 && "$current_git_branch" != "HEAD" ]]; then - # Valid branch (not detached HEAD) - use it - echo "$current_git_branch" - return + if current_git_branch=$(git rev-parse --abbrev-ref HEAD 2>&1); then + if [[ "$current_git_branch" != "HEAD" ]]; then + # Valid branch (not detached HEAD) - use it + echo "$current_git_branch" + return + fi fi # Detached HEAD or git command failed - fall through to normal behavior fi diff --git a/scripts/bash/create-new-feature.sh b/scripts/bash/create-new-feature.sh index 1b2535219..1075f57a8 100644 --- a/scripts/bash/create-new-feature.sh +++ b/scripts/bash/create-new-feature.sh @@ -214,11 +214,13 @@ fi if [[ -n "${SPECIFY_USE_CURRENT_BRANCH:-}" ]]; then if [ "$HAS_GIT" = true ]; then # Use current branch name - BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD 2>&1) - if [[ $? -ne 0 || "$BRANCH_NAME" == "HEAD" ]]; then + branch_name_output=$(git rev-parse --abbrev-ref HEAD 2>&1) + branch_name_status=$? + if [[ $branch_name_status -ne 0 || "$branch_name_output" == "HEAD" ]]; then >&2 echo "[specify] Error: Cannot determine current branch name" exit 1 fi + BRANCH_NAME="$branch_name_output" >&2 echo "[specify] Using current branch: $BRANCH_NAME" else >&2 echo "[specify] Error: SPECIFY_USE_CURRENT_BRANCH requires a git repository" diff --git a/scripts/powershell/common.ps1 b/scripts/powershell/common.ps1 index a64bf492f..81974cf92 100644 --- a/scripts/powershell/common.ps1 +++ b/scripts/powershell/common.ps1 @@ -24,16 +24,12 @@ function Get-CurrentBranch { # Check if SPECIFY_USE_CURRENT_BRANCH is set to use current git branch # without creating a new feature branch if ($env:SPECIFY_USE_CURRENT_BRANCH) { - try { - $currentGitBranch = git rev-parse --abbrev-ref HEAD 2>&1 - if ($LASTEXITCODE -eq 0 -and $currentGitBranch -ne 'HEAD') { - # Valid branch (not detached HEAD) - use it - return $currentGitBranch - } - # Detached HEAD - fall through to normal behavior - } catch { - # Git command failed, fall through + $currentGitBranch = git rev-parse --abbrev-ref HEAD 2>&1 + if ($LASTEXITCODE -eq 0 -and $currentGitBranch -ne 'HEAD') { + # Valid branch (not detached HEAD) - use it + return $currentGitBranch } + # Detached HEAD or git command failed - fall through to normal behavior } # Then check git if available diff --git a/scripts/powershell/create-new-feature.ps1 b/scripts/powershell/create-new-feature.ps1 index 6f92c2b18..b8764d09b 100644 --- a/scripts/powershell/create-new-feature.ps1 +++ b/scripts/powershell/create-new-feature.ps1 @@ -228,17 +228,12 @@ if ($Number -eq 0) { if ($env:SPECIFY_USE_CURRENT_BRANCH) { if ($hasGit) { # Use current branch name - try { - $branchName = git rev-parse --abbrev-ref HEAD 2>&1 - if ($LASTEXITCODE -ne 0 -or $branchName -eq 'HEAD') { - Write-Error "[specify] Error: Cannot determine current branch name" - exit 1 - } - Write-Warning "[specify] Using current branch: $branchName" - } catch { + $branchName = git rev-parse --abbrev-ref HEAD 2>&1 + if ($LASTEXITCODE -ne 0 -or $branchName -eq 'HEAD') { Write-Error "[specify] Error: Cannot determine current branch name" exit 1 } + Write-Warning "[specify] Using current branch: $branchName" } else { Write-Error "[specify] Error: SPECIFY_USE_CURRENT_BRANCH requires a git repository" exit 1 From 1a741a25691ce152edec02f75242cca1c345c3f3 Mon Sep 17 00:00:00 2001 From: Peter Hoburg Date: Tue, 4 Nov 2025 16:07:37 -0500 Subject: [PATCH 5/7] Added "N/A" featureNum output. --- scripts/bash/create-new-feature.sh | 1 + scripts/powershell/create-new-feature.ps1 | 1 + 2 files changed, 2 insertions(+) diff --git a/scripts/bash/create-new-feature.sh b/scripts/bash/create-new-feature.sh index 1075f57a8..e50a0287d 100644 --- a/scripts/bash/create-new-feature.sh +++ b/scripts/bash/create-new-feature.sh @@ -221,6 +221,7 @@ if [[ -n "${SPECIFY_USE_CURRENT_BRANCH:-}" ]]; then exit 1 fi BRANCH_NAME="$branch_name_output" + FEATURE_NUM="N/A" >&2 echo "[specify] Using current branch: $BRANCH_NAME" else >&2 echo "[specify] Error: SPECIFY_USE_CURRENT_BRANCH requires a git repository" diff --git a/scripts/powershell/create-new-feature.ps1 b/scripts/powershell/create-new-feature.ps1 index b8764d09b..524ef29ed 100644 --- a/scripts/powershell/create-new-feature.ps1 +++ b/scripts/powershell/create-new-feature.ps1 @@ -233,6 +233,7 @@ if ($env:SPECIFY_USE_CURRENT_BRANCH) { Write-Error "[specify] Error: Cannot determine current branch name" exit 1 } + $featureNum = "N/A" Write-Warning "[specify] Using current branch: $branchName" } else { Write-Error "[specify] Error: SPECIFY_USE_CURRENT_BRANCH requires a git repository" From 67f023d5fc4761066aa5e1e56b3699629ec1d420 Mon Sep 17 00:00:00 2001 From: Peter Hoburg Date: Tue, 4 Nov 2025 16:32:36 -0500 Subject: [PATCH 6/7] Sanitize windows dir names. --- scripts/powershell/common.ps1 | 31 +++++++++++++++++++- scripts/powershell/create-new-feature.ps1 | 35 +++++++++++++++++++++-- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/scripts/powershell/common.ps1 b/scripts/powershell/common.ps1 index 81974cf92..3e755b679 100644 --- a/scripts/powershell/common.ps1 +++ b/scripts/powershell/common.ps1 @@ -10,11 +10,34 @@ function Get-RepoRoot { } catch { # Git command failed } - + # Fall back to script location for non-git repos return (Resolve-Path (Join-Path $PSScriptRoot "../../..")).Path } +# Sanitize branch name for use as directory name +# Replaces filesystem-forbidden and problematic characters with safe alternatives +function Sanitize-BranchName { + param([string]$BranchName) + + # Replace problematic characters: + # / → - (prevents nesting on all platforms, Windows forbidden) + # \ → - (Windows forbidden) + # : → - (Windows forbidden, macOS translated) + # * → - (Windows forbidden, shell wildcard) + # ? → - (Windows forbidden, shell wildcard) + # " → - (Windows forbidden) + # < → - (Windows forbidden, shell redirect) + # > → - (Windows forbidden, shell redirect) + # | → - (Windows forbidden, shell pipe) + $sanitized = $BranchName -replace '[/\\:*?"<>|]', '-' + $sanitized = $sanitized -replace '\s+', '-' + $sanitized = $sanitized -replace '^[. -]+', '' + $sanitized = $sanitized -replace '[. -]+$', '' + $sanitized = $sanitized -replace '-+', '-' + return $sanitized +} + function Get-CurrentBranch { # First check if SPECIFY_FEATURE environment variable is set if ($env:SPECIFY_FEATURE) { @@ -105,6 +128,12 @@ function Test-FeatureBranch { function Get-FeatureDir { param([string]$RepoRoot, [string]$Branch) + + # When using current branch, sanitize for filesystem compatibility + if ($env:SPECIFY_USE_CURRENT_BRANCH) { + $Branch = Sanitize-BranchName -BranchName $Branch + } + Join-Path $RepoRoot "specs/$Branch" } diff --git a/scripts/powershell/create-new-feature.ps1 b/scripts/powershell/create-new-feature.ps1 index 524ef29ed..a835a7467 100644 --- a/scripts/powershell/create-new-feature.ps1 +++ b/scripts/powershell/create-new-feature.ps1 @@ -35,6 +35,29 @@ if (-not $FeatureDescription -or $FeatureDescription.Count -eq 0) { $featureDesc = ($FeatureDescription -join ' ').Trim() +# Sanitize branch name for use as directory name +# Replaces filesystem-forbidden and problematic characters with safe alternatives +function Sanitize-BranchName { + param([string]$BranchName) + + # Replace problematic characters: + # / → - (prevents nesting on all platforms, Windows forbidden) + # \ → - (Windows forbidden) + # : → - (Windows forbidden, macOS translated) + # * → - (Windows forbidden, shell wildcard) + # ? → - (Windows forbidden, shell wildcard) + # " → - (Windows forbidden) + # < → - (Windows forbidden, shell redirect) + # > → - (Windows forbidden, shell redirect) + # | → - (Windows forbidden, shell pipe) + $sanitized = $BranchName -replace '[/\\:*?"<>|]', '-' + $sanitized = $sanitized -replace '\s+', '-' + $sanitized = $sanitized -replace '^[. -]+', '' + $sanitized = $sanitized -replace '[. -]+$', '' + $sanitized = $sanitized -replace '-+', '-' + return $sanitized +} + # Resolve repository root. Prefer git information when available, but fall back # to searching for repository markers so the workflow still functions in repositories that # were initialized with --no-git. @@ -228,13 +251,19 @@ if ($Number -eq 0) { if ($env:SPECIFY_USE_CURRENT_BRANCH) { if ($hasGit) { # Use current branch name - $branchName = git rev-parse --abbrev-ref HEAD 2>&1 - if ($LASTEXITCODE -ne 0 -or $branchName -eq 'HEAD') { + $originalBranch = git rev-parse --abbrev-ref HEAD 2>&1 + if ($LASTEXITCODE -ne 0 -or $originalBranch -eq 'HEAD') { Write-Error "[specify] Error: Cannot determine current branch name" exit 1 } + # Sanitize branch name for filesystem compatibility + $branchName = Sanitize-BranchName -BranchName $originalBranch $featureNum = "N/A" - Write-Warning "[specify] Using current branch: $branchName" + if ($originalBranch -ne $branchName) { + Write-Warning "[specify] Using current branch: $originalBranch (sanitized to: $branchName)" + } else { + Write-Warning "[specify] Using current branch: $branchName" + } } else { Write-Error "[specify] Error: SPECIFY_USE_CURRENT_BRANCH requires a git repository" exit 1 From e1661c6efe4d14498a46cdf204dda64e908ddb8f Mon Sep 17 00:00:00 2001 From: Peter Hoburg Date: Tue, 4 Nov 2025 16:35:14 -0500 Subject: [PATCH 7/7] Sanitize unix dir names. --- scripts/bash/common.sh | 31 ++++++++++++++++++++++++++++ scripts/bash/create-new-feature.sh | 33 ++++++++++++++++++++++++++++-- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/scripts/bash/common.sh b/scripts/bash/common.sh index 747336a4a..a0b115e76 100644 --- a/scripts/bash/common.sh +++ b/scripts/bash/common.sh @@ -12,6 +12,29 @@ get_repo_root() { fi } +# Sanitize branch name for use as directory name +# Replaces filesystem-forbidden and problematic characters with safe alternatives +sanitize_branch_name() { + local branch="$1" + + # Replace problematic characters: + # / → - (prevents nesting on all platforms, Windows forbidden) + # \ → - (Windows forbidden) + # : → - (Windows forbidden, macOS translated) + # * → - (Windows forbidden, shell wildcard) + # ? → - (Windows forbidden, shell wildcard) + # " → - (Windows forbidden) + # < → - (Windows forbidden, shell redirect) + # > → - (Windows forbidden, shell redirect) + # | → - (Windows forbidden, shell pipe) + echo "$branch" | sed \ + -e 's/[\/\\:*?"<>|]/-/g' \ + -e 's/ */-/g' \ + -e 's/^[. -]*//' \ + -e 's/[. -]*$//' \ + -e 's/--*/-/g' +} + # Get current branch, with fallback for non-git repositories get_current_branch() { # First check if SPECIFY_FEATURE environment variable is set @@ -109,6 +132,14 @@ find_feature_dir_by_prefix() { local branch_name="$2" local specs_dir="$repo_root/specs" + # When using current branch, do exact match only (no prefix matching) + if [[ -n "${SPECIFY_USE_CURRENT_BRANCH:-}" ]]; then + # Sanitize branch name for filesystem compatibility + local sanitized_name=$(sanitize_branch_name "$branch_name") + echo "$specs_dir/$sanitized_name" + return + fi + # Extract numeric prefix from branch (e.g., "004" from "004-whatever") if [[ ! "$branch_name" =~ ^([0-9]{3})- ]]; then # If branch doesn't have numeric prefix, fall back to exact match diff --git a/scripts/bash/create-new-feature.sh b/scripts/bash/create-new-feature.sh index e50a0287d..c6bda8fb7 100644 --- a/scripts/bash/create-new-feature.sh +++ b/scripts/bash/create-new-feature.sh @@ -67,6 +67,29 @@ if [ -z "$FEATURE_DESCRIPTION" ]; then exit 1 fi +# Sanitize branch name for use as directory name +# Replaces filesystem-forbidden and problematic characters with safe alternatives +sanitize_branch_name() { + local branch="$1" + + # Replace problematic characters: + # / → - (prevents nesting on all platforms, Windows forbidden) + # \ → - (Windows forbidden) + # : → - (Windows forbidden, macOS translated) + # * → - (Windows forbidden, shell wildcard) + # ? → - (Windows forbidden, shell wildcard) + # " → - (Windows forbidden) + # < → - (Windows forbidden, shell redirect) + # > → - (Windows forbidden, shell redirect) + # | → - (Windows forbidden, shell pipe) + echo "$branch" | sed \ + -e 's/[\/\\:*?"<>|]/-/g' \ + -e 's/ */-/g' \ + -e 's/^[. -]*//' \ + -e 's/[. -]*$//' \ + -e 's/--*/-/g' +} + # Function to find the repository root by searching for existing project markers find_repo_root() { local dir="$1" @@ -220,9 +243,15 @@ if [[ -n "${SPECIFY_USE_CURRENT_BRANCH:-}" ]]; then >&2 echo "[specify] Error: Cannot determine current branch name" exit 1 fi - BRANCH_NAME="$branch_name_output" + # Sanitize branch name for filesystem compatibility + original_branch="$branch_name_output" + BRANCH_NAME=$(sanitize_branch_name "$branch_name_output") FEATURE_NUM="N/A" - >&2 echo "[specify] Using current branch: $BRANCH_NAME" + if [[ "$original_branch" != "$BRANCH_NAME" ]]; then + >&2 echo "[specify] Using current branch: $original_branch (sanitized to: $BRANCH_NAME)" + else + >&2 echo "[specify] Using current branch: $BRANCH_NAME" + fi else >&2 echo "[specify] Error: SPECIFY_USE_CURRENT_BRANCH requires a git repository" exit 1