Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions scripts/bash/check-prerequisites.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
116 changes: 114 additions & 2 deletions scripts/bash/common.sh
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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' '|')
Copy link

Copilot AI Nov 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existing_branches variable will have a trailing pipe character when branches exist, but will be empty when no branches exist. Line 226's check echo \"|${existing_branches}\" | grep -qF \"|${dirname}|\" will fail for the first branch name when existing_branches is non-empty because of the missing leading pipe in existing_branches. The variable should be |branch1|branch2| format, but tr '\\n' '|' produces branch1|branch2|. This will cause false positives for orphan detection.

Suggested change
local existing_branches=$(git for-each-ref refs/heads/ --format='%(refname:short)' 2>/dev/null | tr '\n' '|')
local existing_branches="|$(git for-each-ref refs/heads/ --format='%(refname:short)' 2>/dev/null | tr '\n' '|')"

Copilot uses AI. Check for mistakes.

# 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/<old-name> 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
}
7 changes: 7 additions & 0 deletions scripts/bash/setup-plan.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link

Copilot AI Nov 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return code handling logic is unclear. According to the function documentation (lines 167-172 in common.sh), the function only returns 0 (success/no mismatch) or 1 (mismatch detected). The condition if [[ $result -ne 0 && $result -ne 1 ]] suggests there are other return codes, but none are documented or implemented. Either simplify this to handle only the documented return codes, or update the function documentation to specify what other return codes mean.

Suggested change
if [[ $result -ne 0 && $result -ne 1 ]]; then
if [[ $result -eq 1 ]]; then

Copilot uses AI. Check for mistakes.
exit 1
fi
Comment on lines +37 to +41
Copy link

Copilot AI Nov 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent error handling between setup-plan.sh and check-prerequisites.sh. In check-prerequisites.sh (line 87), the function is called with || exit 1, treating any non-zero return as an error. Here, return code 1 is explicitly allowed to continue. This inconsistency suggests different intentions that should be clarified with comments or unified.

Copilot uses AI. Check for mistakes.

# Ensure the feature directory exists
mkdir -p "$FEATURE_DIR"

Expand Down