| #!/usr/bin/env bash |
|
|
| set -e |
|
|
| JSON_MODE=false |
| SHORT_NAME="" |
| BRANCH_NUMBER="" |
| ARGS=() |
| i=1 |
| while [ $i -le $# ]; do |
| 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)) |
| 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)) |
| 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" |
| exit 0 |
| ;; |
| *) |
| ARGS+=("$arg") |
| ;; |
| esac |
| i=$((i + 1)) |
| done |
|
|
| FEATURE_DESCRIPTION="${ARGS[*]}" |
| if [ -z "$FEATURE_DESCRIPTION" ]; then |
| echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>" >&2 |
| exit 1 |
| fi |
|
|
| |
| 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 |
| } |
|
|
| |
| get_highest_from_specs() { |
| local specs_dir="$1" |
| local highest=0 |
| |
| if [ -d "$specs_dir" ]; then |
| for dir in "$specs_dir"/*; do |
| [ -d "$dir" ] || continue |
| dirname=$(basename "$dir") |
| number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0") |
| number=$((10#$number)) |
| if [ "$number" -gt "$highest" ]; then |
| highest=$number |
| fi |
| done |
| fi |
| |
| echo "$highest" |
| } |
|
|
| |
| get_highest_from_branches() { |
| local highest=0 |
| |
| |
| branches=$(git branch -a 2>/dev/null || echo "") |
| |
| if [ -n "$branches" ]; then |
| while IFS= read -r branch; do |
| |
| clean_branch=$(echo "$branch" | sed 's/^[* ]*//; s|^remotes/[^/]*/||') |
| |
| |
| if echo "$clean_branch" | grep -q '^[0-9]\{3\}-'; then |
| number=$(echo "$clean_branch" | grep -o '^[0-9]\{3\}' || echo "0") |
| number=$((10#$number)) |
| if [ "$number" -gt "$highest" ]; then |
| highest=$number |
| fi |
| fi |
| done <<< "$branches" |
| fi |
| |
| echo "$highest" |
| } |
|
|
| |
| check_existing_branches() { |
| local specs_dir="$1" |
|
|
| |
| git fetch --all --prune 2>/dev/null || true |
|
|
| |
| local highest_branch=$(get_highest_from_branches) |
|
|
| |
| local highest_spec=$(get_highest_from_specs "$specs_dir") |
|
|
| |
| local max_num=$highest_branch |
| if [ "$highest_spec" -gt "$max_num" ]; then |
| max_num=$highest_spec |
| fi |
|
|
| |
| echo $((max_num + 1)) |
| } |
|
|
| |
| clean_branch_name() { |
| local name="$1" |
| echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//' |
| } |
|
|
| |
| |
| |
| SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
|
|
| 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. Please run this script from within the repository." >&2 |
| exit 1 |
| fi |
| HAS_GIT=false |
| fi |
|
|
| cd "$REPO_ROOT" |
|
|
| SPECS_DIR="$REPO_ROOT/specs" |
| mkdir -p "$SPECS_DIR" |
|
|
| |
| generate_branch_name() { |
| local description="$1" |
| |
| |
| 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)$" |
| |
| |
| local clean_name=$(echo "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g') |
| |
| |
| local meaningful_words=() |
| for word in $clean_name; do |
| |
| [ -z "$word" ] && continue |
| |
| |
| 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 |
| |
| meaningful_words+=("$word") |
| fi |
| fi |
| done |
| |
| |
| 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 |
| |
| local cleaned=$(clean_branch_name "$description") |
| echo "$cleaned" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//' |
| fi |
| } |
|
|
| |
| if [ -n "$SHORT_NAME" ]; then |
| |
| BRANCH_SUFFIX=$(clean_branch_name "$SHORT_NAME") |
| else |
| |
| BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION") |
| fi |
|
|
| |
| if [ -z "$BRANCH_NUMBER" ]; then |
| if [ "$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 |
|
|
| |
| FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))") |
| BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}" |
|
|
| |
| |
| MAX_BRANCH_LENGTH=244 |
| if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then |
| |
| |
| MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - 4)) |
| |
| |
| 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}" |
| |
| >&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 |
|
|
| FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME" |
| mkdir -p "$FEATURE_DIR" |
|
|
| TEMPLATE="$REPO_ROOT/.specify/templates/spec-template.md" |
| SPEC_FILE="$FEATURE_DIR/spec.md" |
| if [ -f "$TEMPLATE" ]; then cp "$TEMPLATE" "$SPEC_FILE"; else touch "$SPEC_FILE"; fi |
|
|
| |
| 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 |
|
|