Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion extensions/git/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Git repository initialization, feature branch creation, numbering (sequential/ti
This extension provides Git operations as an optional, self-contained module. It manages:

- **Repository initialization** with configurable commit messages
- **Feature branch creation** with sequential (`001-feature-name`) or timestamp (`20260319-143022-feature-name`) numbering
- **Feature branch creation** with sequential (`001-feature-name`) or timestamp (`20260319-143022-feature-name`) numbering, with optional branch prefix (e.g., `feature`, `bugfix`)
- **Branch validation** to ensure branches follow naming conventions
- **Git remote detection** for GitHub integration (e.g., issue creation)
- **Auto-commit** after core commands (configurable per-command with custom messages)
Expand Down Expand Up @@ -65,6 +65,16 @@ auto_commit:
message: "[Spec Kit] Add specification"
```

### Branch Prefix

The `create-new-feature` scripts accept a `--prefix` (Bash) / `-Prefix` (PowerShell) option to prepend a custom prefix to branch names. Common prefixes:

- `feature` — new features or enhancements
- `bugfix` — bug fixes
- `hotfix` — urgent production fixes

The trailing `/` is optional — the script auto-appends it if missing. The final branch name format is `{prefix}/{number}-{short-name}` (e.g., `--prefix "feature"` produces `feature/001-user-auth`).

## Installation

```bash
Expand Down
21 changes: 17 additions & 4 deletions extensions/git/commands/speckit.git.feature.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ Determine the branch numbering strategy by checking configuration in this order:
2. Check `.specify/init-options.json` for `branch_numbering` value (backward compatibility)
3. Default to `sequential` if neither exists

## Branch Prefix

Determine the branch prefix based on the nature of the work:

- Use `feature` for new features or enhancements
- Use `bugfix` for bug fixes
- Use `hotfix` for urgent production fixes
- Use no prefix (omit `--prefix`) for the default flat naming

Choose the most appropriate prefix based on the feature description and context. The trailing `/` is optional — the script auto-appends it if missing. The final branch name format is `{prefix}/{number}-{short-name}` (e.g., `--prefix "feature"` produces `feature/001-user-auth`).

## Execution

Generate a concise short name (2-4 words) for the branch:
Expand All @@ -43,10 +54,12 @@ Generate a concise short name (2-4 words) for the branch:

Run the appropriate script based on your platform:

- **Bash**: `.specify/extensions/git/scripts/bash/create-new-feature.sh --json --short-name "<short-name>" "<feature description>"`
- **Bash (timestamp)**: `.specify/extensions/git/scripts/bash/create-new-feature.sh --json --timestamp --short-name "<short-name>" "<feature description>"`
- **PowerShell**: `.specify/extensions/git/scripts/powershell/create-new-feature.ps1 -Json -ShortName "<short-name>" "<feature description>"`
- **PowerShell (timestamp)**: `.specify/extensions/git/scripts/powershell/create-new-feature.ps1 -Json -Timestamp -ShortName "<short-name>" "<feature description>"`
- **Bash**: `.specify/extensions/git/scripts/bash/create-new-feature.sh --json --prefix "<prefix>" --short-name "<short-name>" "<feature description>"`
- **Bash (timestamp)**: `.specify/extensions/git/scripts/bash/create-new-feature.sh --json --prefix "<prefix>" --timestamp --short-name "<short-name>" "<feature description>"`
- **PowerShell**: `.specify/extensions/git/scripts/powershell/create-new-feature.ps1 -Json -Prefix "<prefix>" -ShortName "<short-name>" "<feature description>"`
- **PowerShell (timestamp)**: `.specify/extensions/git/scripts/powershell/create-new-feature.ps1 -Json -Prefix "<prefix>" -Timestamp -ShortName "<short-name>" "<feature description>"`

If no prefix is needed, omit `--prefix` / `-Prefix` entirely.

**IMPORTANT**:
- Do NOT pass `--number` — the script determines the correct next number automatically
Expand Down
47 changes: 41 additions & 6 deletions extensions/git/scripts/bash/create-new-feature.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ DRY_RUN=false
ALLOW_EXISTING=false
SHORT_NAME=""
BRANCH_NUMBER=""
BRANCH_PREFIX=""
USE_TIMESTAMP=false
ARGS=()
i=1
Expand Down Expand Up @@ -59,8 +60,21 @@ while [ $i -le $# ]; do
--timestamp)
USE_TIMESTAMP=true
;;
--prefix)
if [ $((i + 1)) -gt $# ]; then
echo 'Error: --prefix requires a value' >&2
exit 1
fi
i=$((i + 1))
next_arg="${!i}"
if [[ "$next_arg" == --* ]]; then
echo 'Error: --prefix requires a value' >&2
exit 1
fi
BRANCH_PREFIX="$next_arg"
;;
Comment thread
leoxiao2012 marked this conversation as resolved.
--help|-h)
echo "Usage: $0 [--json] [--dry-run] [--allow-existing-branch] [--short-name <name>] [--number N] [--timestamp] <feature_description>"
echo "Usage: $0 [--json] [--dry-run] [--allow-existing-branch] [--short-name <name>] [--number N] [--timestamp] [--prefix <prefix>] <feature_description>"
echo ""
echo "Options:"
echo " --json Output in JSON format"
Expand All @@ -69,6 +83,7 @@ while [ $i -le $# ]; do
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 " --timestamp Use timestamp prefix (YYYYMMDD-HHMMSS) instead of sequential numbering"
echo " --prefix <prefix> Custom prefix for the branch name (e.g. 'feature', 'bugfix')"
echo " --help, -h Show this help message"
echo ""
echo "Environment variables:"
Expand All @@ -88,9 +103,25 @@ while [ $i -le $# ]; do
i=$((i + 1))
done

# Validate and normalize branch prefix
if [ -n "$BRANCH_PREFIX" ]; then
BRANCH_PREFIX=$(echo "$BRANCH_PREFIX" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
if [ -z "$BRANCH_PREFIX" ]; then
echo 'Error: --prefix cannot be empty or whitespace' >&2
exit 1
fi
# Strip optional trailing '/' before checking for embedded slashes
_check_prefix="${BRANCH_PREFIX%/}"
if [[ "$_check_prefix" == */* ]]; then
echo 'Error: --prefix must be a single segment (no embedded slashes); e.g. "feature", "bugfix"' >&2
exit 1
fi
BRANCH_PREFIX="$_check_prefix/"
fi
Comment on lines +106 to +120

FEATURE_DESCRIPTION="${ARGS[*]}"
if [ -z "$FEATURE_DESCRIPTION" ]; then
echo "Usage: $0 [--json] [--dry-run] [--allow-existing-branch] [--short-name <name>] [--number N] [--timestamp] <feature_description>" >&2
echo "Usage: $0 [--json] [--dry-run] [--allow-existing-branch] [--short-name <name>] [--number N] [--timestamp] [--prefix <prefix>] <feature_description>" >&2
exit 1
fi

Expand Down Expand Up @@ -134,6 +165,10 @@ _extract_highest_number() {
local highest=0
while IFS= read -r name; do
[ -z "$name" ] && continue
# Strip optional prefix segment (e.g., "feature/003-name" -> "003-name")
if [[ "$name" =~ ^([^/]+)/([^/]+)$ ]]; then
name="${BASH_REMATCH[2]}"
fi
if echo "$name" | grep -Eq '^[0-9]{3,}-' && ! echo "$name" | grep -Eq '^[0-9]{8}-[0-9]{6}-'; then
number=$(echo "$name" | grep -Eo '^[0-9]+' || echo "0")
number=$((10#$number))
Expand Down Expand Up @@ -334,7 +369,7 @@ else
# Determine branch prefix
if [ "$USE_TIMESTAMP" = true ]; then
FEATURE_NUM=$(date +%Y%m%d-%H%M%S)
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
BRANCH_NAME="${BRANCH_PREFIX}${FEATURE_NUM}-${BRANCH_SUFFIX}"
else
if [ -z "$BRANCH_NUMBER" ]; then
if [ "$DRY_RUN" = true ] && [ "$HAS_GIT" = true ]; then
Expand All @@ -351,7 +386,7 @@ else
fi

FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))")
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
BRANCH_NAME="${BRANCH_PREFIX}${FEATURE_NUM}-${BRANCH_SUFFIX}"
fi
fi

Expand All @@ -363,14 +398,14 @@ if [ -n "${GIT_BRANCH_NAME:-}" ] && [ "$BRANCH_BYTE_LEN" -gt $MAX_BRANCH_LENGTH
>&2 echo "Error: GIT_BRANCH_NAME must be 244 bytes or fewer in UTF-8. Provided value is ${BRANCH_BYTE_LEN} bytes."
exit 1
elif [ "$BRANCH_BYTE_LEN" -gt $MAX_BRANCH_LENGTH ]; then
PREFIX_LENGTH=$(( ${#FEATURE_NUM} + 1 ))
PREFIX_LENGTH=$(( ${#BRANCH_PREFIX} + ${#FEATURE_NUM} + 1 ))
MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - PREFIX_LENGTH))

TRUNCATED_SUFFIX=$(echo "$BRANCH_SUFFIX" | cut -c1-$MAX_SUFFIX_LENGTH)
TRUNCATED_SUFFIX=$(echo "$TRUNCATED_SUFFIX" | sed 's/-$//')

ORIGINAL_BRANCH_NAME="$BRANCH_NAME"
BRANCH_NAME="${FEATURE_NUM}-${TRUNCATED_SUFFIX}"
BRANCH_NAME="${BRANCH_PREFIX}${FEATURE_NUM}-${TRUNCATED_SUFFIX}"

>&2 echo "[specify] Warning: Branch name exceeded GitHub's 244-byte limit"
>&2 echo "[specify] Original: $ORIGINAL_BRANCH_NAME (${#ORIGINAL_BRANCH_NAME} bytes)"
Expand Down
33 changes: 27 additions & 6 deletions extensions/git/scripts/powershell/create-new-feature.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ param(
[switch]$AllowExistingBranch,
[switch]$DryRun,
[string]$ShortName,
[string]$Prefix = "",
[Parameter()]
[long]$Number = 0,
[switch]$Timestamp,
Expand All @@ -19,13 +20,14 @@ param(
$ErrorActionPreference = 'Stop'

if ($Help) {
Write-Host "Usage: ./create-new-feature.ps1 [-Json] [-DryRun] [-AllowExistingBranch] [-ShortName <name>] [-Number N] [-Timestamp] <feature description>"
Write-Host "Usage: ./create-new-feature.ps1 [-Json] [-DryRun] [-AllowExistingBranch] [-ShortName <name>] [-Prefix <prefix>] [-Number N] [-Timestamp] <feature description>"
Write-Host ""
Write-Host "Options:"
Comment thread
leoxiao2012 marked this conversation as resolved.
Write-Host " -Json Output in JSON format"
Write-Host " -DryRun Compute branch name without creating the branch"
Write-Host " -AllowExistingBranch Switch to branch if it already exists instead of failing"
Write-Host " -ShortName <name> Provide a custom short name (2-4 words) for the branch"
Write-Host " -Prefix <prefix> Custom prefix for the branch name (e.g. 'feature', 'bugfix')"
Write-Host " -Number N Specify branch number manually (overrides auto-detection)"
Write-Host " -Timestamp Use timestamp prefix (YYYYMMDD-HHMMSS) instead of sequential numbering"
Write-Host " -Help Show this help message"
Expand All @@ -37,12 +39,27 @@ if ($Help) {
}

if (-not $FeatureDescription -or $FeatureDescription.Count -eq 0) {
Write-Error "Usage: ./create-new-feature.ps1 [-Json] [-DryRun] [-AllowExistingBranch] [-ShortName <name>] [-Number N] [-Timestamp] <feature description>"
Write-Error "Usage: ./create-new-feature.ps1 [-Json] [-DryRun] [-AllowExistingBranch] [-ShortName <name>] [-Prefix <prefix>] [-Number N] [-Timestamp] <feature description>"
exit 1
}

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

# Validate and normalize branch prefix
if ($Prefix) {
$Prefix = $Prefix.Trim()
if ([string]::IsNullOrWhiteSpace($Prefix)) {
Write-Error "Error: -Prefix cannot be empty or whitespace"
exit 1
}
$checkPrefix = $Prefix.TrimEnd('/')
if ($checkPrefix.Contains('/')) {
Write-Error "Error: -Prefix must be a single segment (no embedded slashes); e.g. 'feature', 'bugfix'"
exit 1
}
$Prefix = "$checkPrefix/"
}
Comment on lines +48 to +61

if ([string]::IsNullOrWhiteSpace($featureDesc)) {
Write-Error "Error: Feature description cannot be empty or contain only whitespace"
exit 1
Expand Down Expand Up @@ -70,6 +87,10 @@ function Get-HighestNumberFromNames {

[long]$highest = 0
foreach ($name in $Names) {
# Strip optional prefix segment (e.g., "feature/003-name" -> "003-name")
if ($name -match '^[^/]+/([^/]+)$') {
$name = $Matches[1]
}
if ($name -match '^(\d{3,})-' -and $name -notmatch '^\d{8}-\d{6}-') {
[long]$num = 0
if ([long]::TryParse($matches[1], [ref]$num) -and $num -gt $highest) {
Expand Down Expand Up @@ -289,7 +310,7 @@ if ($env:GIT_BRANCH_NAME) {

if ($Timestamp) {
$featureNum = Get-Date -Format 'yyyyMMdd-HHmmss'
$branchName = "$featureNum-$branchSuffix"
$branchName = "$Prefix$featureNum-$branchSuffix"
} else {
if ($Number -eq 0) {
if ($DryRun -and $hasGit) {
Expand All @@ -304,20 +325,20 @@ if ($env:GIT_BRANCH_NAME) {
}

$featureNum = ('{0:000}' -f $Number)
$branchName = "$featureNum-$branchSuffix"
$branchName = "$Prefix$featureNum-$branchSuffix"
}
}

$maxBranchLength = 244
if ($branchName.Length -gt $maxBranchLength) {
$prefixLength = $featureNum.Length + 1
$prefixLength = $Prefix.Length + $featureNum.Length + 1
$maxSuffixLength = $maxBranchLength - $prefixLength

$truncatedSuffix = $branchSuffix.Substring(0, [Math]::Min($branchSuffix.Length, $maxSuffixLength))
$truncatedSuffix = $truncatedSuffix -replace '-$', ''

$originalBranchName = $branchName
$branchName = "$featureNum-$truncatedSuffix"
$branchName = "$Prefix$featureNum-$truncatedSuffix"
Comment on lines 332 to +341

Write-Warning "[specify] Branch name exceeded GitHub's 244-byte limit"
Write-Warning "[specify] Original: $originalBranchName ($($originalBranchName.Length) bytes)"
Expand Down
Loading