Skip to content

Commit 4fae020

Browse files
committed
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
1 parent b9c1a1c commit 4fae020

2 files changed

Lines changed: 104 additions & 2 deletions

File tree

scripts/bash/create-new-feature.sh

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ set -e
55
JSON_MODE=false
66
SHORT_NAME=""
77
BRANCH_NUMBER=""
8+
NUMBER_EXPLICIT=false # true only when --number was explicitly passed by the caller
89
ARGS=()
910
i=1
1011
while [ $i -le $# ]; do
@@ -39,14 +40,26 @@ while [ $i -le $# ]; do
3940
exit 1
4041
fi
4142
BRANCH_NUMBER="$next_arg"
43+
# Validate --number is an integer in [1,999]; downstream tooling
44+
# expects a 3-digit prefix (^[0-9]{3}-) so out-of-range values
45+
# produce malformed branch names.
46+
if ! [[ "$next_arg" =~ ^[0-9]+$ ]]; then
47+
>&2 echo 'Error: --number must be an integer between 1 and 999'
48+
exit 1
49+
fi
50+
if (( next_arg < 1 || next_arg > 999 )); then
51+
>&2 echo 'Error: --number must be an integer between 1 and 999'
52+
exit 1
53+
fi
54+
NUMBER_EXPLICIT=true
4255
;;
4356
--help|-h)
4457
echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>"
4558
echo ""
4659
echo "Options:"
4760
echo " --json Output in JSON format"
4861
echo " --short-name <name> Provide a custom short name (2-4 words) for the branch"
49-
echo " --number N Specify branch number manually (overrides auto-detection)"
62+
echo " --number N Preferred branch number (auto-corrected if prefix already exists in specs or branches)"
5063
echo " --help, -h Show this help message"
5164
echo ""
5265
echo "Examples:"
@@ -266,6 +279,53 @@ fi
266279

267280
# Force base-10 interpretation to prevent octal conversion (e.g., 010 → 8 in octal, but should be 10 in decimal)
268281
FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))")
282+
283+
# ── Guardrail: auto-correct if --number was explicitly passed and the prefix
284+
# already exists in specs/ or as a git branch. Only fires on explicit --number
285+
# to preserve the existing auto-detection contract.
286+
# Fetches remotes once here; subsequent max-finding reuses local branch data
287+
# to avoid a second network round-trip.
288+
if [ "$NUMBER_EXPLICIT" = true ]; then
289+
# Check for conflict in spec directories (directories only)
290+
SPEC_CONFLICT=false
291+
while IFS= read -r spec_path; do
292+
if [ -d "$spec_path" ]; then
293+
SPEC_CONFLICT=true
294+
break
295+
fi
296+
done < <(compgen -G "$SPECS_DIR/${FEATURE_NUM}-*" 2>/dev/null)
297+
298+
# Check for conflict in git branches (local and remote)
299+
# Fetch once here so both conflict detection and recalculation use
300+
# up-to-date remote info without a second network call.
301+
BRANCH_CONFLICT=false
302+
if [ "$HAS_GIT" = true ]; then
303+
git fetch --all --prune >/dev/null 2>&1 || true
304+
if git branch -a 2>/dev/null | grep -qE "(^|[[:space:]])(remotes/[^/]+/)?${FEATURE_NUM}-"; then
305+
BRANCH_CONFLICT=true
306+
fi
307+
fi
308+
309+
if [ "$SPEC_CONFLICT" = true ] || [ "$BRANCH_CONFLICT" = true ]; then
310+
REQUESTED_NUM="$FEATURE_NUM"
311+
# Inline the max-finding using already-fetched local branch data
312+
# to avoid a second network round-trip.
313+
if [ "$HAS_GIT" = true ]; then
314+
HIGHEST_BRANCH=$(get_highest_from_branches)
315+
else
316+
HIGHEST_BRANCH=0
317+
fi
318+
HIGHEST_SPEC=$(get_highest_from_specs "$SPECS_DIR")
319+
if [ "$HIGHEST_SPEC" -gt "$HIGHEST_BRANCH" ]; then
320+
BRANCH_NUMBER=$((HIGHEST_SPEC + 1))
321+
else
322+
BRANCH_NUMBER=$((HIGHEST_BRANCH + 1))
323+
fi
324+
FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))")
325+
>&2 echo "⚠️ [specify] --number $REQUESTED_NUM conflicts with an existing spec dir or branch. Auto-corrected to $FEATURE_NUM."
326+
fi
327+
fi
328+
269329
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
270330

271331
# GitHub enforces a 244-byte limit on branch names

scripts/powershell/create-new-feature.ps1

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
param(
55
[switch]$Json,
66
[string]$ShortName,
7+
[ValidateRange(1, 999)]
78
[int]$Number = 0,
89
[switch]$Help,
910
[Parameter(ValueFromRemainingArguments = $true)]
@@ -18,7 +19,7 @@ if ($Help) {
1819
Write-Host "Options:"
1920
Write-Host " -Json Output in JSON format"
2021
Write-Host " -ShortName <name> Provide a custom short name (2-4 words) for the branch"
21-
Write-Host " -Number N Specify branch number manually (overrides auto-detection)"
22+
Write-Host " -Number N Preferred branch number (auto-corrected if prefix already exists in specs or branches)"
2223
Write-Host " -Help Show this help message"
2324
Write-Host ""
2425
Write-Host "Examples:"
@@ -213,6 +214,10 @@ if ($ShortName) {
213214
}
214215

215216
# Determine branch number
217+
# Track whether the caller explicitly passed a non-zero -Number so the guardrail
218+
# below only fires for explicit overrides, not for auto-detected numbers.
219+
# Exclude -Number 0 since 0 is the auto-detect sentinel and produces prefix "000".
220+
$numberExplicit = $PSBoundParameters.ContainsKey('Number') -and $Number -ne 0
216221
if ($Number -eq 0) {
217222
if ($hasGit) {
218223
# Check existing branches on remotes
@@ -224,6 +229,43 @@ if ($Number -eq 0) {
224229
}
225230

226231
$featureNum = ('{0:000}' -f $Number)
232+
233+
# ── Guardrail: auto-correct if -Number was explicitly passed and the prefix
234+
# already exists in specs/ or as a git branch. Only fires on explicit -Number
235+
# to preserve the existing auto-detection contract.
236+
# Reuses Get-NextBranchNumber, which fetches remotes and checks both
237+
# specs directories and all local/remote branches.
238+
if ($numberExplicit) {
239+
$requestedNum = $featureNum
240+
241+
# Check for conflict in spec directories
242+
$specConflict = (Get-ChildItem -Path $specsDir -Directory -ErrorAction SilentlyContinue |`
243+
Where-Object { $_.Name -match "^$featureNum-" }).Count -gt 0
244+
245+
# Check for conflict in git branches (local and remote)
246+
# Note: we do NOT fetch here — if a conflict is found, Get-NextBranchNumber
247+
# below will fetch exactly once before computing the corrected number.
248+
$branchConflict = $false
249+
if ($hasGit) {
250+
$allBranches = git branch -a 2>$null
251+
if ($LASTEXITCODE -eq 0) {
252+
$branchConflict = ($allBranches | Where-Object { $_ -match "(^|\s)(remotes/[^/]+/)?$featureNum-" }).Count -gt 0
253+
}
254+
}
255+
256+
if ($specConflict -or $branchConflict) {
257+
# Delegate to Get-NextBranchNumber, which fetches and computes
258+
# max(all specs, all branches) + 1 — same logic used by auto-detection.
259+
if ($hasGit) {
260+
$Number = Get-NextBranchNumber -SpecsDir $specsDir
261+
} else {
262+
$Number = (Get-HighestNumberFromSpecs -SpecsDir $specsDir) + 1
263+
}
264+
$featureNum = ('{0:000}' -f $Number)
265+
Write-Warning "[specify] -Number $requestedNum conflicts with an existing spec dir or branch. Auto-corrected to $featureNum."
266+
}
267+
}
268+
227269
$branchName = "$featureNum-$branchSuffix"
228270

229271
# GitHub enforces a 244-byte limit on branch names

0 commit comments

Comments
 (0)