-
Notifications
You must be signed in to change notification settings - Fork 5.6k
Customizable Branch Naming Templates #1511
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
5f3a51e
3b9ce4f
62895a5
8b0d700
fd7ec44
b95112c
4ad743c
11dbb3f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -154,3 +154,205 @@ EOF | |
| check_file() { [[ -f "$1" ]] && echo " ✓ $2" || echo " ✗ $2"; } | ||
| check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; } | ||
|
|
||
| # ============================================================================= | ||
| # TOML Settings Functions (for branch template customization) | ||
| # ============================================================================= | ||
|
|
||
| # Parse a single key from a TOML file | ||
| # Usage: get_toml_value "file.toml" "branch.template" | ||
| # Supports dotted keys by searching within [section] blocks | ||
| get_toml_value() { | ||
| local file="$1" | ||
| local key="$2" | ||
|
|
||
| [[ ! -f "$file" ]] && return 1 | ||
|
|
||
| # Handle dotted keys like "branch.template" | ||
| if [[ "$key" == *.* ]]; then | ||
| local section="${key%%.*}" | ||
| local subkey="${key#*.}" | ||
| # Find the section and extract the key value | ||
| awk -v section="$section" -v key="$subkey" ' | ||
| BEGIN { in_section = 0 } | ||
| /^\[/ { | ||
| gsub(/[\[\]]/, "") | ||
| in_section = ($0 == section) | ||
| } | ||
| in_section && $0 ~ "^"key"[[:space:]]*=" { | ||
| sub(/^[^=]*=[[:space:]]*/, "") | ||
| gsub(/^"|"$/, "") # Remove surrounding quotes | ||
| exit | ||
| } | ||
| ' "$file" | ||
| else | ||
| # Simple key without section | ||
| grep -E "^${key}[[:space:]]*=" "$file" 2>/dev/null | \ | ||
| sed 's/.*=[[:space:]]*"\([^"]*\)".*/\1/' | head -1 | ||
| fi | ||
| } | ||
|
|
||
| # Load branch template from settings file | ||
| # Returns: template string or empty if not found | ||
| load_branch_template() { | ||
| local repo_root="${1:-$(get_repo_root)}" | ||
| local settings_file="$repo_root/.specify/settings.toml" | ||
|
|
||
| if [[ -f "$settings_file" ]]; then | ||
| get_toml_value "$settings_file" "branch.template" | ||
| fi | ||
| } | ||
|
|
||
| # ============================================================================= | ||
| # Username and Email Resolution Functions | ||
| # ============================================================================= | ||
|
|
||
| # Resolve {username} variable from Git config or OS fallback | ||
| # Returns: normalized username (lowercase, hyphens for special chars) | ||
| resolve_username() { | ||
| local username | ||
| username=$(git config user.name 2>/dev/null || echo "") | ||
|
|
||
| if [[ -z "$username" ]]; then | ||
| # Fallback to OS username | ||
| username="${USER:-${USERNAME:-unknown}}" | ||
| fi | ||
|
|
||
| # Normalize: lowercase, replace non-alphanumeric with hyphens, collapse multiple hyphens | ||
| echo "$username" | tr '[:upper:]' '[:lower:]' | \ | ||
| sed 's/[^a-z0-9]/-/g' | \ | ||
| sed 's/-\+/-/g' | \ | ||
| sed 's/^-//' | \ | ||
| sed 's/-$//' | ||
| } | ||
|
|
||
| # Resolve {email_prefix} variable from Git config | ||
| # Returns: email prefix (portion before @) or empty string | ||
| resolve_email_prefix() { | ||
| local email | ||
| email=$(git config user.email 2>/dev/null || echo "") | ||
|
|
||
| if [[ -n "$email" && "$email" == *@* ]]; then | ||
| echo "${email%%@*}" | tr '[:upper:]' '[:lower:]' | ||
| fi | ||
| # Returns empty string if no email configured (per FR-002 clarification) | ||
| } | ||
|
|
||
| # ============================================================================= | ||
| # Branch Name Validation Functions | ||
| # ============================================================================= | ||
|
|
||
| # Validate branch name against Git rules | ||
| # Args: $1 = branch name | ||
| # Returns: 0 if valid, 1 if invalid (prints error to stderr) | ||
| validate_branch_name() { | ||
| local name="$1" | ||
|
|
||
| # Cannot be empty | ||
| if [[ -z "$name" ]]; then | ||
| echo "Error: Branch name cannot be empty" >&2 | ||
| return 1 | ||
| fi | ||
|
|
||
| # Cannot start with hyphen | ||
| if [[ "$name" == -* ]]; then | ||
| echo "Error: Branch name cannot start with hyphen: $name" >&2 | ||
| return 1 | ||
| fi | ||
|
|
||
| # Cannot contain .. | ||
| if [[ "$name" == *..* ]]; then | ||
| echo "Error: Branch name cannot contain '..': $name" >&2 | ||
| return 1 | ||
| fi | ||
|
|
||
| # Cannot contain forbidden characters: ~ ^ : ? * [ \ | ||
| if [[ "$name" =~ [~\^:\?\*\[\\] ]]; then | ||
| echo "Error: Branch name contains invalid characters (~^:?*[\\): $name" >&2 | ||
| return 1 | ||
| fi | ||
|
|
||
| # Cannot end with .lock | ||
| if [[ "$name" == *.lock ]]; then | ||
| echo "Error: Branch name cannot end with '.lock': $name" >&2 | ||
| return 1 | ||
| fi | ||
|
|
||
| # Cannot end with / | ||
| if [[ "$name" == */ ]]; then | ||
| echo "Error: Branch name cannot end with '/': $name" >&2 | ||
| return 1 | ||
| fi | ||
|
|
||
| # Cannot contain // | ||
| if [[ "$name" == *//* ]]; then | ||
| echo "Error: Branch name cannot contain '//': $name" >&2 | ||
| return 1 | ||
| fi | ||
|
|
||
| # Check max length (244 bytes for GitHub) | ||
| if [[ ${#name} -gt 244 ]]; then | ||
| echo "Warning: Branch name exceeds 244 bytes (GitHub limit): $name" >&2 | ||
| # Return success but warn - truncation handled elsewhere | ||
| fi | ||
|
|
||
| return 0 | ||
| } | ||
|
|
||
| # ============================================================================= | ||
| # Per-User Number Scoping Functions | ||
| # ============================================================================= | ||
|
|
||
| # Get highest feature number for a specific prefix pattern | ||
| # Args: $1 = prefix (e.g., "johndoe/" or "feature/johndoe/") | ||
| # Returns: highest number found (0 if none) | ||
| get_highest_for_prefix() { | ||
| local prefix="$1" | ||
| local repo_root="${2:-$(get_repo_root)}" | ||
| local specs_dir="$repo_root/specs" | ||
| local highest=0 | ||
|
|
||
| # Escape special regex characters in prefix for grep | ||
| local escaped_prefix | ||
| escaped_prefix=$(printf '%s' "$prefix" | sed 's/[.[\*^$()+?{|\\]/\\&/g') | ||
|
|
||
| # Check specs directory for matching directories | ||
| if [[ -d "$specs_dir" ]]; then | ||
| for dir in "$specs_dir"/"${prefix}"*; do | ||
| [[ -d "$dir" ]] || continue | ||
| local dirname | ||
| dirname=$(basename "$dir") | ||
| # Extract number after prefix: prefix + 3-digit number | ||
| if [[ "$dirname" =~ ^${escaped_prefix}([0-9]{3})- ]]; then | ||
| local num=$((10#${BASH_REMATCH[1]})) | ||
| if [[ "$num" -gt "$highest" ]]; then | ||
| highest=$num | ||
| fi | ||
| fi | ||
| done | ||
| fi | ||
|
|
||
| # Also check git branches if available | ||
| if git rev-parse --show-toplevel >/dev/null 2>&1; then | ||
| local branches | ||
| branches=$(git branch -a 2>/dev/null || echo "") | ||
| if [[ -n "$branches" ]]; then | ||
| while IFS= read -r branch; do | ||
| # Clean branch name | ||
| local clean_branch | ||
| clean_branch=$(echo "$branch" | sed 's/^[* ]*//; s|^remotes/[^/]*/||') | ||
|
|
||
| # Check if branch matches prefix pattern | ||
| if [[ "$clean_branch" =~ ^${escaped_prefix}([0-9]{3})- ]]; then | ||
| local num=$((10#${BASH_REMATCH[1]})) | ||
| if [[ "$num" -gt "$highest" ]]; then | ||
| highest=$num | ||
| fi | ||
| fi | ||
| done <<< "$branches" | ||
|
Comment on lines
+315
to
+352
|
||
| fi | ||
| fi | ||
|
|
||
| echo "$highest" | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The branch validation logic earlier in this file (
check_feature_branchusing the^[0-9]{3}-pattern) is now inconsistent with the new template-based naming introduced here (e.g.,{username}/{number}-{short_name}), which produces branches likejdoe/001-add-loginthat no longer start with a 3‑digit prefix. Scripts such assetup-plan.shthat callcheck_feature_branchwill reject branches created bycreate-new-feature.shwhen a prefix is configured, breaking the workflow for teams that customizebranch.template. The validation should be updated to accept the configured template pattern (or at least to locate and validate the numeric{number}portion after any prefix) instead of hard‑coding a leadingNNN-requirement, and related error messages should be generalized away from001-feature-name.