mirror of
https://github.com/github/spec-kit.git
synced 2026-02-02 05:53:36 +00:00
feat: check remote branches to prevent duplicate branch numbers
- Add --number parameter to create-new-feature scripts (bash & PowerShell) - Add check_existing_branches() function to fetch and scan remote branches - Update branch numbering logic to check remotes before creating new branches - Update /speckit.specify command to document remote branch checking workflow - Prevents duplicate branch numbers when branches exist on remotes but not locally - Maintains backward compatibility with existing workflows - Falls back to local directory scanning when Git is not available
This commit is contained in:
@@ -4,6 +4,7 @@ set -e
|
|||||||
|
|
||||||
JSON_MODE=false
|
JSON_MODE=false
|
||||||
SHORT_NAME=""
|
SHORT_NAME=""
|
||||||
|
BRANCH_NUMBER=""
|
||||||
ARGS=()
|
ARGS=()
|
||||||
i=1
|
i=1
|
||||||
while [ $i -le $# ]; do
|
while [ $i -le $# ]; do
|
||||||
@@ -26,17 +27,31 @@ while [ $i -le $# ]; do
|
|||||||
fi
|
fi
|
||||||
SHORT_NAME="$next_arg"
|
SHORT_NAME="$next_arg"
|
||||||
;;
|
;;
|
||||||
|
--number)
|
||||||
|
if [ $((i + 1)) -gt $# ]; then
|
||||||
|
echo 'Error: --number requires a value' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
i=$((i + 1))
|
||||||
|
next_arg="${!i}"
|
||||||
|
if [[ "$next_arg" == --* ]]; then
|
||||||
|
echo 'Error: --number requires a value' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
BRANCH_NUMBER="$next_arg"
|
||||||
|
;;
|
||||||
--help|-h)
|
--help|-h)
|
||||||
echo "Usage: $0 [--json] [--short-name <name>] <feature_description>"
|
echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Options:"
|
echo "Options:"
|
||||||
echo " --json Output in JSON format"
|
echo " --json Output in JSON format"
|
||||||
echo " --short-name <name> Provide a custom short name (2-4 words) for the branch"
|
echo " --short-name <name> Provide a custom short name (2-4 words) for the branch"
|
||||||
|
echo " --number N Specify branch number manually (overrides auto-detection)"
|
||||||
echo " --help, -h Show this help message"
|
echo " --help, -h Show this help message"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Examples:"
|
echo "Examples:"
|
||||||
echo " $0 'Add user authentication system' --short-name 'user-auth'"
|
echo " $0 'Add user authentication system' --short-name 'user-auth'"
|
||||||
echo " $0 'Implement OAuth2 integration for API'"
|
echo " $0 'Implement OAuth2 integration for API' --number 5"
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
@@ -48,7 +63,7 @@ done
|
|||||||
|
|
||||||
FEATURE_DESCRIPTION="${ARGS[*]}"
|
FEATURE_DESCRIPTION="${ARGS[*]}"
|
||||||
if [ -z "$FEATURE_DESCRIPTION" ]; then
|
if [ -z "$FEATURE_DESCRIPTION" ]; then
|
||||||
echo "Usage: $0 [--json] [--short-name <name>] <feature_description>" >&2
|
echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -65,6 +80,28 @@ find_repo_root() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Function to check existing branches (local and remote) and return next available number
|
||||||
|
check_existing_branches() {
|
||||||
|
local short_name="$1"
|
||||||
|
|
||||||
|
# Fetch all remotes to get latest branch info (suppress errors if no remotes)
|
||||||
|
git fetch --all --prune 2>/dev/null || true
|
||||||
|
|
||||||
|
# Find all branches matching the pattern (local and remote)
|
||||||
|
local branches=$(git branch -a 2>/dev/null | grep -E "feature/[0-9]+-${short_name}$" | sed 's/.*feature\///' | sed "s/-${short_name}$//" | sort -n)
|
||||||
|
|
||||||
|
# Get the highest number
|
||||||
|
local max_num=0
|
||||||
|
for num in $branches; do
|
||||||
|
if [ "$num" -gt "$max_num" ]; then
|
||||||
|
max_num=$num
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Return next number
|
||||||
|
echo $((max_num + 1))
|
||||||
|
}
|
||||||
|
|
||||||
# Resolve repository root. Prefer git information when available, but fall back
|
# Resolve repository root. Prefer git information when available, but fall back
|
||||||
# to searching for repository markers so the workflow still functions in repositories that
|
# to searching for repository markers so the workflow still functions in repositories that
|
||||||
# were initialised with --no-git.
|
# were initialised with --no-git.
|
||||||
@@ -87,20 +124,6 @@ cd "$REPO_ROOT"
|
|||||||
SPECS_DIR="$REPO_ROOT/specs"
|
SPECS_DIR="$REPO_ROOT/specs"
|
||||||
mkdir -p "$SPECS_DIR"
|
mkdir -p "$SPECS_DIR"
|
||||||
|
|
||||||
HIGHEST=0
|
|
||||||
if [ -d "$SPECS_DIR" ]; then
|
|
||||||
for dir in "$SPECS_DIR"/*; do
|
|
||||||
[ -d "$dir" ] || continue
|
|
||||||
dirname=$(basename "$dir")
|
|
||||||
number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0")
|
|
||||||
number=$((10#$number))
|
|
||||||
if [ "$number" -gt "$HIGHEST" ]; then HIGHEST=$number; fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
NEXT=$((HIGHEST + 1))
|
|
||||||
FEATURE_NUM=$(printf "%03d" "$NEXT")
|
|
||||||
|
|
||||||
# Function to generate branch name with stop word filtering and length filtering
|
# Function to generate branch name with stop word filtering and length filtering
|
||||||
generate_branch_name() {
|
generate_branch_name() {
|
||||||
local description="$1"
|
local description="$1"
|
||||||
@@ -157,6 +180,28 @@ else
|
|||||||
BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION")
|
BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Determine branch number
|
||||||
|
if [ -z "$BRANCH_NUMBER" ]; then
|
||||||
|
if [ "$HAS_GIT" = true ]; then
|
||||||
|
# Check existing branches on remotes
|
||||||
|
BRANCH_NUMBER=$(check_existing_branches "$BRANCH_SUFFIX")
|
||||||
|
else
|
||||||
|
# Fall back to local directory check
|
||||||
|
HIGHEST=0
|
||||||
|
if [ -d "$SPECS_DIR" ]; then
|
||||||
|
for dir in "$SPECS_DIR"/*; do
|
||||||
|
[ -d "$dir" ] || continue
|
||||||
|
dirname=$(basename "$dir")
|
||||||
|
number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0")
|
||||||
|
number=$((10#$number))
|
||||||
|
if [ "$number" -gt "$HIGHEST" ]; then HIGHEST=$number; fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
BRANCH_NUMBER=$((HIGHEST + 1))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
FEATURE_NUM=$(printf "%03d" "$BRANCH_NUMBER")
|
||||||
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
|
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
|
||||||
|
|
||||||
# GitHub enforces a 244-byte limit on branch names
|
# GitHub enforces a 244-byte limit on branch names
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
param(
|
param(
|
||||||
[switch]$Json,
|
[switch]$Json,
|
||||||
[string]$ShortName,
|
[string]$ShortName,
|
||||||
|
[int]$Number = 0,
|
||||||
[switch]$Help,
|
[switch]$Help,
|
||||||
[Parameter(ValueFromRemainingArguments = $true)]
|
[Parameter(ValueFromRemainingArguments = $true)]
|
||||||
[string[]]$FeatureDescription
|
[string[]]$FeatureDescription
|
||||||
@@ -12,11 +13,12 @@ $ErrorActionPreference = 'Stop'
|
|||||||
|
|
||||||
# Show help if requested
|
# Show help if requested
|
||||||
if ($Help) {
|
if ($Help) {
|
||||||
Write-Host "Usage: ./create-new-feature.ps1 [-Json] [-ShortName <name>] <feature description>"
|
Write-Host "Usage: ./create-new-feature.ps1 [-Json] [-ShortName <name>] [-Number N] <feature description>"
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "Options:"
|
Write-Host "Options:"
|
||||||
Write-Host " -Json Output in JSON format"
|
Write-Host " -Json Output in JSON format"
|
||||||
Write-Host " -ShortName <name> Provide a custom short name (2-4 words) for the branch"
|
Write-Host " -ShortName <name> Provide a custom short name (2-4 words) for the branch"
|
||||||
|
Write-Host " -Number N Specify branch number manually (overrides auto-detection)"
|
||||||
Write-Host " -Help Show this help message"
|
Write-Host " -Help Show this help message"
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "Examples:"
|
Write-Host "Examples:"
|
||||||
@@ -56,6 +58,45 @@ function Find-RepositoryRoot {
|
|||||||
$current = $parent
|
$current = $parent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Get-NextBranchNumber {
|
||||||
|
param(
|
||||||
|
[string]$ShortName
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fetch all remotes to get latest branch info (suppress errors if no remotes)
|
||||||
|
try {
|
||||||
|
git fetch --all --prune 2>$null | Out-Null
|
||||||
|
} catch {
|
||||||
|
# Ignore fetch errors
|
||||||
|
}
|
||||||
|
|
||||||
|
# Find all branches matching the pattern (local and remote)
|
||||||
|
$branches = @()
|
||||||
|
try {
|
||||||
|
$allBranches = git branch -a 2>$null
|
||||||
|
if ($allBranches) {
|
||||||
|
$branches = $allBranches | Where-Object { $_ -match "feature/(\d+)-$([regex]::Escape($ShortName))$" } | ForEach-Object {
|
||||||
|
if ($_ -match "feature/(\d+)-") {
|
||||||
|
[int]$matches[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
# Ignore errors
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get the highest number
|
||||||
|
$maxNum = 0
|
||||||
|
foreach ($num in $branches) {
|
||||||
|
if ($num -gt $maxNum) {
|
||||||
|
$maxNum = $num
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Return next number
|
||||||
|
return $maxNum + 1
|
||||||
|
}
|
||||||
$fallbackRoot = (Find-RepositoryRoot -StartDir $PSScriptRoot)
|
$fallbackRoot = (Find-RepositoryRoot -StartDir $PSScriptRoot)
|
||||||
if (-not $fallbackRoot) {
|
if (-not $fallbackRoot) {
|
||||||
Write-Error "Error: Could not determine repository root. Please run this script from within the repository."
|
Write-Error "Error: Could not determine repository root. Please run this script from within the repository."
|
||||||
@@ -79,18 +120,6 @@ Set-Location $repoRoot
|
|||||||
$specsDir = Join-Path $repoRoot 'specs'
|
$specsDir = Join-Path $repoRoot 'specs'
|
||||||
New-Item -ItemType Directory -Path $specsDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $specsDir -Force | Out-Null
|
||||||
|
|
||||||
$highest = 0
|
|
||||||
if (Test-Path $specsDir) {
|
|
||||||
Get-ChildItem -Path $specsDir -Directory | ForEach-Object {
|
|
||||||
if ($_.Name -match '^(\d{3})') {
|
|
||||||
$num = [int]$matches[1]
|
|
||||||
if ($num -gt $highest) { $highest = $num }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$next = $highest + 1
|
|
||||||
$featureNum = ('{0:000}' -f $next)
|
|
||||||
|
|
||||||
# Function to generate branch name with stop word filtering and length filtering
|
# Function to generate branch name with stop word filtering and length filtering
|
||||||
function Get-BranchName {
|
function Get-BranchName {
|
||||||
param([string]$Description)
|
param([string]$Description)
|
||||||
@@ -145,6 +174,27 @@ if ($ShortName) {
|
|||||||
$branchSuffix = Get-BranchName -Description $featureDesc
|
$branchSuffix = Get-BranchName -Description $featureDesc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Determine branch number
|
||||||
|
if ($Number -eq 0) {
|
||||||
|
if ($hasGit) {
|
||||||
|
# Check existing branches on remotes
|
||||||
|
$Number = Get-NextBranchNumber -ShortName $branchSuffix
|
||||||
|
} else {
|
||||||
|
# Fall back to local directory check
|
||||||
|
$highest = 0
|
||||||
|
if (Test-Path $specsDir) {
|
||||||
|
Get-ChildItem -Path $specsDir -Directory | ForEach-Object {
|
||||||
|
if ($_.Name -match '^(\d{3})') {
|
||||||
|
$num = [int]$matches[1]
|
||||||
|
if ($num -gt $highest) { $highest = $num }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$Number = $highest + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$featureNum = ('{0:000}' -f $Number)
|
||||||
$branchName = "$featureNum-$branchSuffix"
|
$branchName = "$featureNum-$branchSuffix"
|
||||||
|
|
||||||
# GitHub enforces a 244-byte limit on branch names
|
# GitHub enforces a 244-byte limit on branch names
|
||||||
|
|||||||
@@ -31,16 +31,34 @@ Given that feature description, do this:
|
|||||||
- "Create a dashboard for analytics" → "analytics-dashboard"
|
- "Create a dashboard for analytics" → "analytics-dashboard"
|
||||||
- "Fix payment processing timeout bug" → "fix-payment-timeout"
|
- "Fix payment processing timeout bug" → "fix-payment-timeout"
|
||||||
|
|
||||||
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.
|
2. **Check for existing branches before creating new one**:
|
||||||
|
|
||||||
|
a. First, fetch all remote branches to ensure we have the latest information:
|
||||||
|
```bash
|
||||||
|
git fetch --all --prune
|
||||||
|
```
|
||||||
|
|
||||||
|
b. List all branches (local and remote) that match the short-name pattern:
|
||||||
|
```bash
|
||||||
|
git branch -a | grep -E "feature/[0-9]+-<short-name>$"
|
||||||
|
```
|
||||||
|
|
||||||
|
c. Determine the next available number:
|
||||||
|
- Extract all numbers from existing branches (both local and remote)
|
||||||
|
- Find the highest number N from branches that exist
|
||||||
|
- Use N+1 for the new branch number
|
||||||
|
|
||||||
|
d. Run the script `{SCRIPT}` with the calculated number and short-name:
|
||||||
|
- Pass `--number N+1` and `--short-name "your-short-name"` along with the feature description
|
||||||
|
- Bash example: `{SCRIPT} --json --number 5 --short-name "user-auth" "Add user authentication"`
|
||||||
|
- PowerShell example: `{SCRIPT} -Json -Number 5 -ShortName "user-auth" "Add user authentication"`
|
||||||
|
|
||||||
**IMPORTANT**:
|
**IMPORTANT**:
|
||||||
|
- Only consider branches that still exist (local or remote)
|
||||||
- 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.
|
- If no existing branches found with this short-name, start with number 1
|
||||||
- Bash example: `--short-name "your-generated-short-name" "Feature description here"`
|
- The JSON output will contain BRANCH_NAME and SPEC_FILE paths
|
||||||
- PowerShell example: `-ShortName "your-generated-short-name" "Feature description here"`
|
- You must only ever run this script once per feature
|
||||||
- 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")
|
- 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")
|
||||||
- You must only ever run this script once
|
|
||||||
- The JSON is provided in the terminal as output - always refer to it to get the actual content you're looking for
|
|
||||||
|
|
||||||
3. Load `templates/spec-template.md` to understand required sections.
|
3. Load `templates/spec-template.md` to understand required sections.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user