228 lines
8 KiB
Bash
Executable file
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 |