Skip to content
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
60 changes: 54 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,8 @@ DRY_RUN=false
ALLOW_EXISTING=false
SHORT_NAME=""
BRANCH_NUMBER=""
BRANCH_PREFIX=""
MAX_PREFIX_LEN=16
USE_TIMESTAMP=false
ARGS=()
i=1
Expand Down Expand Up @@ -59,8 +61,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 +84,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 +104,37 @@ 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 [ -z "$_check_prefix" ]; then
echo 'Error: --prefix must contain at least one non-slash character' >&2
exit 1
fi
if [[ "$_check_prefix" == */* ]]; then
echo 'Error: --prefix must be a single segment (no embedded slashes); e.g. "feature", "bugfix"' >&2
exit 1
fi
if ! echo "$_check_prefix" | grep -qE '^[a-z0-9][-a-z0-9]*$'; then
echo 'Error: --prefix must start with a letter or digit and contain only ASCII lowercase letters, digits, and hyphens' >&2
exit 1
fi
if [ ${#_check_prefix} -gt "$MAX_PREFIX_LEN" ]; then
echo "Error: --prefix must be $MAX_PREFIX_LEN characters or fewer" >&2
exit 1
fi
BRANCH_PREFIX="$_check_prefix/"
fi
Comment thread
leoxiao2012 marked this conversation as resolved.

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 +178,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 +382,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 +399,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 +411,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
47 changes: 41 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 @@ -18,14 +19,17 @@ param(
)
$ErrorActionPreference = 'Stop'

$MaxPrefixLen = 16

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 +41,39 @@ 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 ([string]::IsNullOrEmpty($checkPrefix)) {
Write-Error "Error: -Prefix must contain at least one non-slash character"
exit 1
}
if ($checkPrefix.Contains('/')) {
Write-Error "Error: -Prefix must be a single segment (no embedded slashes); e.g. 'feature', 'bugfix'"
exit 1
}
if ($checkPrefix -notmatch '^[a-z0-9][-a-z0-9]*$') {
Write-Error "Error: -Prefix must start with a letter or digit and contain only ASCII lowercase letters, digits, and hyphens"
exit 1
}
if ($checkPrefix.Length -gt $MaxPrefixLen) {
Write-Error "Error: -Prefix must be $MaxPrefixLen characters or fewer"
exit 1
}
$Prefix = "$checkPrefix/"
}
Comment thread
leoxiao2012 marked this conversation as resolved.

if ([string]::IsNullOrWhiteSpace($featureDesc)) {
Write-Error "Error: Feature description cannot be empty or contain only whitespace"
exit 1
Expand Down Expand Up @@ -70,6 +101,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 +324,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 +339,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 thread
leoxiao2012 marked this conversation as resolved.

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