Skip to content

Commit e6d6f3c

Browse files
authored
Merge pull request #1019 from sigent-amazon/feature/check-remote-branches-for-numbering
Check remote branches to prevent duplicate branch numbers
2 parents 926836e + 598148c commit e6d6f3c

File tree

3 files changed

+192
-38
lines changed

3 files changed

+192
-38
lines changed

scripts/bash/create-new-feature.sh

Lines changed: 71 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ set -e
44

55
JSON_MODE=false
66
SHORT_NAME=""
7+
BRANCH_NUMBER=""
78
ARGS=()
89
i=1
910
while [ $i -le $# ]; do
@@ -26,17 +27,31 @@ while [ $i -le $# ]; do
2627
fi
2728
SHORT_NAME="$next_arg"
2829
;;
30+
--number)
31+
if [ $((i + 1)) -gt $# ]; then
32+
echo 'Error: --number requires a value' >&2
33+
exit 1
34+
fi
35+
i=$((i + 1))
36+
next_arg="${!i}"
37+
if [[ "$next_arg" == --* ]]; then
38+
echo 'Error: --number requires a value' >&2
39+
exit 1
40+
fi
41+
BRANCH_NUMBER="$next_arg"
42+
;;
2943
--help|-h)
30-
echo "Usage: $0 [--json] [--short-name <name>] <feature_description>"
44+
echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>"
3145
echo ""
3246
echo "Options:"
3347
echo " --json Output in JSON format"
3448
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)"
3550
echo " --help, -h Show this help message"
3651
echo ""
3752
echo "Examples:"
3853
echo " $0 'Add user authentication system' --short-name 'user-auth'"
39-
echo " $0 'Implement OAuth2 integration for API'"
54+
echo " $0 'Implement OAuth2 integration for API' --number 5"
4055
exit 0
4156
;;
4257
*)
@@ -48,7 +63,7 @@ done
4863

4964
FEATURE_DESCRIPTION="${ARGS[*]}"
5065
if [ -z "$FEATURE_DESCRIPTION" ]; then
51-
echo "Usage: $0 [--json] [--short-name <name>] <feature_description>" >&2
66+
echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>" >&2
5267
exit 1
5368
fi
5469

@@ -65,6 +80,37 @@ find_repo_root() {
6580
return 1
6681
}
6782

83+
# Function to check existing branches (local and remote) and return next available number
84+
check_existing_branches() {
85+
local short_name="$1"
86+
87+
# Fetch all remotes to get latest branch info (suppress errors if no remotes)
88+
git fetch --all --prune 2>/dev/null || true
89+
90+
# Find all branches matching the pattern using git ls-remote (more reliable)
91+
local remote_branches=$(git ls-remote --heads origin 2>/dev/null | grep -E "refs/heads/[0-9]+-${short_name}$" | sed 's/.*\/\([0-9]*\)-.*/\1/' | sort -n)
92+
93+
# Also check local branches
94+
local local_branches=$(git branch 2>/dev/null | grep -E "^[* ]*[0-9]+-${short_name}$" | sed 's/^[* ]*//' | sed 's/-.*//' | sort -n)
95+
96+
# Check specs directory as well
97+
local spec_dirs=""
98+
if [ -d "$SPECS_DIR" ]; then
99+
spec_dirs=$(find "$SPECS_DIR" -maxdepth 1 -type d -name "[0-9]*-${short_name}" 2>/dev/null | xargs -n1 basename 2>/dev/null | sed 's/-.*//' | sort -n)
100+
fi
101+
102+
# Combine all sources and get the highest number
103+
local max_num=0
104+
for num in $remote_branches $local_branches $spec_dirs; do
105+
if [ "$num" -gt "$max_num" ]; then
106+
max_num=$num
107+
fi
108+
done
109+
110+
# Return next number
111+
echo $((max_num + 1))
112+
}
113+
68114
# Resolve repository root. Prefer git information when available, but fall back
69115
# to searching for repository markers so the workflow still functions in repositories that
70116
# were initialised with --no-git.
@@ -87,20 +133,6 @@ cd "$REPO_ROOT"
87133
SPECS_DIR="$REPO_ROOT/specs"
88134
mkdir -p "$SPECS_DIR"
89135

90-
HIGHEST=0
91-
if [ -d "$SPECS_DIR" ]; then
92-
for dir in "$SPECS_DIR"/*; do
93-
[ -d "$dir" ] || continue
94-
dirname=$(basename "$dir")
95-
number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0")
96-
number=$((10#$number))
97-
if [ "$number" -gt "$HIGHEST" ]; then HIGHEST=$number; fi
98-
done
99-
fi
100-
101-
NEXT=$((HIGHEST + 1))
102-
FEATURE_NUM=$(printf "%03d" "$NEXT")
103-
104136
# Function to generate branch name with stop word filtering and length filtering
105137
generate_branch_name() {
106138
local description="$1"
@@ -157,6 +189,28 @@ else
157189
BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION")
158190
fi
159191

192+
# Determine branch number
193+
if [ -z "$BRANCH_NUMBER" ]; then
194+
if [ "$HAS_GIT" = true ]; then
195+
# Check existing branches on remotes
196+
BRANCH_NUMBER=$(check_existing_branches "$BRANCH_SUFFIX")
197+
else
198+
# Fall back to local directory check
199+
HIGHEST=0
200+
if [ -d "$SPECS_DIR" ]; then
201+
for dir in "$SPECS_DIR"/*; do
202+
[ -d "$dir" ] || continue
203+
dirname=$(basename "$dir")
204+
number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0")
205+
number=$((10#$number))
206+
if [ "$number" -gt "$HIGHEST" ]; then HIGHEST=$number; fi
207+
done
208+
fi
209+
BRANCH_NUMBER=$((HIGHEST + 1))
210+
fi
211+
fi
212+
213+
FEATURE_NUM=$(printf "%03d" "$BRANCH_NUMBER")
160214
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
161215

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

scripts/powershell/create-new-feature.ps1

Lines changed: 93 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
param(
55
[switch]$Json,
66
[string]$ShortName,
7+
[int]$Number = 0,
78
[switch]$Help,
89
[Parameter(ValueFromRemainingArguments = $true)]
910
[string[]]$FeatureDescription
@@ -12,11 +13,12 @@ $ErrorActionPreference = 'Stop'
1213

1314
# Show help if requested
1415
if ($Help) {
15-
Write-Host "Usage: ./create-new-feature.ps1 [-Json] [-ShortName <name>] <feature description>"
16+
Write-Host "Usage: ./create-new-feature.ps1 [-Json] [-ShortName <name>] [-Number N] <feature description>"
1617
Write-Host ""
1718
Write-Host "Options:"
1819
Write-Host " -Json Output in JSON format"
1920
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)"
2022
Write-Host " -Help Show this help message"
2123
Write-Host ""
2224
Write-Host "Examples:"
@@ -56,6 +58,75 @@ function Find-RepositoryRoot {
5658
$current = $parent
5759
}
5860
}
61+
62+
function Get-NextBranchNumber {
63+
param(
64+
[string]$ShortName,
65+
[string]$SpecsDir
66+
)
67+
68+
# Fetch all remotes to get latest branch info (suppress errors if no remotes)
69+
try {
70+
git fetch --all --prune 2>$null | Out-Null
71+
} catch {
72+
# Ignore fetch errors
73+
}
74+
75+
# Find remote branches matching the pattern using git ls-remote
76+
$remoteBranches = @()
77+
try {
78+
$remoteRefs = git ls-remote --heads origin 2>$null
79+
if ($remoteRefs) {
80+
$remoteBranches = $remoteRefs | Where-Object { $_ -match "refs/heads/(\d+)-$([regex]::Escape($ShortName))$" } | ForEach-Object {
81+
if ($_ -match "refs/heads/(\d+)-") {
82+
[int]$matches[1]
83+
}
84+
}
85+
}
86+
} catch {
87+
# Ignore errors
88+
}
89+
90+
# Check local branches
91+
$localBranches = @()
92+
try {
93+
$allBranches = git branch 2>$null
94+
if ($allBranches) {
95+
$localBranches = $allBranches | Where-Object { $_ -match "^\*?\s*(\d+)-$([regex]::Escape($ShortName))$" } | ForEach-Object {
96+
if ($_ -match "(\d+)-") {
97+
[int]$matches[1]
98+
}
99+
}
100+
}
101+
} catch {
102+
# Ignore errors
103+
}
104+
105+
# Check specs directory
106+
$specDirs = @()
107+
if (Test-Path $SpecsDir) {
108+
try {
109+
$specDirs = Get-ChildItem -Path $SpecsDir -Directory | Where-Object { $_.Name -match "^(\d+)-$([regex]::Escape($ShortName))$" } | ForEach-Object {
110+
if ($_.Name -match "^(\d+)-") {
111+
[int]$matches[1]
112+
}
113+
}
114+
} catch {
115+
# Ignore errors
116+
}
117+
}
118+
119+
# Combine all sources and get the highest number
120+
$maxNum = 0
121+
foreach ($num in ($remoteBranches + $localBranches + $specDirs)) {
122+
if ($num -gt $maxNum) {
123+
$maxNum = $num
124+
}
125+
}
126+
127+
# Return next number
128+
return $maxNum + 1
129+
}
59130
$fallbackRoot = (Find-RepositoryRoot -StartDir $PSScriptRoot)
60131
if (-not $fallbackRoot) {
61132
Write-Error "Error: Could not determine repository root. Please run this script from within the repository."
@@ -79,18 +150,6 @@ Set-Location $repoRoot
79150
$specsDir = Join-Path $repoRoot 'specs'
80151
New-Item -ItemType Directory -Path $specsDir -Force | Out-Null
81152

82-
$highest = 0
83-
if (Test-Path $specsDir) {
84-
Get-ChildItem -Path $specsDir -Directory | ForEach-Object {
85-
if ($_.Name -match '^(\d{3})') {
86-
$num = [int]$matches[1]
87-
if ($num -gt $highest) { $highest = $num }
88-
}
89-
}
90-
}
91-
$next = $highest + 1
92-
$featureNum = ('{0:000}' -f $next)
93-
94153
# Function to generate branch name with stop word filtering and length filtering
95154
function Get-BranchName {
96155
param([string]$Description)
@@ -145,6 +204,27 @@ if ($ShortName) {
145204
$branchSuffix = Get-BranchName -Description $featureDesc
146205
}
147206

207+
# Determine branch number
208+
if ($Number -eq 0) {
209+
if ($hasGit) {
210+
# Check existing branches on remotes
211+
$Number = Get-NextBranchNumber -ShortName $branchSuffix -SpecsDir $specsDir
212+
} else {
213+
# Fall back to local directory check
214+
$highest = 0
215+
if (Test-Path $specsDir) {
216+
Get-ChildItem -Path $specsDir -Directory | ForEach-Object {
217+
if ($_.Name -match '^(\d{3})') {
218+
$num = [int]$matches[1]
219+
if ($num -gt $highest) { $highest = $num }
220+
}
221+
}
222+
}
223+
$Number = $highest + 1
224+
}
225+
}
226+
227+
$featureNum = ('{0:000}' -f $Number)
148228
$branchName = "$featureNum-$branchSuffix"
149229

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

templates/commands/specify.md

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,36 @@ Given that feature description, do this:
3131
- "Create a dashboard for analytics" → "analytics-dashboard"
3232
- "Fix payment processing timeout bug" → "fix-payment-timeout"
3333

34-
2. Run the script `{SCRIPT}` from repo root **with the short-name argument** and parse its JSON output for BRANCH_NAME and SPEC_FILE. All file paths must be absolute.
35-
34+
2. **Check for existing branches before creating new one**:
35+
36+
a. First, fetch all remote branches to ensure we have the latest information:
37+
```bash
38+
git fetch --all --prune
39+
```
40+
41+
b. Find the highest feature number across all sources for the short-name:
42+
- Remote branches: `git ls-remote --heads origin | grep -E 'refs/heads/[0-9]+-<short-name>$'`
43+
- Local branches: `git branch | grep -E '^[* ]*[0-9]+-<short-name>$'`
44+
- Specs directories: Check for directories matching `specs/[0-9]+-<short-name>`
45+
46+
c. Determine the next available number:
47+
- Extract all numbers from all three sources
48+
- Find the highest number N
49+
- Use N+1 for the new branch number
50+
51+
d. Run the script `{SCRIPT}` with the calculated number and short-name:
52+
- Pass `--number N+1` and `--short-name "your-short-name"` along with the feature description
53+
- Bash example: `{SCRIPT} --json --number 5 --short-name "user-auth" "Add user authentication"`
54+
- PowerShell example: `{SCRIPT} -Json -Number 5 -ShortName "user-auth" "Add user authentication"`
55+
3656
**IMPORTANT**:
37-
38-
- Append the short-name argument to the `{SCRIPT}` command with the 2-4 word short name you created in step 1. Keep the feature description as the final argument.
39-
- Bash example: `--short-name "your-generated-short-name" "Feature description here"`
40-
- PowerShell example: `-ShortName "your-generated-short-name" "Feature description here"`
41-
- For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot")
42-
- You must only ever run this script once
57+
- Check all three sources (remote branches, local branches, specs directories) to find the highest number
58+
- Only match branches/directories with the exact short-name pattern
59+
- If no existing branches/directories found with this short-name, start with number 1
60+
- You must only ever run this script once per feature
4361
- The JSON is provided in the terminal as output - always refer to it to get the actual content you're looking for
62+
- The JSON output will contain BRANCH_NAME and SPEC_FILE paths
63+
- For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot")
4464
4565
3. Load `templates/spec-template.md` to understand required sections.
4666

0 commit comments

Comments
 (0)