diff --git a/scripts/bash/check-prerequisites.sh b/scripts/bash/check-prerequisites.sh index 54f32ec36..3ca106846 100644 --- a/scripts/bash/check-prerequisites.sh +++ b/scripts/bash/check-prerequisites.sh @@ -82,6 +82,11 @@ source "$SCRIPT_DIR/common.sh" eval $(get_feature_paths) check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1 +# Check if branch and spec directory are in sync (skip in paths-only mode) +if ! $PATHS_ONLY; then + check_and_fix_spec_directory_mismatch || exit 1 +fi + # If paths-only mode, output paths and exit (support JSON + paths-only combined) if $PATHS_ONLY; then if $JSON_MODE; then diff --git a/scripts/bash/common.sh b/scripts/bash/common.sh index 6931eccc8..1cdde5b05 100644 --- a/scripts/bash/common.sh +++ b/scripts/bash/common.sh @@ -1,6 +1,9 @@ #!/usr/bin/env bash # Common functions and variables for all scripts +# Feature branch naming pattern (3 digits followed by hyphen) +readonly FEATURE_BRANCH_PATTERN='^[0-9]{3}-' + # Get repository root, with fallback for non-git repositories get_repo_root() { if git rev-parse --show-toplevel >/dev/null 2>&1; then @@ -37,6 +40,8 @@ get_current_branch() { for dir in "$specs_dir"/*; do if [[ -d "$dir" ]]; then local dirname=$(basename "$dir") + # Note: Cannot use FEATURE_BRANCH_PATTERN here as we need the capture group + # to extract the numeric part for comparison if [[ "$dirname" =~ ^([0-9]{3})- ]]; then local number=${BASH_REMATCH[1]} number=$((10#$number)) @@ -71,8 +76,8 @@ check_feature_branch() { echo "[specify] Warning: Git repository not detected; skipped branch validation" >&2 return 0 fi - - if [[ ! "$branch" =~ ^[0-9]{3}- ]]; then + + if [[ ! "$branch" =~ $FEATURE_BRANCH_PATTERN ]]; then echo "ERROR: Not on a feature branch. Current branch: $branch" >&2 echo "Feature branches should be named like: 001-feature-name" >&2 return 1 @@ -154,3 +159,110 @@ EOF check_file() { [[ -f "$1" ]] && echo " ✓ $2" || echo " ✗ $2"; } check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; } +# Check if current branch matches a spec directory, and offer to fix mismatches +# +# This function detects "orphaned" spec directories (directories with no matching git branch) +# and provides guidance to rename them to match the current branch. +# +# Returns: +# 0 - Success (no mismatch or check skipped) +# 1 - Mismatch detected, user action required +# +# Side effects: +# Writes warning messages and remediation instructions to stderr +# +# Dependencies: +# Requires git for orphan detection (gracefully skips if unavailable) +check_and_fix_spec_directory_mismatch() { + local repo_root=$(get_repo_root) + local current_branch=$(get_current_branch) + + # Validate required variables + if [[ -z "$repo_root" ]] || [[ -z "$current_branch" ]]; then + return 0 # Skip check if we can't determine context + fi + + local expected_dir="$repo_root/specs/$current_branch" + + # Skip check for non-git repos or main branch + if [[ "$current_branch" == "main" ]] || ! has_git; then + return 0 + fi + + # Skip check if branch doesn't follow feature branch naming convention + if [[ ! "$current_branch" =~ $FEATURE_BRANCH_PATTERN ]]; then + return 0 + fi + + # If expected directory exists, all good + if [[ -d "$expected_dir" ]]; then + return 0 + fi + + # Directory doesn't exist - look for orphaned spec directories + local specs_dir="$repo_root/specs" + local orphaned_dirs=() + + if [[ -d "$specs_dir" ]] && has_git; then + # Get all existing branch names for efficient lookup (Bash 3.2 compatible) + local existing_branches=$(git for-each-ref refs/heads/ --format='%(refname:short)' 2>/dev/null | tr '\n' '|') + + # Enable nullglob to handle empty directories gracefully + local original_nullglob=$(shopt -p nullglob) + shopt -s nullglob + + for dir in "$specs_dir"/*; do + # Skip if not a directory or if it's a symlink + if [[ -d "$dir" ]] && [[ ! -L "$dir" ]]; then + local dirname=$(basename "$dir") + + # Skip hidden/special directories + if [[ "$dirname" =~ ^\. ]]; then + continue + fi + + # Check if this spec dir has no matching branch + # Using grep for compatibility with older bash versions + if ! echo "|${existing_branches}" | grep -qF "|${dirname}|"; then + orphaned_dirs+=("$dirname") + fi + fi + done + + # Restore original nullglob setting + eval "$original_nullglob" + fi + + # If we found exactly one orphaned directory, suggest renaming it + if [[ ${#orphaned_dirs[@]} -eq 1 ]]; then + local orphaned="${orphaned_dirs[0]}" + echo "" >&2 + echo "WARNING: Branch '$current_branch' has no matching spec directory" >&2 + echo " Found orphaned spec directory: specs/$orphaned" >&2 + echo " This may be from a deleted or renamed branch." >&2 + echo "" >&2 + echo " To fix this issue, run:" >&2 + echo " git mv specs/$orphaned specs/$current_branch" >&2 + echo "" >&2 + return 1 + elif [[ ${#orphaned_dirs[@]} -gt 1 ]]; then + echo "" >&2 + echo "WARNING: Branch '$current_branch' has no matching spec directory" >&2 + echo " Found multiple orphaned spec directories:" >&2 + for dir in "${orphaned_dirs[@]}"; do + echo " - specs/$dir" >&2 + done + echo "" >&2 + echo " To fix this, manually rename the correct directory:" >&2 + echo " git mv specs/ specs/$current_branch" >&2 + echo "" >&2 + return 1 + else + # No spec directory exists at all - might be a new branch + echo "" >&2 + echo "WARNING: No spec directory found for branch '$current_branch'" >&2 + echo " Run scripts/bash/create-new-feature.sh to create a new feature specification." >&2 + echo "" >&2 + return 1 + fi +} diff --git a/scripts/bash/setup-plan.sh b/scripts/bash/setup-plan.sh index 740a1438c..4ac243d29 100644 --- a/scripts/bash/setup-plan.sh +++ b/scripts/bash/setup-plan.sh @@ -33,6 +33,13 @@ eval $(get_feature_paths) # Check if we're on a proper feature branch (only for git repos) check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1 +# Check if branch and spec directory are in sync +check_and_fix_spec_directory_mismatch +result=$? +if [[ $result -ne 0 && $result -ne 1 ]]; then + exit 1 +fi + # Ensure the feature directory exists mkdir -p "$FEATURE_DIR"