diff --git a/scripts/bash/common.sh b/scripts/bash/common.sh index c332ceb88..416fcadfc 100644 --- a/scripts/bash/common.sh +++ b/scripts/bash/common.sh @@ -1,15 +1,48 @@ #!/usr/bin/env bash # Common functions and variables for all scripts -# Get repository root, with fallback for non-git repositories +# Find repository root by searching upward for .specify directory +# This is the primary marker for spec-kit projects +find_specify_root() { + local dir="${1:-$(pwd)}" + # Normalize to absolute path to prevent infinite loop with relative paths + # Use -- to handle paths starting with - (e.g., -P, -L) + dir="$(cd -- "$dir" 2>/dev/null && pwd)" || return 1 + local prev_dir="" + while true; do + if [ -d "$dir/.specify" ]; then + echo "$dir" + return 0 + fi + # Stop if we've reached filesystem root or dirname stops changing + if [ "$dir" = "/" ] || [ "$dir" = "$prev_dir" ]; then + break + fi + prev_dir="$dir" + dir="$(dirname "$dir")" + done + return 1 +} + +# Get repository root, prioritizing .specify directory over git +# This prevents using a parent git repo when spec-kit is initialized in a subdirectory get_repo_root() { + # First, look for .specify directory (spec-kit's own marker) + local specify_root + if specify_root=$(find_specify_root); then + echo "$specify_root" + return + fi + + # Fallback to git if no .specify found if git rev-parse --show-toplevel >/dev/null 2>&1; then git rev-parse --show-toplevel - else - # Fall back to script location for non-git repos - local script_dir="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - (cd "$script_dir/../../.." && pwd) + return fi + + # Final fallback to script location for non-git repos + local script_dir="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + (cd "$script_dir/../../.." && pwd) } # Get current branch, with fallback for non-git repositories @@ -20,14 +53,14 @@ get_current_branch() { return fi - # Then check git if available - if git rev-parse --abbrev-ref HEAD >/dev/null 2>&1; then - git rev-parse --abbrev-ref HEAD + # Then check git if available at the spec-kit root (not parent) + local repo_root=$(get_repo_root) + if has_git; then + git -C "$repo_root" rev-parse --abbrev-ref HEAD return fi # For non-git repos, try to find the latest feature directory - local repo_root=$(get_repo_root) local specs_dir="$repo_root/specs" if [[ -d "$specs_dir" ]]; then @@ -68,9 +101,17 @@ get_current_branch() { echo "main" # Final fallback } -# Check if we have git available +# Check if we have git available at the spec-kit root level +# Returns true only if git is installed and the repo root is inside a git work tree +# Handles both regular repos (.git directory) and worktrees/submodules (.git file) has_git() { - git rev-parse --show-toplevel >/dev/null 2>&1 + # First check if git command is available (before calling get_repo_root which may use git) + command -v git >/dev/null 2>&1 || return 1 + local repo_root=$(get_repo_root) + # Check if .git exists (directory or file for worktrees/submodules) + [ -e "$repo_root/.git" ] || return 1 + # Verify it's actually a valid git work tree + git -C "$repo_root" rev-parse --is-inside-work-tree >/dev/null 2>&1 } check_feature_branch() { diff --git a/scripts/bash/create-new-feature.sh b/scripts/bash/create-new-feature.sh index 0df4adc3e..579d34752 100644 --- a/scripts/bash/create-new-feature.sh +++ b/scripts/bash/create-new-feature.sh @@ -80,19 +80,6 @@ if [ -z "$FEATURE_DESCRIPTION" ]; then exit 1 fi -# 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 get highest number from specs directory get_highest_from_specs() { local specs_dir="$1" @@ -171,21 +158,16 @@ clean_branch_name() { echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//' } -# Resolve repository root. Prefer git information when available, but fall back -# to searching for repository markers so the workflow still functions in repositories that -# were initialised with --no-git. +# Resolve repository root using common.sh functions which prioritize .specify over git SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/common.sh" -if git rev-parse --show-toplevel >/dev/null 2>&1; then - REPO_ROOT=$(git rev-parse --show-toplevel) +REPO_ROOT=$(get_repo_root) + +# Check if git is available at this repo root (not a parent) +if has_git; then 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 diff --git a/scripts/powershell/common.ps1 b/scripts/powershell/common.ps1 index 1595bd8a0..c67097773 100644 --- a/scripts/powershell/common.ps1 +++ b/scripts/powershell/common.ps1 @@ -1,7 +1,38 @@ #!/usr/bin/env pwsh # Common PowerShell functions analogous to common.sh +# Find repository root by searching upward for .specify directory +# This is the primary marker for spec-kit projects +function Find-SpecifyRoot { + param([string]$StartDir = (Get-Location).Path) + + # Normalize to absolute path to prevent issues with relative paths + # Use -LiteralPath to handle paths with wildcard characters ([, ], *, ?) + $current = (Resolve-Path -LiteralPath $StartDir -ErrorAction SilentlyContinue)?.Path + if (-not $current) { return $null } + + while ($true) { + if (Test-Path -LiteralPath (Join-Path $current ".specify") -PathType Container) { + return $current + } + $parent = Split-Path $current -Parent + if ([string]::IsNullOrEmpty($parent) -or $parent -eq $current) { + return $null + } + $current = $parent + } +} + +# Get repository root, prioritizing .specify directory over git +# This prevents using a parent git repo when spec-kit is initialized in a subdirectory function Get-RepoRoot { + # First, look for .specify directory (spec-kit's own marker) + $specifyRoot = Find-SpecifyRoot + if ($specifyRoot) { + return $specifyRoot + } + + # Fallback to git if no .specify found try { $result = git rev-parse --show-toplevel 2>$null if ($LASTEXITCODE -eq 0) { @@ -10,9 +41,10 @@ function Get-RepoRoot { } catch { # Git command failed } - - # Fall back to script location for non-git repos - return (Resolve-Path (Join-Path $PSScriptRoot "../../..")).Path + + # Final fallback to script location for non-git repos + # Use -LiteralPath to handle paths with wildcard characters + return (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "../../..")).Path } function Get-CurrentBranch { @@ -20,19 +52,21 @@ function Get-CurrentBranch { if ($env:SPECIFY_FEATURE) { return $env:SPECIFY_FEATURE } - - # Then check git if available - try { - $result = git rev-parse --abbrev-ref HEAD 2>$null - if ($LASTEXITCODE -eq 0) { - return $result - } - } catch { - # Git command failed - } - - # For non-git repos, try to find the latest feature directory + + # Then check git if available at the spec-kit root (not parent) $repoRoot = Get-RepoRoot + if (Test-HasGit) { + try { + $result = git -C $repoRoot rev-parse --abbrev-ref HEAD 2>$null + if ($LASTEXITCODE -eq 0) { + return $result + } + } catch { + # Git command failed + } + } + + # For non-git repos, try to find the latest feature directory $specsDir = Join-Path $repoRoot "specs" if (Test-Path $specsDir) { @@ -69,9 +103,23 @@ function Get-CurrentBranch { return "main" } +# Check if we have git available at the spec-kit root level +# Returns true only if git is installed and the repo root is inside a git work tree +# Handles both regular repos (.git directory) and worktrees/submodules (.git file) function Test-HasGit { + # First check if git command is available (before calling Get-RepoRoot which may use git) + if (-not (Get-Command git -ErrorAction SilentlyContinue)) { + return $false + } + $repoRoot = Get-RepoRoot + # Check if .git exists (directory or file for worktrees/submodules) + # Use -LiteralPath to handle paths with wildcard characters + if (-not (Test-Path -LiteralPath (Join-Path $repoRoot ".git"))) { + return $false + } + # Verify it's actually a valid git work tree try { - git rev-parse --show-toplevel 2>$null | Out-Null + $null = git -C $repoRoot rev-parse --is-inside-work-tree 2>$null return ($LASTEXITCODE -eq 0) } catch { return $false diff --git a/scripts/powershell/create-new-feature.ps1 b/scripts/powershell/create-new-feature.ps1 index 473c925b9..9adae131d 100644 --- a/scripts/powershell/create-new-feature.ps1 +++ b/scripts/powershell/create-new-feature.ps1 @@ -45,30 +45,6 @@ if ([string]::IsNullOrWhiteSpace($featureDesc)) { exit 1 } -# Resolve repository root. Prefer git information when available, but fall back -# to searching for repository markers so the workflow still functions in repositories that -# were initialized with --no-git. -function Find-RepositoryRoot { - param( - [string]$StartDir, - [string[]]$Markers = @('.git', '.specify') - ) - $current = Resolve-Path $StartDir - while ($true) { - foreach ($marker in $Markers) { - if (Test-Path (Join-Path $current $marker)) { - return $current - } - } - $parent = Split-Path $current -Parent - if ($parent -eq $current) { - # Reached filesystem root without finding markers - return $null - } - $current = $parent - } -} - function Get-HighestNumberFromSpecs { param([string]$SpecsDir) @@ -139,26 +115,14 @@ function ConvertTo-CleanBranchName { return $Name.ToLower() -replace '[^a-z0-9]', '-' -replace '-{2,}', '-' -replace '^-', '' -replace '-$', '' } -$fallbackRoot = (Find-RepositoryRoot -StartDir $PSScriptRoot) -if (-not $fallbackRoot) { - Write-Error "Error: Could not determine repository root. Please run this script from within the repository." - exit 1 -} - -# Load common functions (includes Resolve-Template) +# Load common functions (includes Get-RepoRoot, Test-HasGit, Resolve-Template) . "$PSScriptRoot/common.ps1" -try { - $repoRoot = git rev-parse --show-toplevel 2>$null - if ($LASTEXITCODE -eq 0) { - $hasGit = $true - } else { - throw "Git not available" - } -} catch { - $repoRoot = $fallbackRoot - $hasGit = $false -} +# Use common.ps1 functions which prioritize .specify over git +$repoRoot = Get-RepoRoot + +# Check if git is available at this repo root (not a parent) +$hasGit = Test-HasGit Set-Location $repoRoot