Skip to content

Commit b1ba972

Browse files
mbachorikiamaeroplaneclaude
authored
fix(scripts): prioritize .specify over git for repo root detection (#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 #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 24247c2 commit b1ba972

File tree

4 files changed

+125
-90
lines changed

4 files changed

+125
-90
lines changed

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
@@ -68,9 +101,17 @@ get_current_branch() {
68101
echo "main" # Final fallback
69102
}
70103

71-
# Check if we have git available
104+
# Check if we have git available at the spec-kit root level
105+
# Returns true only if git is installed and the repo root is inside a git work tree
106+
# Handles both regular repos (.git directory) and worktrees/submodules (.git file)
72107
has_git() {
73-
git rev-parse --show-toplevel >/dev/null 2>&1
108+
# First check if git command is available (before calling get_repo_root which may use git)
109+
command -v git >/dev/null 2>&1 || return 1
110+
local repo_root=$(get_repo_root)
111+
# Check if .git exists (directory or file for worktrees/submodules)
112+
[ -e "$repo_root/.git" ] || return 1
113+
# Verify it's actually a valid git work tree
114+
git -C "$repo_root" rev-parse --is-inside-work-tree >/dev/null 2>&1
74115
}
75116

76117
check_feature_branch() {

scripts/bash/create-new-feature.sh

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -80,19 +80,6 @@ if [ -z "$FEATURE_DESCRIPTION" ]; then
8080
exit 1
8181
fi
8282

83-
# Function to find the repository root by searching for existing project markers
84-
find_repo_root() {
85-
local dir="$1"
86-
while [ "$dir" != "/" ]; do
87-
if [ -d "$dir/.git" ] || [ -d "$dir/.specify" ]; then
88-
echo "$dir"
89-
return 0
90-
fi
91-
dir="$(dirname "$dir")"
92-
done
93-
return 1
94-
}
95-
9683
# Function to get highest number from specs directory
9784
get_highest_from_specs() {
9885
local specs_dir="$1"
@@ -171,21 +158,16 @@ clean_branch_name() {
171158
echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//'
172159
}
173160

174-
# Resolve repository root. Prefer git information when available, but fall back
175-
# to searching for repository markers so the workflow still functions in repositories that
176-
# were initialised with --no-git.
161+
# Resolve repository root using common.sh functions which prioritize .specify over git
177162
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
178163
source "$SCRIPT_DIR/common.sh"
179164

180-
if git rev-parse --show-toplevel >/dev/null 2>&1; then
181-
REPO_ROOT=$(git rev-parse --show-toplevel)
165+
REPO_ROOT=$(get_repo_root)
166+
167+
# Check if git is available at this repo root (not a parent)
168+
if has_git; then
182169
HAS_GIT=true
183170
else
184-
REPO_ROOT="$(find_repo_root "$SCRIPT_DIR")"
185-
if [ -z "$REPO_ROOT" ]; then
186-
echo "Error: Could not determine repository root. Please run this script from within the repository." >&2
187-
exit 1
188-
fi
189171
HAS_GIT=false
190172
fi
191173

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) {
@@ -69,9 +103,23 @@ function Get-CurrentBranch {
69103
return "main"
70104
}
71105

106+
# Check if we have git available at the spec-kit root level
107+
# Returns true only if git is installed and the repo root is inside a git work tree
108+
# Handles both regular repos (.git directory) and worktrees/submodules (.git file)
72109
function Test-HasGit {
110+
# First check if git command is available (before calling Get-RepoRoot which may use git)
111+
if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
112+
return $false
113+
}
114+
$repoRoot = Get-RepoRoot
115+
# Check if .git exists (directory or file for worktrees/submodules)
116+
# Use -LiteralPath to handle paths with wildcard characters
117+
if (-not (Test-Path -LiteralPath (Join-Path $repoRoot ".git"))) {
118+
return $false
119+
}
120+
# Verify it's actually a valid git work tree
73121
try {
74-
git rev-parse --show-toplevel 2>$null | Out-Null
122+
$null = git -C $repoRoot rev-parse --is-inside-work-tree 2>$null
75123
return ($LASTEXITCODE -eq 0)
76124
} catch {
77125
return $false

scripts/powershell/create-new-feature.ps1

Lines changed: 6 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -45,30 +45,6 @@ if ([string]::IsNullOrWhiteSpace($featureDesc)) {
4545
exit 1
4646
}
4747

48-
# Resolve repository root. Prefer git information when available, but fall back
49-
# to searching for repository markers so the workflow still functions in repositories that
50-
# were initialized with --no-git.
51-
function Find-RepositoryRoot {
52-
param(
53-
[string]$StartDir,
54-
[string[]]$Markers = @('.git', '.specify')
55-
)
56-
$current = Resolve-Path $StartDir
57-
while ($true) {
58-
foreach ($marker in $Markers) {
59-
if (Test-Path (Join-Path $current $marker)) {
60-
return $current
61-
}
62-
}
63-
$parent = Split-Path $current -Parent
64-
if ($parent -eq $current) {
65-
# Reached filesystem root without finding markers
66-
return $null
67-
}
68-
$current = $parent
69-
}
70-
}
71-
7248
function Get-HighestNumberFromSpecs {
7349
param([string]$SpecsDir)
7450

@@ -139,26 +115,14 @@ function ConvertTo-CleanBranchName {
139115

140116
return $Name.ToLower() -replace '[^a-z0-9]', '-' -replace '-{2,}', '-' -replace '^-', '' -replace '-$', ''
141117
}
142-
$fallbackRoot = (Find-RepositoryRoot -StartDir $PSScriptRoot)
143-
if (-not $fallbackRoot) {
144-
Write-Error "Error: Could not determine repository root. Please run this script from within the repository."
145-
exit 1
146-
}
147-
148-
# Load common functions (includes Resolve-Template)
118+
# Load common functions (includes Get-RepoRoot, Test-HasGit, Resolve-Template)
149119
. "$PSScriptRoot/common.ps1"
150120

151-
try {
152-
$repoRoot = git rev-parse --show-toplevel 2>$null
153-
if ($LASTEXITCODE -eq 0) {
154-
$hasGit = $true
155-
} else {
156-
throw "Git not available"
157-
}
158-
} catch {
159-
$repoRoot = $fallbackRoot
160-
$hasGit = $false
161-
}
121+
# Use common.ps1 functions which prioritize .specify over git
122+
$repoRoot = Get-RepoRoot
123+
124+
# Check if git is available at this repo root (not a parent)
125+
$hasGit = Test-HasGit
162126

163127
Set-Location $repoRoot
164128

0 commit comments

Comments
 (0)