#!/usr/bin/env bash set -e # Function to find the repository root by searching for existing project markers find_repo_root() { local dir="$1" while [ "$dir" != "/" ]; do if [ -d "$dir/.git" ] || [ -d "$dir/.specify" ]; then echo "$dir" return 0 fi dir="$(dirname "$dir")" done return 1 } # Function to check existing branches (local and remote) and return next available number check_existing_branches() { local short_name="$1" local specs_dir="$2" # 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 using git ls-remote (more reliable) local remote_branches=$(git ls-remote --heads origin 2>/dev/null | grep -E "refs/heads/[0-9]+-${short_name}$" | sed 's/.*\/\([0-9]*\)-.* /\1/' | sort -n) # Also check local branches local local_branches=$(git branch 2>/dev/null | grep -E "^[* ]*[0-9]+-${short_name}$" | sed 's/^[* ]*//' | sed 's/-.*//' | sort -n) # Check specs directory as well local spec_dirs="" if [ -d "$specs_dir" ]; then spec_dirs=$(find "$specs_dir" -maxdepth 1 -type d -name "[0-9]*-${short_name}" 2>/dev/null | xargs -n1 basename 2>/dev/null | sed 's/-.*//' | sort -n) fi # Combine all sources and get the highest number local max_num=0 for num in $remote_branches $local_branches $spec_dirs; do if [ "$num" -gt "$max_num" ]; then max_num=$num fi done # Return next number echo $((max_num + 1)) } # 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)$" # Split into words and lowercase local words=$(echo "$description" | tr '[:upper:]' '[:lower:]' | tr -s '[:punct:][:space:]' '\n') local meaningful_words=() for word in $words; do [[ -z "$word" ]] && continue # Keep if not a stop word AND (length >= 3 OR uppercase acronym in original) if ! echo "$word" | grep -qiE "$stop_words"; then if [[ ${#word} -ge 3 ]] || echo "$description" | grep -q "\b${word^^}\b"; then meaningful_words+=("$word") fi fi done if [[ ${#meaningful_words[@]} -gt 0 ]]; then # Use first 4 meaningful words joined by hyphens printf "%s\n" "${meaningful_words[@]}" | head -n 4 | tr '\n' '-' | sed 's/-$//' else # Fallback: just take first 3 non-empty words echo "$description" | tr '[:upper:]' '[:lower:]' | tr -s '[:punct:][:space:]' '\n' | grep -v '^$' | head -n 3 | tr '\n' '-' | sed 's/-$//' fi } main() { local json_mode=false local short_name="" local branch_number="" local args=() local i=1 while [ $i -le $# ]; do local arg="${!i}" case "$arg" in --json) json_mode=true ;; --short-name) if [ $((i + 1)) -gt $# ]; then echo 'Error: --short-name requires a value' >&2 exit 1 fi i=$((i + 1)) local next_arg="${!i}" if [[ "$next_arg" == --* ]]; then echo 'Error: --short-name requires a value' >&2 exit 1 fi short_name="$next_arg" ;; --number) if [ $((i + 1)) -gt $# ]; then echo 'Error: --number requires a value' >&2 exit 1 fi i=$((i + 1)) local next_arg="${!i}" if [[ "$next_arg" == --* ]]; then echo 'Error: --number requires a value' >&2 exit 1 fi branch_number="$next_arg" ;; --help|-h) echo "Usage: $0 [--json] [--short-name ] [--number N] " echo "" echo "Options:" echo " --json Output in JSON format" echo " --short-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 "" echo "Examples:" echo " $0 'Add user authentication system' --short-name 'user-auth'" echo " $0 'Implement OAuth2 integration for API' --number 5" return 0 ;; *) args+=("$arg") ;; esac i=$((i + 1)) done local feature_description="${args[*]}" if [ -z "$feature_description" ]; then echo "Usage: $0 [--json] [--short-name ] [--number N] " >&2 exit 1 fi local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" local repo_root local has_git if git rev-parse --show-toplevel >/dev/null 2>&1; then repo_root=$(git rev-parse --show-toplevel) has_git=true else repo_root="$(find_repo_root "$script_dir")" if [ -z "$repo_root" ]; then echo "Error: Could not determine repository root." >&2 exit 1 fi has_git=false fi cd "$repo_root" local specs_dir="$repo_root/specs" mkdir -p "$specs_dir" # Generate branch name suffix local branch_suffix if [ -n "$short_name" ]; then branch_suffix=$(echo "$short_name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//') else branch_suffix=$(generate_branch_name "$feature_description") fi # Determine feature number local feature_num if [ -n "$branch_number" ]; then feature_num="$branch_number" else feature_num=$(check_existing_branches "$branch_suffix" "$specs_dir") fi local branch_name="${feature_num}-${branch_suffix}" # Validate and truncate if necessary local max_branch_length=244 if [ ${#branch_name} -gt $max_branch_length ]; then local max_suffix_length=$((max_branch_length - 4)) local truncated_suffix=$(echo "$branch_suffix" | cut -c1-$max_suffix_length | sed 's/-$//') local 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" else >&2 echo "[specify] Warning: Git repository not detected; skipped branch creation for $branch_name" fi local feature_dir="$specs_dir/$branch_name" mkdir -p "$feature_dir" local template="$repo_root/.specify/templates/spec-template.md" local spec_file="$feature_dir/spec.md" if [ -f "$template" ]; then cp "$template" "$spec_file"; else touch "$spec_file"; fi # Set the SPECIFY_FEATURE environment variable for the current session export SPECIFY_FEATURE="$branch_name" if $json_mode; then printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$branch_name" "$spec_file" "$feature_num" else echo "BRANCH_NAME: $branch_name" echo "SPEC_FILE: $spec_file" echo "FEATURE_NUM: $feature_num" echo "SPECIFY_FEATURE environment variable set to: $branch_name" fi } # Execute main function if script is run directly if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then main "$@" fi