Skip to content

Commit 21376bb

Browse files
mbachorikiamaeroplaneclaude
authored andcommitted
fix(scripts): prioritize .specify over git for repo root detection (github#1933)
* fix(scripts): prioritize .specify over git for repo root detection When spec-kit is initialized in a subdirectory that doesn't have its own .git, but a parent directory does, spec-kit was incorrectly using the parent's git repository root. This caused specs to be created in the wrong location. The fix changes repo root detection to prioritize .specify directory over git rev-parse, ensuring spec-kit respects its own initialization boundary rather than inheriting a parent git repo. Fixes github#1932 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: address code review feedback - Normalize paths in find_specify_root to prevent infinite loop with relative paths - Use -PathType Container in PowerShell to only match .specify directories - Improve has_git/Test-HasGit to check git command availability and validate work tree - Handle git worktrees/submodules where .git can be a file - Remove dead fallback code in create-new-feature scripts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: check .specify before termination in find_specify_root Fixes edge case where project root is at filesystem root (common in containers). The loop now checks for .specify before checking the termination condition. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: scope git operations to spec-kit root & remove unused helpers - get_current_branch now uses has_git check and runs git with -C to prevent using parent git repo branch names in .specify-only projects - Same fix applied to PowerShell Get-CurrentBranch - Removed unused find_repo_root() from create-new-feature.sh - Removed unused Find-RepositoryRoot from create-new-feature.ps1 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: use cd -- to handle paths starting with dash Prevents cd from interpreting directory names like -P or -L as options. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: check git command exists before calling get_repo_root in has_git Avoids unnecessary work when git isn't installed since get_repo_root may internally call git rev-parse. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(powershell): use LiteralPath and check git before Get-RepoRoot - Use -LiteralPath in Find-SpecifyRoot to handle paths with wildcard characters ([, ], *, ?) - Check Get-Command git before calling Get-RepoRoot in Test-HasGit to avoid unnecessary work when git isn't installed Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(powershell): use LiteralPath for .git check in Test-HasGit Prevents Test-Path from treating wildcard characters in paths as globs. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(powershell): use LiteralPath in Get-RepoRoot fallback Prevents Resolve-Path from treating wildcard characters as patterns. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: iamaeroplane <michal.bachorik@gmail.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 9044112 commit 21376bb

4 files changed

Lines changed: 135 additions & 65 deletions

File tree

scripts/bash/common.sh

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,48 @@
11
#!/usr/bin/env bash
22
# Common functions and variables for all scripts
33

4-
# Get repository root, with fallback for non-git repositories
4+
# Find repository root by searching upward for .specify directory
5+
# This is the primary marker for spec-kit projects
6+
find_specify_root() {
7+
local dir="${1:-$(pwd)}"
8+
# Normalize to absolute path to prevent infinite loop with relative paths
9+
# Use -- to handle paths starting with - (e.g., -P, -L)
10+
dir="$(cd -- "$dir" 2>/dev/null && pwd)" || return 1
11+
local prev_dir=""
12+
while true; do
13+
if [ -d "$dir/.specify" ]; then
14+
echo "$dir"
15+
return 0
16+
fi
17+
# Stop if we've reached filesystem root or dirname stops changing
18+
if [ "$dir" = "/" ] || [ "$dir" = "$prev_dir" ]; then
19+
break
20+
fi
21+
prev_dir="$dir"
22+
dir="$(dirname "$dir")"
23+
done
24+
return 1
25+
}
26+
27+
# Get repository root, prioritizing .specify directory over git
28+
# This prevents using a parent git repo when spec-kit is initialized in a subdirectory
529
get_repo_root() {
30+
# First, look for .specify directory (spec-kit's own marker)
31+
local specify_root
32+
if specify_root=$(find_specify_root); then
33+
echo "$specify_root"
34+
return
35+
fi
36+
37+
# Fallback to git if no .specify found
638
if git rev-parse --show-toplevel >/dev/null 2>&1; then
739
git rev-parse --show-toplevel
8-
else
9-
# Fall back to script location for non-git repos
10-
local script_dir="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11-
(cd "$script_dir/../../.." && pwd)
40+
return
1241
fi
42+
43+
# Final fallback to script location for non-git repos
44+
local script_dir="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
45+
(cd "$script_dir/../../.." && pwd)
1346
}
1447

1548
# Get current branch, with fallback for non-git repositories
@@ -20,14 +53,14 @@ get_current_branch() {
2053
return
2154
fi
2255

23-
# Then check git if available
24-
if git rev-parse --abbrev-ref HEAD >/dev/null 2>&1; then
25-
git rev-parse --abbrev-ref HEAD
56+
# Then check git if available at the spec-kit root (not parent)
57+
local repo_root=$(get_repo_root)
58+
if has_git; then
59+
git -C "$repo_root" rev-parse --abbrev-ref HEAD
2660
return
2761
fi
2862

2963
# For non-git repos, try to find the latest feature directory
30-
local repo_root=$(get_repo_root)
3164
local specs_dir="$repo_root/specs"
3265

3366
if [[ -d "$specs_dir" ]]; then
@@ -57,9 +90,17 @@ get_current_branch() {
5790
echo "main" # Final fallback
5891
}
5992

60-
# Check if we have git available
93+
# Check if we have git available at the spec-kit root level
94+
# Returns true only if git is installed and the repo root is inside a git work tree
95+
# Handles both regular repos (.git directory) and worktrees/submodules (.git file)
6196
has_git() {
62-
git rev-parse --show-toplevel >/dev/null 2>&1
97+
# First check if git command is available (before calling get_repo_root which may use git)
98+
command -v git >/dev/null 2>&1 || return 1
99+
local repo_root=$(get_repo_root)
100+
# Check if .git exists (directory or file for worktrees/submodules)
101+
[ -e "$repo_root/.git" ] || return 1
102+
# Verify it's actually a valid git work tree
103+
git -C "$repo_root" rev-parse --is-inside-work-tree >/dev/null 2>&1
63104
}
64105

65106
check_feature_branch() {

scripts/bash/create-new-feature.sh

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -71,19 +71,6 @@ if [ -z "$FEATURE_DESCRIPTION" ]; then
7171
exit 1
7272
fi
7373

74-
# Function to find the repository root by searching for existing project markers
75-
find_repo_root() {
76-
local dir="$1"
77-
while [ "$dir" != "/" ]; do
78-
if [ -d "$dir/.git" ] || [ -d "$dir/.specify" ]; then
79-
echo "$dir"
80-
return 0
81-
fi
82-
dir="$(dirname "$dir")"
83-
done
84-
return 1
85-
}
86-
8774
# Function to get highest number from specs directory
8875
get_highest_from_specs() {
8976
local specs_dir="$1"
@@ -220,20 +207,16 @@ branch_exists() {
220207
return 1
221208
}
222209

223-
# Resolve repository root. Prefer git information when available, but fall back
224-
# to searching for repository markers so the workflow still functions in repositories that
225-
# were initialised with --no-git.
226-
# Note: SCRIPT_DIR is already set at the top of this script
210+
# Resolve repository root using common.sh functions which prioritize .specify over git
211+
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
212+
source "$SCRIPT_DIR/common.sh"
227213

228-
if git rev-parse --show-toplevel >/dev/null 2>&1; then
229-
REPO_ROOT=$(git rev-parse --show-toplevel)
214+
REPO_ROOT=$(get_repo_root)
215+
216+
# Check if git is available at this repo root (not a parent)
217+
if has_git; then
230218
HAS_GIT=true
231219
else
232-
REPO_ROOT="$(find_repo_root "$SCRIPT_DIR")"
233-
if [ -z "$REPO_ROOT" ]; then
234-
echo "Error: Could not determine repository root. Please run this script from within the repository." >&2
235-
exit 1
236-
fi
237220
HAS_GIT=false
238221
fi
239222

scripts/powershell/common.ps1

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,38 @@
11
#!/usr/bin/env pwsh
22
# Common PowerShell functions analogous to common.sh
33

4+
# Find repository root by searching upward for .specify directory
5+
# This is the primary marker for spec-kit projects
6+
function Find-SpecifyRoot {
7+
param([string]$StartDir = (Get-Location).Path)
8+
9+
# Normalize to absolute path to prevent issues with relative paths
10+
# Use -LiteralPath to handle paths with wildcard characters ([, ], *, ?)
11+
$current = (Resolve-Path -LiteralPath $StartDir -ErrorAction SilentlyContinue)?.Path
12+
if (-not $current) { return $null }
13+
14+
while ($true) {
15+
if (Test-Path -LiteralPath (Join-Path $current ".specify") -PathType Container) {
16+
return $current
17+
}
18+
$parent = Split-Path $current -Parent
19+
if ([string]::IsNullOrEmpty($parent) -or $parent -eq $current) {
20+
return $null
21+
}
22+
$current = $parent
23+
}
24+
}
25+
26+
# Get repository root, prioritizing .specify directory over git
27+
# This prevents using a parent git repo when spec-kit is initialized in a subdirectory
428
function Get-RepoRoot {
29+
# First, look for .specify directory (spec-kit's own marker)
30+
$specifyRoot = Find-SpecifyRoot
31+
if ($specifyRoot) {
32+
return $specifyRoot
33+
}
34+
35+
# Fallback to git if no .specify found
536
try {
637
$result = git rev-parse --show-toplevel 2>$null
738
if ($LASTEXITCODE -eq 0) {
@@ -10,29 +41,32 @@ function Get-RepoRoot {
1041
} catch {
1142
# Git command failed
1243
}
13-
14-
# Fall back to script location for non-git repos
15-
return (Resolve-Path (Join-Path $PSScriptRoot "../../..")).Path
44+
45+
# Final fallback to script location for non-git repos
46+
# Use -LiteralPath to handle paths with wildcard characters
47+
return (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "../../..")).Path
1648
}
1749

1850
function Get-CurrentBranch {
1951
# First check if SPECIFY_FEATURE environment variable is set
2052
if ($env:SPECIFY_FEATURE) {
2153
return $env:SPECIFY_FEATURE
2254
}
23-
24-
# Then check git if available
25-
try {
26-
$result = git rev-parse --abbrev-ref HEAD 2>$null
27-
if ($LASTEXITCODE -eq 0) {
28-
return $result
55+
56+
# Then check git if available at the spec-kit root (not parent)
57+
$repoRoot = Get-RepoRoot
58+
if (Test-HasGit) {
59+
try {
60+
$result = git -C $repoRoot rev-parse --abbrev-ref HEAD 2>$null
61+
if ($LASTEXITCODE -eq 0) {
62+
return $result
63+
}
64+
} catch {
65+
# Git command failed
2966
}
30-
} catch {
31-
# Git command failed
3267
}
33-
68+
3469
# For non-git repos, try to find the latest feature directory
35-
$repoRoot = Get-RepoRoot
3670
$specsDir = Join-Path $repoRoot "specs"
3771

3872
if (Test-Path $specsDir) {
@@ -58,9 +92,23 @@ function Get-CurrentBranch {
5892
return "main"
5993
}
6094

95+
# Check if we have git available at the spec-kit root level
96+
# Returns true only if git is installed and the repo root is inside a git work tree
97+
# Handles both regular repos (.git directory) and worktrees/submodules (.git file)
6198
function Test-HasGit {
99+
# First check if git command is available (before calling Get-RepoRoot which may use git)
100+
if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
101+
return $false
102+
}
103+
$repoRoot = Get-RepoRoot
104+
# Check if .git exists (directory or file for worktrees/submodules)
105+
# Use -LiteralPath to handle paths with wildcard characters
106+
if (-not (Test-Path -LiteralPath (Join-Path $repoRoot ".git"))) {
107+
return $false
108+
}
109+
# Verify it's actually a valid git work tree
62110
try {
63-
git rev-parse --show-toplevel 2>$null | Out-Null
111+
$null = git -C $repoRoot rev-parse --is-inside-work-tree 2>$null
64112
return ($LASTEXITCODE -eq 0)
65113
} catch {
66114
return $false

scripts/powershell/create-new-feature.ps1

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ if (-not $FeatureDescription -or $FeatureDescription.Count -eq 0) {
3838

3939
$featureDesc = ($FeatureDescription -join ' ').Trim()
4040

41+
# Validate description is not empty after trimming (e.g., user passed only whitespace)
42+
if ([string]::IsNullOrWhiteSpace($featureDesc)) {
43+
Write-Error "Error: Feature description cannot be empty or contain only whitespace"
44+
exit 1
45+
}
46+
4147
# Resolve repository root. Prefer git information when available, but fall back
4248
# to searching for repository markers so the workflow still functions in repositories that
4349
# were initialized with --no-git.
@@ -133,6 +139,7 @@ function ConvertTo-CleanBranchName {
133139
return $Name.ToLower() -replace '[^a-z0-9]', '-' -replace '-{2,}', '-' -replace '^-', '' -replace '-$', ''
134140
}
135141

142+
136143
# Calculate worktree path based on strategy
137144
# Naming convention: <repo_name>-<branch_name> for sibling/custom strategies
138145
function Get-WorktreePath {
@@ -196,23 +203,14 @@ function Test-BranchExists {
196203
return $false
197204
}
198205

199-
$fallbackRoot = (Find-RepositoryRoot -StartDir $PSScriptRoot)
200-
if (-not $fallbackRoot) {
201-
Write-Error "Error: Could not determine repository root. Please run this script from within the repository."
202-
exit 1
203-
}
206+
# Load common functions (includes Get-RepoRoot, Test-HasGit, Resolve-Template)
207+
. "$PSScriptRoot/common.ps1"
204208

205-
try {
206-
$repoRoot = git rev-parse --show-toplevel 2>$null
207-
if ($LASTEXITCODE -eq 0) {
208-
$hasGit = $true
209-
} else {
210-
throw "Git not available"
211-
}
212-
} catch {
213-
$repoRoot = $fallbackRoot
214-
$hasGit = $false
215-
}
209+
# Use common.ps1 functions which prioritize .specify over git
210+
$repoRoot = Get-RepoRoot
211+
212+
# Check if git is available at this repo root (not a parent)
213+
$hasGit = Test-HasGit
216214

217215
Set-Location $repoRoot
218216

0 commit comments

Comments
 (0)