diff --git a/CHANGELOG.md b/CHANGELOG.md index 219a4cb9..bb47e365 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,29 @@ All notable changes to the Specify CLI and templates are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.0.20] - 2025-10-14 + +### Added + +- **Intelligent Branch Naming**: `create-new-feature` scripts now support `--short-name` parameter for custom branch names + - When `--short-name` provided: Uses the custom name directly (cleaned and formatted) + - When omitted: Automatically generates meaningful names using stop word filtering and length-based filtering + - Filters out common stop words (I, want, to, the, for, etc.) + - Removes words shorter than 3 characters (unless they're uppercase acronyms) + - Takes 3-4 most meaningful words from the description + - **Enforces GitHub's 244-byte branch name limit** with automatic truncation and warnings + - Examples: + - "I want to create user authentication" → `001-create-user-authentication` + - "Implement OAuth2 integration for API" → `001-implement-oauth2-integration-api` + - "Fix payment processing bug" → `001-fix-payment-processing` + - Very long descriptions are automatically truncated at word boundaries to stay within limits + - Designed for AI agents to provide semantic short names while maintaining standalone usability + +### Changed + +- Enhanced help documentation for `create-new-feature.sh` and `create-new-feature.ps1` scripts with examples +- Branch names now validated against GitHub's 244-byte limit with automatic truncation if needed + ## [0.0.19] - 2025-10-10 ### Added diff --git a/scripts/bash/create-new-feature.sh b/scripts/bash/create-new-feature.sh index 5cb17fab..574e9152 100644 --- a/scripts/bash/create-new-feature.sh +++ b/scripts/bash/create-new-feature.sh @@ -3,18 +3,42 @@ set -e JSON_MODE=false +SHORT_NAME="" ARGS=() -for arg in "$@"; do +i=0 +while [ $i -lt $# ]; do + arg="${!i}" case "$arg" in - --json) JSON_MODE=true ;; - --help|-h) echo "Usage: $0 [--json] "; exit 0 ;; - *) ARGS+=("$arg") ;; + --json) + JSON_MODE=true + ;; + --short-name) + i=$((i + 1)) + SHORT_NAME="${!i}" + ;; + --help|-h) + echo "Usage: $0 [--json] [--short-name ] " + echo "" + echo "Options:" + echo " --json Output in JSON format" + echo " --short-name Provide a custom short name (2-4 words) for the branch" + echo " --help, -h Show this help message" + echo "" + echo "Examples:" + echo " $0 'Add user authentication system' --short-name 'user-auth'" + echo " $0 'Implement OAuth2 integration for API'" + exit 0 + ;; + *) + ARGS+=("$arg") + ;; esac + i=$((i + 1)) done FEATURE_DESCRIPTION="${ARGS[*]}" if [ -z "$FEATURE_DESCRIPTION" ]; then - echo "Usage: $0 [--json] " >&2 + echo "Usage: $0 [--json] [--short-name ] " >&2 exit 1 fi @@ -67,9 +91,84 @@ fi NEXT=$((HIGHEST + 1)) FEATURE_NUM=$(printf "%03d" "$NEXT") -BRANCH_NAME=$(echo "$FEATURE_DESCRIPTION" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//') -WORDS=$(echo "$BRANCH_NAME" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//') -BRANCH_NAME="${FEATURE_NUM}-${WORDS}" +# Function to generate branch name with stop word filtering and length filtering +generate_branch_name() { + local description="$1" + + # Common stop words to filter out + local stop_words="^(i|a|an|the|to|for|of|in|on|at|by|with|from|is|are|was|were|be|been|being|have|has|had|do|does|did|will|would|should|could|can|may|might|must|shall|this|that|these|those|my|your|our|their|want|need|add|get|set)$" + + # Convert to lowercase and split into words + local clean_name=$(echo "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g') + + # Filter words: remove stop words and words shorter than 3 chars (unless they're uppercase acronyms in original) + local meaningful_words=() + for word in $clean_name; do + # Skip empty words + [ -z "$word" ] && continue + + # Keep words that are NOT stop words AND (length >= 3 OR are potential acronyms) + if ! echo "$word" | grep -qiE "$stop_words"; then + if [ ${#word} -ge 3 ]; then + meaningful_words+=("$word") + elif echo "$description" | grep -q "\b${word^^}\b"; then + # Keep short words if they appear as uppercase in original (likely acronyms) + meaningful_words+=("$word") + fi + fi + done + + # If we have meaningful words, use first 3-4 of them + if [ ${#meaningful_words[@]} -gt 0 ]; then + local max_words=3 + if [ ${#meaningful_words[@]} -eq 4 ]; then max_words=4; fi + + local result="" + local count=0 + for word in "${meaningful_words[@]}"; do + if [ $count -ge $max_words ]; then break; fi + if [ -n "$result" ]; then result="$result-"; fi + result="$result$word" + count=$((count + 1)) + done + echo "$result" + else + # Fallback to original logic if no meaningful words found + echo "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//' | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//' + fi +} + +# Generate branch name +if [ -n "$SHORT_NAME" ]; then + # Use provided short name, just clean it up + BRANCH_SUFFIX=$(echo "$SHORT_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//') +else + # Generate from description with smart filtering + BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION") +fi + +BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}" + +# GitHub enforces a 244-byte limit on branch names +# Validate and truncate if necessary +MAX_BRANCH_LENGTH=244 +if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then + # Calculate how much we need to trim from suffix + # Account for: feature number (3) + hyphen (1) = 4 chars + MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - 4)) + + # Truncate suffix at word boundary if possible + TRUNCATED_SUFFIX=$(echo "$BRANCH_SUFFIX" | cut -c1-$MAX_SUFFIX_LENGTH) + # Remove trailing hyphen if truncation created one + TRUNCATED_SUFFIX=$(echo "$TRUNCATED_SUFFIX" | sed 's/-$//') + + ORIGINAL_BRANCH_NAME="$BRANCH_NAME" + BRANCH_NAME="${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)" + >&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)" +fi if [ "$HAS_GIT" = true ]; then git checkout -b "$BRANCH_NAME" diff --git a/scripts/powershell/create-new-feature.ps1 b/scripts/powershell/create-new-feature.ps1 index 0f1f5912..a9a57ef9 100644 --- a/scripts/powershell/create-new-feature.ps1 +++ b/scripts/powershell/create-new-feature.ps1 @@ -3,15 +3,34 @@ [CmdletBinding()] param( [switch]$Json, + [string]$ShortName, [Parameter(ValueFromRemainingArguments = $true)] [string[]]$FeatureDescription ) $ErrorActionPreference = 'Stop' -if (-not $FeatureDescription -or $FeatureDescription.Count -eq 0) { - Write-Error "Usage: ./create-new-feature.ps1 [-Json] " - exit 1 +# Show help if requested or no description provided +if (($FeatureDescription -contains '--help') -or ($FeatureDescription -contains '-h') -or + (-not $FeatureDescription) -or ($FeatureDescription.Count -eq 0)) { + + if (($FeatureDescription -contains '--help') -or ($FeatureDescription -contains '-h')) { + Write-Host "Usage: ./create-new-feature.ps1 [-Json] [-ShortName ] " + Write-Host "" + Write-Host "Options:" + Write-Host " -Json Output in JSON format" + Write-Host " -ShortName Provide a custom short name (2-4 words) for the branch" + Write-Host " -h, --help Show this help message" + Write-Host "" + Write-Host "Examples:" + Write-Host " ./create-new-feature.ps1 'Add user authentication system' -ShortName 'user-auth'" + Write-Host " ./create-new-feature.ps1 'Implement OAuth2 integration for API'" + exit 0 + } else { + Write-Error "Usage: ./create-new-feature.ps1 [-Json] [-ShortName ] " + exit 1 + } } + $featureDesc = ($FeatureDescription -join ' ').Trim() # Resolve repository root. Prefer git information when available, but fall back @@ -72,9 +91,82 @@ if (Test-Path $specsDir) { $next = $highest + 1 $featureNum = ('{0:000}' -f $next) -$branchName = $featureDesc.ToLower() -replace '[^a-z0-9]', '-' -replace '-{2,}', '-' -replace '^-', '' -replace '-$', '' -$words = ($branchName -split '-') | Where-Object { $_ } | Select-Object -First 3 -$branchName = "$featureNum-$([string]::Join('-', $words))" +# Function to generate branch name with stop word filtering and length filtering +function Get-BranchName { + param([string]$Description) + + # Common stop words to filter out + $stopWords = @( + 'i', 'a', 'an', 'the', 'to', 'for', 'of', 'in', 'on', 'at', 'by', 'with', 'from', + 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', + 'do', 'does', 'did', 'will', 'would', 'should', 'could', 'can', 'may', 'might', 'must', 'shall', + 'this', 'that', 'these', 'those', 'my', 'your', 'our', 'their', + 'want', 'need', 'add', 'get', 'set' + ) + + # Convert to lowercase and extract words (alphanumeric only) + $cleanName = $Description.ToLower() -replace '[^a-z0-9\s]', ' ' + $words = $cleanName -split '\s+' | Where-Object { $_ } + + # Filter words: remove stop words and words shorter than 3 chars (unless they're uppercase acronyms in original) + $meaningfulWords = @() + foreach ($word in $words) { + # Skip stop words + if ($stopWords -contains $word) { continue } + + # Keep words that are length >= 3 OR appear as uppercase in original (likely acronyms) + if ($word.Length -ge 3) { + $meaningfulWords += $word + } elseif ($Description -match "\b$($word.ToUpper())\b") { + # Keep short words if they appear as uppercase in original (likely acronyms) + $meaningfulWords += $word + } + } + + # If we have meaningful words, use first 3-4 of them + if ($meaningfulWords.Count -gt 0) { + $maxWords = if ($meaningfulWords.Count -eq 4) { 4 } else { 3 } + $result = ($meaningfulWords | Select-Object -First $maxWords) -join '-' + return $result + } else { + # Fallback to original logic if no meaningful words found + $result = $Description.ToLower() -replace '[^a-z0-9]', '-' -replace '-{2,}', '-' -replace '^-', '' -replace '-$', '' + $fallbackWords = ($result -split '-') | Where-Object { $_ } | Select-Object -First 3 + return [string]::Join('-', $fallbackWords) + } +} + +# Generate branch name +if ($ShortName) { + # Use provided short name, just clean it up + $branchSuffix = $ShortName.ToLower() -replace '[^a-z0-9]', '-' -replace '-{2,}', '-' -replace '^-', '' -replace '-$', '' +} else { + # Generate from description with smart filtering + $branchSuffix = Get-BranchName -Description $featureDesc +} + +$branchName = "$featureNum-$branchSuffix" + +# GitHub enforces a 244-byte limit on branch names +# Validate and truncate if necessary +$maxBranchLength = 244 +if ($branchName.Length -gt $maxBranchLength) { + # Calculate how much we need to trim from suffix + # Account for: feature number (3) + hyphen (1) = 4 chars + $maxSuffixLength = $maxBranchLength - 4 + + # Truncate suffix + $truncatedSuffix = $branchSuffix.Substring(0, [Math]::Min($branchSuffix.Length, $maxSuffixLength)) + # Remove trailing hyphen if truncation created one + $truncatedSuffix = $truncatedSuffix -replace '-$', '' + + $originalBranchName = $branchName + $branchName = "$featureNum-$truncatedSuffix" + + Write-Warning "[specify] Branch name exceeded GitHub's 244-byte limit" + Write-Warning "[specify] Original: $originalBranchName ($($originalBranchName.Length) bytes)" + Write-Warning "[specify] Truncated to: $branchName ($($branchName.Length) bytes)" +} if ($hasGit) { try {