-
Notifications
You must be signed in to change notification settings - Fork 5.9k
feat: Add SPECIFY_SPECS_DIR for centralized specs directory and worktree support #1579
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
4f6b64b
f1292f5
fdf0510
8d99e69
71aa427
b2e9c95
296517a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,23 @@ | ||
| #!/usr/bin/env bash | ||
| # Common functions and variables for all scripts | ||
|
|
||
| # Escape a string for safe inclusion in JSON (handles all JSON-required escapes) | ||
| json_escape() { | ||
| local str="$1" | ||
| # Escape backslashes first, then quotes, then control characters | ||
| str="${str//\\/\\\\}" | ||
| str="${str//\"/\\\"}" | ||
| str="${str//$'\n'/\\n}" | ||
| str="${str//$'\r'/\\r}" | ||
| str="${str//$'\t'/\\t}" | ||
| str="${str//$'\b'/\\b}" | ||
| str="${str//$'\f'/\\f}" | ||
| # Remove any remaining control characters (U+0000-U+001F) that are | ||
| # not covered above, since they are invalid unescaped in JSON strings. | ||
| str="$(printf '%s' "$str" | tr -d '\000-\006\016-\037')" | ||
| printf '%s' "$str" | ||
| } | ||
|
|
||
| # Get repository root, with fallback for non-git repositories | ||
| get_repo_root() { | ||
| if git rev-parse --show-toplevel >/dev/null 2>&1; then | ||
|
|
@@ -12,6 +29,24 @@ get_repo_root() { | |
| fi | ||
| } | ||
|
|
||
| # Get specs directory, with support for external location via SPECIFY_SPECS_DIR | ||
| get_specs_dir() { | ||
| local repo_root="${1:-$(get_repo_root)}" | ||
| local specs_dir | ||
|
|
||
| if [[ -n "${SPECIFY_SPECS_DIR:-}" ]]; then | ||
| specs_dir="$SPECIFY_SPECS_DIR" | ||
| # Resolve relative paths against repo root | ||
| if [[ "$specs_dir" != /* ]]; then | ||
| specs_dir="$repo_root/$specs_dir" | ||
| fi | ||
| else | ||
| specs_dir="$repo_root/specs" | ||
| fi | ||
|
|
||
| echo "$specs_dir" | ||
| } | ||
|
Comment on lines
+33
to
+48
|
||
|
|
||
| # Get current branch, with fallback for non-git repositories | ||
| get_current_branch() { | ||
| # First check if SPECIFY_FEATURE environment variable is set | ||
|
|
@@ -28,7 +63,7 @@ get_current_branch() { | |
|
|
||
| # For non-git repos, try to find the latest feature directory | ||
| local repo_root=$(get_repo_root) | ||
| local specs_dir="$repo_root/specs" | ||
| local specs_dir="$(get_specs_dir "$repo_root")" | ||
|
|
||
| if [[ -d "$specs_dir" ]]; then | ||
| local latest_feature="" | ||
|
|
@@ -66,6 +101,12 @@ check_feature_branch() { | |
| local branch="$1" | ||
| local has_git_repo="$2" | ||
|
|
||
| # When SPECIFY_SPECS_DIR is set (e.g., worktree mode), skip branch naming | ||
| # validation since the branch/worktree may not follow the NNN- convention. | ||
| if [[ -n "${SPECIFY_SPECS_DIR:-}" ]]; then | ||
| return 0 | ||
| fi | ||
|
|
||
| # For non-git repos, we can't enforce branch naming but still provide output | ||
| if [[ "$has_git_repo" != "true" ]]; then | ||
| echo "[specify] Warning: Git repository not detected; skipped branch validation" >&2 | ||
|
|
@@ -81,14 +122,14 @@ check_feature_branch() { | |
| return 0 | ||
| } | ||
|
|
||
| get_feature_dir() { echo "$1/specs/$2"; } | ||
| get_feature_dir() { echo "$(get_specs_dir "$1")/$2"; } | ||
|
|
||
| # Find feature directory by numeric prefix instead of exact branch match | ||
| # This allows multiple branches to work on the same spec (e.g., 004-fix-bug, 004-add-feature) | ||
| find_feature_dir_by_prefix() { | ||
| local repo_root="$1" | ||
| local branch_name="$2" | ||
| local specs_dir="$repo_root/specs" | ||
| local specs_dir="$(get_specs_dir "$repo_root")" | ||
|
|
||
| # Extract numeric prefix from branch (e.g., "004" from "004-whatever") | ||
| if [[ ! "$branch_name" =~ ^([0-9]{3})- ]]; then | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -15,6 +15,21 @@ function Get-RepoRoot { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return (Resolve-Path (Join-Path $PSScriptRoot "../../..")).Path | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Get specs directory, with support for external location via SPECIFY_SPECS_DIR | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function Get-SpecsDir { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| param([string]$RepoRoot = (Get-RepoRoot)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ($env:SPECIFY_SPECS_DIR) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $specsDir = $env:SPECIFY_SPECS_DIR | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Resolve relative paths against repo root | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (-not [System.IO.Path]::IsPathRooted($specsDir)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $specsDir = Join-Path $RepoRoot $specsDir | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return $specsDir | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Join-Path $RepoRoot "specs" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
alanmeadows marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
+22
to
+30
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ($env:SPECIFY_SPECS_DIR) { | |
| $specsDir = $env:SPECIFY_SPECS_DIR | |
| # Resolve relative paths against repo root | |
| if (-not [System.IO.Path]::IsPathRooted($specsDir)) { | |
| $specsDir = Join-Path $RepoRoot $specsDir | |
| } | |
| return $specsDir | |
| } | |
| return Join-Path $RepoRoot "specs" | |
| $specsDir = $null | |
| if ($env:SPECIFY_SPECS_DIR) { | |
| $specsDir = $env:SPECIFY_SPECS_DIR | |
| # Resolve relative paths against repo root | |
| if (-not [System.IO.Path]::IsPathRooted($specsDir)) { | |
| $specsDir = Join-Path $RepoRoot $specsDir | |
| } | |
| } else { | |
| $specsDir = Join-Path $RepoRoot "specs" | |
| } | |
| # Validate that, if the path exists, it is a directory | |
| if (Test-Path $specsDir) { | |
| if (-not (Test-Path $specsDir -PathType Container)) { | |
| Write-Error "Invalid specs directory path '$specsDir': path exists but is not a directory." | |
| return $null | |
| } | |
| } | |
| return $specsDir |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SPECIFY_SPECS_DIRis user-controlled and can now influence values emitted byget_feature_paths(which are later consumed viaeval $(get_feature_paths)in multiple scripts). Becauseget_feature_pathswraps values in single quotes, aSPECIFY_SPECS_DIRcontaining a'can break out of quoting and lead to command injection. Consider switching away fromeval-based exports, or ensure values are safely shell-escaped (e.g., escape single quotes) before being embedded in theget_feature_pathsoutput.