From 4fae0209a77690f54db40f33e0f8560a44db1d9a Mon Sep 17 00:00:00 2001 From: Daniel Graham Date: Fri, 13 Mar 2026 12:54:21 -0400 Subject: [PATCH] fix: auto-correct prefix conflicts in create-new-feature scripts When --number (Bash) or -Number (PowerShell) is explicitly passed and the requested prefix already exists as a spec directory or git branch, auto-correct to the global max + 1 rather than failing or creating duplicate numbering. Changes: - Guard fires only on explicit --number/-Number (Bash: NUMBER_EXPLICIT flag; PowerShell: $PSBoundParameters.ContainsKey('Number') -and $Number -ne 0), leaving the auto-detection contract unchanged - Bash: validates --number is an integer in [1,999]; values outside this range are rejected with a clear error message since downstream tooling expects a 3-digit prefix - Conflict detection checks both specs/NNN-* directories (directories only) and all local/remote git branches - Single fetch: Bash fetches once before conflict detection and inlines max-finding from local branch data to avoid a second network call; PowerShell defers the fetch to Get-NextBranchNumber, which runs only when a conflict is actually found - Emits a warning to stderr when auto-correction occurs - Help text updated: --number/-Number is now "Preferred branch number (auto-corrected if prefix already exists in specs or branches)" - Full parity between Bash and PowerShell implementations --- scripts/bash/create-new-feature.sh | 62 ++++++++++++++++++++++- scripts/powershell/create-new-feature.ps1 | 44 +++++++++++++++- 2 files changed, 104 insertions(+), 2 deletions(-) diff --git a/scripts/bash/create-new-feature.sh b/scripts/bash/create-new-feature.sh index 725f84c85..3a861907e 100644 --- a/scripts/bash/create-new-feature.sh +++ b/scripts/bash/create-new-feature.sh @@ -5,6 +5,7 @@ set -e JSON_MODE=false SHORT_NAME="" BRANCH_NUMBER="" +NUMBER_EXPLICIT=false # true only when --number was explicitly passed by the caller ARGS=() i=1 while [ $i -le $# ]; do @@ -39,6 +40,18 @@ while [ $i -le $# ]; do exit 1 fi BRANCH_NUMBER="$next_arg" + # Validate --number is an integer in [1,999]; downstream tooling + # expects a 3-digit prefix (^[0-9]{3}-) so out-of-range values + # produce malformed branch names. + if ! [[ "$next_arg" =~ ^[0-9]+$ ]]; then + >&2 echo 'Error: --number must be an integer between 1 and 999' + exit 1 + fi + if (( next_arg < 1 || next_arg > 999 )); then + >&2 echo 'Error: --number must be an integer between 1 and 999' + exit 1 + fi + NUMBER_EXPLICIT=true ;; --help|-h) echo "Usage: $0 [--json] [--short-name ] [--number N] " @@ -46,7 +59,7 @@ while [ $i -le $# ]; do echo "Options:" echo " --json Output in JSON format" echo " --short-name Provide a custom short name (2-4 words) for the branch" - echo " --number N Specify branch number manually (overrides auto-detection)" + echo " --number N Preferred branch number (auto-corrected if prefix already exists in specs or branches)" echo " --help, -h Show this help message" echo "" echo "Examples:" @@ -266,6 +279,53 @@ fi # Force base-10 interpretation to prevent octal conversion (e.g., 010 → 8 in octal, but should be 10 in decimal) FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))") + +# ── Guardrail: auto-correct if --number was explicitly passed and the prefix +# already exists in specs/ or as a git branch. Only fires on explicit --number +# to preserve the existing auto-detection contract. +# Fetches remotes once here; subsequent max-finding reuses local branch data +# to avoid a second network round-trip. +if [ "$NUMBER_EXPLICIT" = true ]; then + # Check for conflict in spec directories (directories only) + SPEC_CONFLICT=false + while IFS= read -r spec_path; do + if [ -d "$spec_path" ]; then + SPEC_CONFLICT=true + break + fi + done < <(compgen -G "$SPECS_DIR/${FEATURE_NUM}-*" 2>/dev/null) + + # Check for conflict in git branches (local and remote) + # Fetch once here so both conflict detection and recalculation use + # up-to-date remote info without a second network call. + BRANCH_CONFLICT=false + if [ "$HAS_GIT" = true ]; then + git fetch --all --prune >/dev/null 2>&1 || true + if git branch -a 2>/dev/null | grep -qE "(^|[[:space:]])(remotes/[^/]+/)?${FEATURE_NUM}-"; then + BRANCH_CONFLICT=true + fi + fi + + if [ "$SPEC_CONFLICT" = true ] || [ "$BRANCH_CONFLICT" = true ]; then + REQUESTED_NUM="$FEATURE_NUM" + # Inline the max-finding using already-fetched local branch data + # to avoid a second network round-trip. + if [ "$HAS_GIT" = true ]; then + HIGHEST_BRANCH=$(get_highest_from_branches) + else + HIGHEST_BRANCH=0 + fi + HIGHEST_SPEC=$(get_highest_from_specs "$SPECS_DIR") + if [ "$HIGHEST_SPEC" -gt "$HIGHEST_BRANCH" ]; then + BRANCH_NUMBER=$((HIGHEST_SPEC + 1)) + else + BRANCH_NUMBER=$((HIGHEST_BRANCH + 1)) + fi + FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))") + >&2 echo "⚠️ [specify] --number $REQUESTED_NUM conflicts with an existing spec dir or branch. Auto-corrected to $FEATURE_NUM." + fi +fi + BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}" # GitHub enforces a 244-byte limit on branch names diff --git a/scripts/powershell/create-new-feature.ps1 b/scripts/powershell/create-new-feature.ps1 index 172b5bc7d..276778aca 100644 --- a/scripts/powershell/create-new-feature.ps1 +++ b/scripts/powershell/create-new-feature.ps1 @@ -4,6 +4,7 @@ param( [switch]$Json, [string]$ShortName, + [ValidateRange(1, 999)] [int]$Number = 0, [switch]$Help, [Parameter(ValueFromRemainingArguments = $true)] @@ -18,7 +19,7 @@ if ($Help) { Write-Host "Options:" Write-Host " -Json Output in JSON format" Write-Host " -ShortName Provide a custom short name (2-4 words) for the branch" - Write-Host " -Number N Specify branch number manually (overrides auto-detection)" + Write-Host " -Number N Preferred branch number (auto-corrected if prefix already exists in specs or branches)" Write-Host " -Help Show this help message" Write-Host "" Write-Host "Examples:" @@ -213,6 +214,10 @@ if ($ShortName) { } # Determine branch number +# Track whether the caller explicitly passed a non-zero -Number so the guardrail +# below only fires for explicit overrides, not for auto-detected numbers. +# Exclude -Number 0 since 0 is the auto-detect sentinel and produces prefix "000". +$numberExplicit = $PSBoundParameters.ContainsKey('Number') -and $Number -ne 0 if ($Number -eq 0) { if ($hasGit) { # Check existing branches on remotes @@ -224,6 +229,43 @@ if ($Number -eq 0) { } $featureNum = ('{0:000}' -f $Number) + +# ── Guardrail: auto-correct if -Number was explicitly passed and the prefix +# already exists in specs/ or as a git branch. Only fires on explicit -Number +# to preserve the existing auto-detection contract. +# Reuses Get-NextBranchNumber, which fetches remotes and checks both +# specs directories and all local/remote branches. +if ($numberExplicit) { + $requestedNum = $featureNum + + # Check for conflict in spec directories + $specConflict = (Get-ChildItem -Path $specsDir -Directory -ErrorAction SilentlyContinue |` + Where-Object { $_.Name -match "^$featureNum-" }).Count -gt 0 + + # Check for conflict in git branches (local and remote) + # Note: we do NOT fetch here — if a conflict is found, Get-NextBranchNumber + # below will fetch exactly once before computing the corrected number. + $branchConflict = $false + if ($hasGit) { + $allBranches = git branch -a 2>$null + if ($LASTEXITCODE -eq 0) { + $branchConflict = ($allBranches | Where-Object { $_ -match "(^|\s)(remotes/[^/]+/)?$featureNum-" }).Count -gt 0 + } + } + + if ($specConflict -or $branchConflict) { + # Delegate to Get-NextBranchNumber, which fetches and computes + # max(all specs, all branches) + 1 — same logic used by auto-detection. + if ($hasGit) { + $Number = Get-NextBranchNumber -SpecsDir $specsDir + } else { + $Number = (Get-HighestNumberFromSpecs -SpecsDir $specsDir) + 1 + } + $featureNum = ('{0:000}' -f $Number) + Write-Warning "[specify] -Number $requestedNum conflicts with an existing spec dir or branch. Auto-corrected to $featureNum." + } +} + $branchName = "$featureNum-$branchSuffix" # GitHub enforces a 244-byte limit on branch names