skills/.specify/scripts/bash/create-new-feature.sh

228 lines
8 KiB
Bash
Executable file

#!/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 <name>] [--number N] <feature_description>"
echo ""
echo "Options:"
echo " --json Output in JSON format"
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 " --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 <name>] [--number N] <feature_description>" >&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