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
25 changes: 13 additions & 12 deletions extensions/git/commands/speckit.git.feature.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: "Create a feature branch with sequential or timestamp numbering"

# Create Feature Branch

Create a new feature branch for the given specification.
Create and switch to a new git feature branch for the given specification. This command handles **branch creation only** — the spec directory and files are created by the core `/speckit.specify` workflow.

## User Input

Expand All @@ -14,10 +14,17 @@ $ARGUMENTS

You **MUST** consider the user input before proceeding (if not empty).

## Environment Variable Override

If the user explicitly provided `GIT_BRANCH_NAME` (e.g., via environment variable, argument, or in their request), pass it through to the script by setting the `GIT_BRANCH_NAME` environment variable before invoking the script. When `GIT_BRANCH_NAME` is set:
- The script uses the exact value as the branch name, bypassing all prefix/suffix generation
- `--short-name`, `--number`, and `--timestamp` flags are ignored
- `FEATURE_NUM` is extracted from the name if it starts with a numeric prefix, otherwise set to the full branch name

## Prerequisites

- Verify Git is available by running `git rev-parse --is-inside-work-tree 2>/dev/null`
- If Git is not available, warn the user and skip branch creation (spec directory will still be created)
- If Git is not available, warn the user and skip branch creation

## Branch Numbering Mode

Expand Down Expand Up @@ -45,22 +52,16 @@ Run the appropriate script based on your platform:
- Do NOT pass `--number` — the script determines the correct next number automatically
- Always include the JSON flag (`--json` for Bash, `-Json` for PowerShell) so the output can be parsed reliably
- You must only ever run this script once per feature
- The JSON output will contain BRANCH_NAME and SPEC_FILE paths

If the extension scripts are not found at the `.specify/extensions/git/` path, fall back to:
- **Bash**: `scripts/bash/create-new-feature.sh`
- **PowerShell**: `scripts/powershell/create-new-feature.ps1`
- The JSON output will contain `BRANCH_NAME` and `FEATURE_NUM`

## Graceful Degradation

If Git is not installed or the current directory is not a Git repository:
- The script will still create the spec directory under `specs/`
- A warning will be printed: `[specify] Warning: Git repository not detected; skipped branch creation`
- The workflow continues normally without branch creation
- Branch creation is skipped with a warning: `[specify] Warning: Git repository not detected; skipped branch creation`
- The script still outputs `BRANCH_NAME` and `FEATURE_NUM` so the caller can reference them

## Output

The script outputs JSON with:
- `BRANCH_NAME`: The created branch name (e.g., `003-user-auth` or `20260319-143022-user-auth`)
- `SPEC_FILE`: Path to the created spec file
- `BRANCH_NAME`: The branch name (e.g., `003-user-auth` or `20260319-143022-user-auth`)
- `FEATURE_NUM`: The numeric or timestamp prefix used
123 changes: 61 additions & 62 deletions extensions/git/scripts/bash/create-new-feature.sh
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,21 @@ while [ $i -le $# ]; do
echo ""
echo "Options:"
echo " --json Output in JSON format"
echo " --dry-run Compute branch name and paths without creating branches, directories, or files"
echo " --dry-run Compute branch name without creating the branch"
echo " --allow-existing-branch Switch to branch if it already exists instead of failing"
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 " --help, -h Show this help message"
echo ""
echo "Environment variables:"
echo " GIT_BRANCH_NAME Use this exact branch name, bypassing all prefix/suffix generation"
echo ""
echo "Examples:"
echo " $0 'Add user authentication system' --short-name 'user-auth'"
echo " $0 'Implement OAuth2 integration for API' --number 5"
echo " $0 --timestamp --short-name 'user-auth' 'Add user authentication'"
echo " GIT_BRANCH_NAME=my-branch $0 'feature description'"
exit 0
;;
*)
Expand Down Expand Up @@ -258,9 +262,6 @@ fi
cd "$REPO_ROOT"

SPECS_DIR="$REPO_ROOT/specs"
if [ "$DRY_RUN" != true ]; then
mkdir -p "$SPECS_DIR"
fi

# Function to generate branch name with stop word filtering
generate_branch_name() {
Expand Down Expand Up @@ -301,45 +302,67 @@ generate_branch_name() {
fi
}

# Generate branch name
if [ -n "$SHORT_NAME" ]; then
BRANCH_SUFFIX=$(clean_branch_name "$SHORT_NAME")
# Check for GIT_BRANCH_NAME env var override (exact branch name, no prefix/suffix)
if [ -n "${GIT_BRANCH_NAME:-}" ]; then
BRANCH_NAME="$GIT_BRANCH_NAME"
# Extract FEATURE_NUM from the branch name if it starts with a numeric prefix
# Check timestamp pattern first (YYYYMMDD-HHMMSS-) since it also matches the simpler ^[0-9]+ pattern
if echo "$BRANCH_NAME" | grep -Eq '^[0-9]{8}-[0-9]{6}-'; then
FEATURE_NUM=$(echo "$BRANCH_NAME" | grep -Eo '^[0-9]{8}-[0-9]{6}')
BRANCH_SUFFIX="${BRANCH_NAME#${FEATURE_NUM}-}"
elif echo "$BRANCH_NAME" | grep -Eq '^[0-9]+-'; then
FEATURE_NUM=$(echo "$BRANCH_NAME" | grep -Eo '^[0-9]+')
BRANCH_SUFFIX="${BRANCH_NAME#${FEATURE_NUM}-}"
else
FEATURE_NUM="$BRANCH_NAME"
BRANCH_SUFFIX="$BRANCH_NAME"
fi
else
BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION")
fi
# Generate branch name
if [ -n "$SHORT_NAME" ]; then
BRANCH_SUFFIX=$(clean_branch_name "$SHORT_NAME")
else
BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION")
fi

# Warn if --number and --timestamp are both specified
if [ "$USE_TIMESTAMP" = true ] && [ -n "$BRANCH_NUMBER" ]; then
>&2 echo "[specify] Warning: --number is ignored when --timestamp is used"
BRANCH_NUMBER=""
fi
# Warn if --number and --timestamp are both specified
if [ "$USE_TIMESTAMP" = true ] && [ -n "$BRANCH_NUMBER" ]; then
>&2 echo "[specify] Warning: --number is ignored when --timestamp is used"
BRANCH_NUMBER=""
fi

# Determine branch prefix
if [ "$USE_TIMESTAMP" = true ]; then
FEATURE_NUM=$(date +%Y%m%d-%H%M%S)
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
else
if [ -z "$BRANCH_NUMBER" ]; then
if [ "$DRY_RUN" = true ] && [ "$HAS_GIT" = true ]; then
BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR" true)
elif [ "$DRY_RUN" = true ]; then
HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
BRANCH_NUMBER=$((HIGHEST + 1))
elif [ "$HAS_GIT" = true ]; then
BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR")
else
HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
BRANCH_NUMBER=$((HIGHEST + 1))
# Determine branch prefix
if [ "$USE_TIMESTAMP" = true ]; then
FEATURE_NUM=$(date +%Y%m%d-%H%M%S)
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
else
if [ -z "$BRANCH_NUMBER" ]; then
if [ "$DRY_RUN" = true ] && [ "$HAS_GIT" = true ]; then
BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR" true)
elif [ "$DRY_RUN" = true ]; then
HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
BRANCH_NUMBER=$((HIGHEST + 1))
elif [ "$HAS_GIT" = true ]; then
BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR")
else
HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
BRANCH_NUMBER=$((HIGHEST + 1))
fi
fi
fi

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

# GitHub enforces a 244-byte limit on branch names
MAX_BRANCH_LENGTH=244
if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then
_byte_length() { printf '%s' "$1" | LC_ALL=C wc -c | tr -d ' '; }
BRANCH_BYTE_LEN=$(_byte_length "$BRANCH_NAME")
if [ -n "${GIT_BRANCH_NAME:-}" ] && [ "$BRANCH_BYTE_LEN" -gt $MAX_BRANCH_LENGTH ]; then
>&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 ))
MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - PREFIX_LENGTH))

Expand All @@ -354,9 +377,6 @@ if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then
>&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)"
fi

FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
SPEC_FILE="$FEATURE_DIR/spec.md"

if [ "$DRY_RUN" != true ]; then
if [ "$HAS_GIT" = true ]; then
branch_create_error=""
Expand Down Expand Up @@ -391,22 +411,6 @@ if [ "$DRY_RUN" != true ]; then
>&2 echo "[specify] Warning: Git repository not detected; skipped branch creation for $BRANCH_NAME"
fi

mkdir -p "$FEATURE_DIR"

if [ ! -f "$SPEC_FILE" ]; then
if type resolve_template >/dev/null 2>&1; then
TEMPLATE=$(resolve_template "spec-template" "$REPO_ROOT") || true
else
TEMPLATE=""
fi
if [ -n "$TEMPLATE" ] && [ -f "$TEMPLATE" ]; then
cp "$TEMPLATE" "$SPEC_FILE"
else
echo "Warning: Spec template not found; created empty spec file" >&2
touch "$SPEC_FILE"
fi
fi

printf '# To persist: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME" >&2
fi

Expand All @@ -415,35 +419,30 @@ if $JSON_MODE; then
if [ "$DRY_RUN" = true ]; then
jq -cn \
--arg branch_name "$BRANCH_NAME" \
--arg spec_file "$SPEC_FILE" \
--arg feature_num "$FEATURE_NUM" \
'{BRANCH_NAME:$branch_name,SPEC_FILE:$spec_file,FEATURE_NUM:$feature_num,DRY_RUN:true}'
'{BRANCH_NAME:$branch_name,FEATURE_NUM:$feature_num,DRY_RUN:true}'
else
jq -cn \
--arg branch_name "$BRANCH_NAME" \
--arg spec_file "$SPEC_FILE" \
--arg feature_num "$FEATURE_NUM" \
'{BRANCH_NAME:$branch_name,SPEC_FILE:$spec_file,FEATURE_NUM:$feature_num}'
'{BRANCH_NAME:$branch_name,FEATURE_NUM:$feature_num}'
fi
else
if type json_escape >/dev/null 2>&1; then
_je_branch=$(json_escape "$BRANCH_NAME")
_je_spec=$(json_escape "$SPEC_FILE")
_je_num=$(json_escape "$FEATURE_NUM")
else
_je_branch="$BRANCH_NAME"
_je_spec="$SPEC_FILE"
_je_num="$FEATURE_NUM"
fi
if [ "$DRY_RUN" = true ]; then
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s","DRY_RUN":true}\n' "$_je_branch" "$_je_spec" "$_je_num"
printf '{"BRANCH_NAME":"%s","FEATURE_NUM":"%s","DRY_RUN":true}\n' "$_je_branch" "$_je_num"
else
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$_je_branch" "$_je_spec" "$_je_num"
printf '{"BRANCH_NAME":"%s","FEATURE_NUM":"%s"}\n' "$_je_branch" "$_je_num"
fi
fi
else
echo "BRANCH_NAME: $BRANCH_NAME"
echo "SPEC_FILE: $SPEC_FILE"
echo "FEATURE_NUM: $FEATURE_NUM"
if [ "$DRY_RUN" != true ]; then
printf '# To persist in your shell: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME"
Expand Down
Loading
Loading