Skip to content

Commit c76d3de

Browse files
leoxiao2012claude
andcommitted
fix(git): keep specs dir flat when --prefix contains slashes
Separate BRANCH_NAME (git ref, may include prefix like feature/) from FEATURE_DIR_NAME (directory-safe, no prefix) so that specs/ paths and sequential-number detection remain correct when a branch prefix is used. Also adds prefix validation (reject embedded slashes, trim whitespace) and 31 new pytest tests covering --prefix behavior across bash, ps1, and extension scripts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 55ff21f commit c76d3de

5 files changed

Lines changed: 436 additions & 21 deletions

File tree

extensions/git/scripts/bash/create-new-feature.sh

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,20 @@ while [ $i -le $# ]; do
103103
i=$((i + 1))
104104
done
105105

106-
# Auto-append '/' if branch prefix is non-empty and doesn't end with '/'
107-
if [ -n "$BRANCH_PREFIX" ] && [[ ! "$BRANCH_PREFIX" =~ /$ ]]; then
108-
BRANCH_PREFIX="$BRANCH_PREFIX/"
106+
# Validate and normalize branch prefix
107+
if [ -n "$BRANCH_PREFIX" ]; then
108+
BRANCH_PREFIX=$(echo "$BRANCH_PREFIX" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
109+
if [ -z "$BRANCH_PREFIX" ]; then
110+
echo 'Error: --prefix cannot be empty or whitespace' >&2
111+
exit 1
112+
fi
113+
# Strip optional trailing '/' before checking for embedded slashes
114+
_check_prefix="${BRANCH_PREFIX%/}"
115+
if [[ "$_check_prefix" == */* ]]; then
116+
echo 'Error: --prefix must be a single segment (no embedded slashes); e.g. "feature", "bugfix"' >&2
117+
exit 1
118+
fi
119+
BRANCH_PREFIX="$_check_prefix/"
109120
fi
110121

111122
FEATURE_DESCRIPTION="${ARGS[*]}"

extensions/git/scripts/powershell/create-new-feature.ps1

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,19 @@ if (-not $FeatureDescription -or $FeatureDescription.Count -eq 0) {
4545

4646
$featureDesc = ($FeatureDescription -join ' ').Trim()
4747

48-
# Auto-append '/' if branch prefix is non-empty and doesn't end with '/'
49-
if ($Prefix -and -not $Prefix.EndsWith('/')) {
50-
$Prefix = "$Prefix/"
48+
# Validate and normalize branch prefix
49+
if ($Prefix) {
50+
$Prefix = $Prefix.Trim()
51+
if ([string]::IsNullOrWhiteSpace($Prefix)) {
52+
Write-Error "Error: -Prefix cannot be empty or whitespace"
53+
exit 1
54+
}
55+
$checkPrefix = $Prefix.TrimEnd('/')
56+
if ($checkPrefix.Contains('/')) {
57+
Write-Error "Error: -Prefix must be a single segment (no embedded slashes); e.g. 'feature', 'bugfix'"
58+
exit 1
59+
}
60+
$Prefix = "$checkPrefix/"
5161
}
5262

5363
if ([string]::IsNullOrWhiteSpace($featureDesc)) {

scripts/bash/create-new-feature.sh

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,20 @@ while [ $i -le $# ]; do
9393
i=$((i + 1))
9494
done
9595

96-
# Auto-append '/' if branch prefix is non-empty and doesn't end with '/'
97-
if [ -n "$BRANCH_PREFIX" ] && [[ ! "$BRANCH_PREFIX" =~ /$ ]]; then
98-
BRANCH_PREFIX="$BRANCH_PREFIX/"
96+
# Validate and normalize branch prefix
97+
if [ -n "$BRANCH_PREFIX" ]; then
98+
BRANCH_PREFIX=$(echo "$BRANCH_PREFIX" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
99+
if [ -z "$BRANCH_PREFIX" ]; then
100+
echo 'Error: --prefix cannot be empty or whitespace' >&2
101+
exit 1
102+
fi
103+
# Strip optional trailing '/' before checking for embedded slashes
104+
_check_prefix="${BRANCH_PREFIX%/}"
105+
if [[ "$_check_prefix" == */* ]]; then
106+
echo 'Error: --prefix must be a single segment (no embedded slashes); e.g. "feature", "bugfix"' >&2
107+
exit 1
108+
fi
109+
BRANCH_PREFIX="$_check_prefix/"
99110
fi
100111

101112
FEATURE_DESCRIPTION="${ARGS[*]}"
@@ -325,6 +336,9 @@ else
325336
BRANCH_NAME="${BRANCH_PREFIX}${FEATURE_NUM}-${BRANCH_SUFFIX}"
326337
fi
327338

339+
# Directory-safe name (no prefix slash) for specs/ paths
340+
FEATURE_DIR_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
341+
328342
# GitHub enforces a 244-byte limit on branch names
329343
# Validate and truncate if necessary
330344
MAX_BRANCH_LENGTH=244
@@ -333,21 +347,22 @@ if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then
333347
# Account for prefix length: timestamp (15) + hyphen (1) = 16, or sequential (3) + hyphen (1) = 4
334348
PREFIX_LENGTH=$(( ${#BRANCH_PREFIX} + ${#FEATURE_NUM} + 1 ))
335349
MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - PREFIX_LENGTH))
336-
350+
337351
# Truncate suffix at word boundary if possible
338352
TRUNCATED_SUFFIX=$(echo "$BRANCH_SUFFIX" | cut -c1-$MAX_SUFFIX_LENGTH)
339353
# Remove trailing hyphen if truncation created one
340354
TRUNCATED_SUFFIX=$(echo "$TRUNCATED_SUFFIX" | sed 's/-$//')
341-
355+
342356
ORIGINAL_BRANCH_NAME="$BRANCH_NAME"
343357
BRANCH_NAME="${BRANCH_PREFIX}${FEATURE_NUM}-${TRUNCATED_SUFFIX}"
344-
358+
FEATURE_DIR_NAME="${FEATURE_NUM}-${TRUNCATED_SUFFIX}"
359+
345360
>&2 echo "[specify] Warning: Branch name exceeded GitHub's 244-byte limit"
346361
>&2 echo "[specify] Original: $ORIGINAL_BRANCH_NAME (${#ORIGINAL_BRANCH_NAME} bytes)"
347362
>&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)"
348363
fi
349364

350-
FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
365+
FEATURE_DIR="$SPECS_DIR/$FEATURE_DIR_NAME"
351366
SPEC_FILE="$FEATURE_DIR/spec.md"
352367

353368
if [ "$DRY_RUN" != true ]; then
@@ -403,7 +418,7 @@ if [ "$DRY_RUN" != true ]; then
403418
fi
404419

405420
# Inform the user how to persist the feature variable in their own shell
406-
printf '# To persist: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME" >&2
421+
printf '# To persist: export SPECIFY_FEATURE=%q\n' "$FEATURE_DIR_NAME" >&2
407422
fi
408423

409424
if $JSON_MODE; then
@@ -433,6 +448,6 @@ else
433448
echo "SPEC_FILE: $SPEC_FILE"
434449
echo "FEATURE_NUM: $FEATURE_NUM"
435450
if [ "$DRY_RUN" != true ]; then
436-
printf '# To persist in your shell: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME"
451+
printf '# To persist in your shell: export SPECIFY_FEATURE=%q\n' "$FEATURE_DIR_NAME"
437452
fi
438453
fi

scripts/powershell/create-new-feature.ps1

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,19 @@ if (-not $FeatureDescription -or $FeatureDescription.Count -eq 0) {
4545

4646
$featureDesc = ($FeatureDescription -join ' ').Trim()
4747

48-
# Auto-append '/' if branch prefix is non-empty and doesn't end with '/'
49-
if ($Prefix -and -not $Prefix.EndsWith('/')) {
50-
$Prefix = "$Prefix/"
48+
# Validate and normalize branch prefix
49+
if ($Prefix) {
50+
$Prefix = $Prefix.Trim()
51+
if ([string]::IsNullOrWhiteSpace($Prefix)) {
52+
Write-Error "Error: -Prefix cannot be empty or whitespace"
53+
exit 1
54+
}
55+
$checkPrefix = $Prefix.TrimEnd('/')
56+
if ($checkPrefix.Contains('/')) {
57+
Write-Error "Error: -Prefix must be a single segment (no embedded slashes); e.g. 'feature', 'bugfix'"
58+
exit 1
59+
}
60+
$Prefix = "$checkPrefix/"
5161
}
5262

5363
# Validate description is not empty after trimming (e.g., user passed only whitespace)
@@ -276,6 +286,9 @@ if ($Timestamp) {
276286
$branchName = "$Prefix$featureNum-$branchSuffix"
277287
}
278288

289+
# Directory-safe name (no prefix slash) for specs/ paths
290+
$featureDirName = "$featureNum-$branchSuffix"
291+
279292
# GitHub enforces a 244-byte limit on branch names
280293
# Validate and truncate if necessary
281294
$maxBranchLength = 244
@@ -292,13 +305,14 @@ if ($branchName.Length -gt $maxBranchLength) {
292305

293306
$originalBranchName = $branchName
294307
$branchName = "$Prefix$featureNum-$truncatedSuffix"
308+
$featureDirName = "$featureNum-$truncatedSuffix"
295309

296310
Write-Warning "[specify] Branch name exceeded GitHub's 244-byte limit"
297311
Write-Warning "[specify] Original: $originalBranchName ($($originalBranchName.Length) bytes)"
298312
Write-Warning "[specify] Truncated to: $branchName ($($branchName.Length) bytes)"
299313
}
300314

301-
$featureDir = Join-Path $specsDir $branchName
315+
$featureDir = Join-Path $specsDir $featureDirName
302316
$specFile = Join-Path $featureDir 'spec.md'
303317

304318
if (-not $DryRun) {
@@ -368,7 +382,7 @@ if (-not $DryRun) {
368382
}
369383

370384
# Set the SPECIFY_FEATURE environment variable for the current session
371-
$env:SPECIFY_FEATURE = $branchName
385+
$env:SPECIFY_FEATURE = $featureDirName
372386
}
373387

374388
if ($Json) {
@@ -388,6 +402,6 @@ if ($Json) {
388402
Write-Output "FEATURE_NUM: $featureNum"
389403
Write-Output "HAS_GIT: $hasGit"
390404
if (-not $DryRun) {
391-
Write-Output "SPECIFY_FEATURE environment variable set to: $branchName"
405+
Write-Output "SPECIFY_FEATURE environment variable set to: $featureDirName"
392406
}
393407
}

0 commit comments

Comments
 (0)