-
Notifications
You must be signed in to change notification settings - Fork 5.8k
feat: Add Git worktree-aware workflows #1547
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?
Conversation
Implements three source management modes (branch, worktree, none) with automatic Git environment detection during project initialization.
**Key Changes:**
- Add Git environment detection with auto-mode suggestion
- Detects bare repos, worktrees, normal repos, and non-Git environments
- Suggests appropriate mode: worktree for bare/worktree contexts,
branch for normal repos, none when Git unavailable
- Implement configuration management system
- New config file: .specify/memory/config.json
- Schema validation with version "1.0"
- Conditional field validation (worktree_folder required for worktree mode)
- Backward compatibility: defaults to branch mode if config missing
- Update bash scripts for mode-aware workflows
- create-new-feature.sh: Creates worktrees or branches based on config
- common.sh: Add worktree validation helpers
- PowerShell equivalents updated for Windows support
- Add mode indicators across all commands
- `specify check` displays current mode (Branch/Worktree/None)
- Graceful degradation for invalid configs
- Comprehensive documentation updates
- README: New "Source Management Modes" section with decision matrix
**Testing:**
All 10 test scenarios passed including branch workflows, worktree detection, none mode operation, backward compatibility, invalid config handling, and mode indicator consistency.
**Specification:**
- All 51 tasks completed across 8 phases
- Contracts: detection-api.md, config-api.md
- Full backward compatibility maintained
Co-authored-by: Spec-Driven Development Process
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.
Pull request overview
This PR adds Git worktree support to spec-kit, introducing three source management modes (branch, worktree, none) with automatic Git environment detection. The implementation creates a configuration system that automatically selects an appropriate mode during project initialization based on the detected Git environment.
Changes:
- Added Git environment detection logic to identify bare repositories, worktrees, and standard Git repos, with automatic mode selection
- Implemented configuration management system with JSON schema validation stored in
.specify/memory/config.json - Updated bash and PowerShell scripts to create worktrees or branches based on configured mode, with folder validation and permission checks
- Added mode indicators to
specify checkcommand and enhanced documentation with workflow examples
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 16 comments.
Show a summary per file
| File | Description |
|---|---|
| src/specify_cli/init.py | Adds Git environment detection functions, config management (load/save/validate), automatic mode selection in init command, and mode display in check command |
| scripts/bash/create-new-feature.sh | Reads config to determine mode, creates worktrees with validation instead of branches when in worktree mode, handles none mode by skipping Git operations |
| scripts/powershell/create-new-feature.ps1 | PowerShell equivalent of bash changes with same worktree creation and validation logic |
| scripts/bash/common.sh | Adds worktree/branch name alignment validation for worktree directories |
| README.md | Documents three source management modes with usage examples, configuration format, and workflow patterns |
Comments suppressed due to low confidence (1)
src/specify_cli/init.py:1420
- The config is created after template extraction but before Git initialization. If Git initialization is attempted but fails (lines 1406-1420), the config file will already exist with a mode that may no longer be appropriate. For example, if a user runs init in a normal repo, the config will be set to "branch" mode, but if Git init subsequently fails, the repo might end up in an inconsistent state with the config suggesting Git operations are available.
Consider creating the config after successful Git initialization, or add logic to clean up the config if Git initialization fails.
# Detect Git environment and create config (Feature 001: Worktree Detection)
# Always create config, even with --no-git (will detect "none" mode)
original_cwd = Path.cwd()
os.chdir(project_path)
try:
git_env = detect_git_environment()
suggested_mode = git_env.suggest_mode()
# Create config with suggested mode
config_data = {
"version": "1.0",
"source_management_flow": suggested_mode
}
# Add worktree_folder if in worktree mode
if suggested_mode == "worktree":
config_data["worktree_folder"] = "./worktrees"
save_config(config_data)
finally:
os.chdir(original_cwd)
if not no_git:
tracker.start("git")
if is_git_repo(project_path):
tracker.complete("git", "existing repo detected")
elif should_init_git:
success, error_msg = init_git_repo(project_path, quiet=True)
if success:
tracker.complete("git", "initialized")
else:
tracker.error("git", "init failed")
git_error_message = error_msg
else:
tracker.skip("git", "git not available")
else:
tracker.skip("git", "--no-git flag")
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
This is the Spec Kit output (and the constitution) used to make this feature in case that is interesting to anyone. I used OpenCode and Claude 4.5 Sonnet. |
Addresses multiple UX and error handling issues identified in code review:
1. Add mode mismatch warnings when Git unavailable
- Scripts now warn users when config expects Git-based mode (branch/worktree)
but Git is unavailable
- Provides actionable guidance: what's happening and how to fix it
- Applied to both bash and PowerShell create-new-feature scripts
2. Implement interactive source mode selection
- Users now choose source management mode during `specify init`
- Shows detected Git environment with context
- Displays recommendations based on environment detection
- Allows override via arrow-key selection interface
- Replaces silent automatic configuration
3. Add directory recovery on script errors
- Save original directory at script start
- Restore original directory on any error
- Prevents users being stranded in unexpected locations
- Applied to all critical operations (cd, mkdir, cp, touch)
4. Add worktree path conflict detection
- Pre-check if worktree path exists before creation
- Provide detailed resolution options with specific commands
- Include git worktree remove, alternative branch names, manual deletion
5. Update documentation for interactive mode selection
- Change "automatically detects/selects" to "recommends (you can choose)"
- Add comprehensive "Mode Selection" section with 4-step process
- Include example selection screen
- Clarify user agency in mode choice
6. Remove unused imports
- Remove unused Prompt and Confirm from rich.prompt
- Code uses select_with_arrows (custom) and typer.confirm instead
All changes maintain consistency between bash and PowerShell implementations
and preserve backward compatibility.
Addresses two additional robustness issues identified in code review: 1. Fix bare repository detection in create-new-feature scripts - Changed from 'git rev-parse --show-toplevel' to '--git-dir' - --show-toplevel fails in bare repositories, causing misclassification - Added bare repo check with 'git rev-parse --is-bare-repository' - Use git-dir as root for bare repos, show-toplevel for non-bare - Applied to both bash and PowerShell implementations - Tested and confirmed working in both bare repos and worktrees 2. Add strict config validation for worktree_folder field - Reject worktree_folder when mode is 'branch' or 'none' - Previously allowed unused field in config (confusing/misleading) - Now enforces: worktree_folder only valid in worktree mode - Provides clear error messages explaining the constraint - Prevents configuration inconsistencies Both changes improve error handling and prevent edge case failures in Git repository detection and configuration management.
The check_feature_branch() function displayed 'Warning' but returned failure (exit code 1), creating ambiguity about whether the check was blocking or informational. Since Spec Kit creates all worktrees with matching directory/branch names, any mismatch indicates user deviation from the expected workflow (manual worktree creation, directory renaming, or wrong branch checkout). Changes: - Changed message from 'Warning' to 'ERROR' to match return code - Enhanced error message with clear explanation of Spec Kit's expectation - Provided step-by-step resolution instructions - Maintains blocking behavior (return 1) to prevent script execution in misconfigured worktrees This aligns the semantic meaning with the actual behavior and makes it clear that worktree/branch name alignment is a requirement for Spec Kit workflows, not just a suggestion.
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.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 11 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Check 1: Git installed and in a repo? | ||
| try: | ||
| subprocess.run( | ||
| ["git", "rev-parse", "--is-inside-work-tree"], | ||
| check=True, | ||
| capture_output=True, | ||
| cwd=Path.cwd(), | ||
| ) | ||
| has_git = True | ||
| except (subprocess.CalledProcessError, FileNotFoundError): | ||
| return GitEnvironment(has_git=False, is_bare=False, is_worktree=False) |
Copilot
AI
Feb 1, 2026
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.
In bare repository detection, detect_git_environment() uses git rev-parse --is-inside-work-tree to check for Git availability. However, this command will FAIL in bare repositories (returning non-zero exit code) because bare repositories have no working tree. This means bare repositories will be incorrectly classified as "no Git" (has_git=False).
The function should first check if Git is available at all (using git rev-parse --git-dir or similar), then separately check if it's a bare repository.
| local worktree_dir=$(basename "$(pwd)") | ||
|
|
Copilot
AI
Feb 1, 2026
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.
The worktree validation logic uses basename "$(pwd)" to get the directory name, but this will only work if the user is currently in the worktree directory. If they're in a subdirectory of the worktree (e.g., worktrees/001-feature/src/), this will return the subdirectory name (e.g., "src") instead of the worktree name (e.g., "001-feature"), causing a false mismatch error.
Consider using git rev-parse --show-toplevel to get the worktree root directory before extracting the basename.
| local worktree_dir=$(basename "$(pwd)") | |
| local worktree_root | |
| local worktree_dir | |
| if worktree_root="$(git rev-parse --show-toplevel 2>/dev/null)"; then | |
| worktree_dir="$(basename "$worktree_root")" | |
| else | |
| # Fallback: use current directory name if git metadata is unavailable | |
| worktree_dir="$(basename "$(pwd)")" | |
| fi |
| # Parse JSON config using grep/sed (no jq dependency needed) | ||
| SOURCE_MODE=$(grep -o '"source_management_flow"[[:space:]]*:[[:space:]]*"[^"]*"' "$CONFIG_FILE" | sed 's/.*"\([^"]*\)".*/\1/' || echo "branch") | ||
| WORKTREE_FOLDER=$(grep -o '"worktree_folder"[[:space:]]*:[[:space:]]*"[^"]*"' "$CONFIG_FILE" | sed 's/.*"\([^"]*\)".*/\1/' || echo "") | ||
| fi | ||
|
|
||
| # Fallback to branch mode if config is invalid | ||
| if [ -z "$SOURCE_MODE" ]; then |
Copilot
AI
Feb 1, 2026
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.
The JSON parsing using grep/sed is fragile and may fail with certain valid JSON formats. For example:
- If there's no whitespace after the colon:
"source_management_flow":"branch" - If there are newlines in the string value
- If the field appears in a nested object or comment
The regex pattern [[:space:]]* requires at least the ability to match zero spaces, but the overall pattern may still fail. Consider using a more robust approach or adding validation that the parsed value is one of the expected modes (branch/worktree/none).
| # Parse JSON config using grep/sed (no jq dependency needed) | |
| SOURCE_MODE=$(grep -o '"source_management_flow"[[:space:]]*:[[:space:]]*"[^"]*"' "$CONFIG_FILE" | sed 's/.*"\([^"]*\)".*/\1/' || echo "branch") | |
| WORKTREE_FOLDER=$(grep -o '"worktree_folder"[[:space:]]*:[[:space:]]*"[^"]*"' "$CONFIG_FILE" | sed 's/.*"\([^"]*\)".*/\1/' || echo "") | |
| fi | |
| # Fallback to branch mode if config is invalid | |
| if [ -z "$SOURCE_MODE" ]; then | |
| # Parse JSON config using python's json module for robustness | |
| if CONFIG_VALUES=$(python3 - <<'PY' "$CONFIG_FILE" 2>/dev/null | |
| import json | |
| import sys | |
| path = sys.argv[1] | |
| try: | |
| with open(path, 'r', encoding='utf-8') as f: | |
| data = json.load(f) | |
| except Exception: | |
| # On any parsing/IO error, fall back to defaults handled in shell | |
| print("branch") | |
| print("") | |
| sys.exit(0) | |
| source_mode = data.get("source_management_flow", "branch") | |
| worktree_folder = data.get("worktree_folder", "") | |
| # Ensure we always print exactly two lines | |
| print(source_mode if isinstance(source_mode, str) else "branch") | |
| print(worktree_folder if isinstance(worktree_folder, str) else "") | |
| PY | |
| ); then | |
| SOURCE_MODE=$(printf '%s\n' "$CONFIG_VALUES" | sed -n '1p') | |
| WORKTREE_FOLDER=$(printf '%s\n' "$CONFIG_VALUES" | sed -n '2p') | |
| fi | |
| fi | |
| # Fallback to branch mode if config is invalid or unexpected | |
| if [ -z "$SOURCE_MODE" ] || { [ "$SOURCE_MODE" != "branch" ] && [ "$SOURCE_MODE" != "worktree" ] && [ "$SOURCE_MODE" != "none" ]; }; then |
| console.print(f"[red]Error:[/red] Invalid config file: {e}") | ||
| console.print(f"[yellow]Config location:[/yellow] {get_config_path(path)}") | ||
| console.print( | ||
| "[dim]Delete the config file and run 'specify init' to recreate it.[/dim]" | ||
| ) |
Copilot
AI
Feb 1, 2026
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.
The get_current_mode function respects warn_if_missing for missing configs but always prints error messages for invalid configs (lines 845-849), even when warn_if_missing=False. This means the check command (which calls get_current_mode(warn_if_missing=False)) will still display config validation errors, which might be unexpected or distracting when users just want to check tool availability.
Consider adding a parameter like warn_on_error to control whether validation errors should be printed, or at least respect the warn_if_missing flag for all output.
| console.print(f"[red]Error:[/red] Invalid config file: {e}") | |
| console.print(f"[yellow]Config location:[/yellow] {get_config_path(path)}") | |
| console.print( | |
| "[dim]Delete the config file and run 'specify init' to recreate it.[/dim]" | |
| ) | |
| if warn_if_missing: | |
| console.print(f"[red]Error:[/red] Invalid config file: {e}") | |
| console.print(f"[yellow]Config location:[/yellow] {get_config_path(path)}") | |
| console.print( | |
| "[dim]Delete the config file and run 'specify init' to recreate it.[/dim]" | |
| ) |
| # Create worktree with new branch | ||
| git worktree add -b "$BRANCH_NAME" "$WORKTREE_PATH" || { | ||
| >&2 echo "[specify] Error: Failed to create worktree" | ||
| exit 1 |
Copilot
AI
Feb 1, 2026
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.
When git worktree add -b fails after creating the branch but before completing the worktree setup (e.g., due to filesystem issues), the branch may be left dangling without a corresponding worktree. The error handler at line 356-358 doesn't attempt to clean up the branch in such cases.
Consider adding cleanup logic to remove the newly created branch if worktree creation fails, or at least document this potential state in the error message.
| # Create worktree with new branch | |
| git worktree add -b "$BRANCH_NAME" "$WORKTREE_PATH" || { | |
| >&2 echo "[specify] Error: Failed to create worktree" | |
| exit 1 | |
| # Track whether the branch existed before creating the worktree | |
| if git show-ref --verify --quiet "refs/heads/$BRANCH_NAME"; then | |
| BRANCH_ALREADY_EXISTS=true | |
| else | |
| BRANCH_ALREADY_EXISTS=false | |
| fi | |
| # Create worktree with new branch | |
| git worktree add -b "$BRANCH_NAME" "$WORKTREE_PATH" || { | |
| worktree_status=$? | |
| >&2 echo "[specify] Error: Failed to create worktree at: $WORKTREE_PATH" | |
| # If the branch did not exist before, but exists now, attempt cleanup | |
| if [ "$BRANCH_ALREADY_EXISTS" = "false" ]; then | |
| if git show-ref --verify --quiet "refs/heads/$BRANCH_NAME"; then | |
| >&2 echo "[specify] Attempting to delete dangling branch '$BRANCH_NAME' created during failed worktree setup..." | |
| if ! git branch -D "$BRANCH_NAME" >/dev/null 2>&1; then | |
| >&2 echo "[specify] Warning: Failed to delete dangling branch '$BRANCH_NAME'." | |
| >&2 echo "[specify] You may need to delete it manually: git branch -D \"$BRANCH_NAME\"" | |
| fi | |
| fi | |
| fi | |
| exit "$worktree_status" |
| exit 1 | ||
| } | ||
|
|
||
| TEMPLATE="$REPO_ROOT/.specify/templates/spec-template.md" |
Copilot
AI
Feb 1, 2026
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.
After creating a worktree and changing directory to it, REPO_ROOT is updated to the worktree path (line 371). This causes the template lookup at line 403 to search for .specify/templates/spec-template.md inside the worktree directory rather than the main repository. Since .specify/ doesn't exist in worktrees, the template will never be found, and an empty spec.md file will be created instead.
Consider preserving the original repository root in a separate variable (e.g., MAIN_REPO_ROOT) so that the template can be found regardless of the current working directory.
| TEMPLATE="$REPO_ROOT/.specify/templates/spec-template.md" | |
| # Use the main repository root when resolving the template path so that | |
| # worktree mode still finds the shared .specify directory in the main repo. | |
| if [ "$SOURCE_MODE" = "worktree" ] && [ "$HAS_GIT" = true ]; then | |
| MAIN_REPO_ROOT="$ORIGINAL_DIR" | |
| else | |
| MAIN_REPO_ROOT="$REPO_ROOT" | |
| fi | |
| TEMPLATE="$MAIN_REPO_ROOT/.specify/templates/spec-template.md" |
| exit 1 | ||
| } | ||
|
|
||
| $template = Join-Path $repoRoot '.specify/templates/spec-template.md' |
Copilot
AI
Feb 1, 2026
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.
Same issue as in the bash script: after creating a worktree and updating $repoRoot to the worktree path (line 359), the template lookup at line 402 searches for .specify/templates/spec-template.md inside the worktree directory rather than the main repository. Since .specify/ doesn't exist in worktrees, the template will never be found.
Consider preserving the original repository root in a separate variable so that the template can be found regardless of the current working directory.
| $template = Join-Path $repoRoot '.specify/templates/spec-template.md' | |
| # Use the main repository root for templates, even when using a worktree | |
| if ($sourceMode -eq 'worktree' -and $hasGit) { | |
| $templateRepoRoot = $originalDir | |
| } else { | |
| $templateRepoRoot = $repoRoot | |
| } | |
| $template = Join-Path $templateRepoRoot '.specify/templates/spec-template.md' |
| # Update SPECS_DIR if we're in a worktree | ||
| if [ "$SOURCE_MODE" = "worktree" ] && [ "$HAS_GIT" = true ]; then | ||
| SPECS_DIR="$REPO_ROOT/specs" | ||
| fi | ||
|
|
Copilot
AI
Feb 1, 2026
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.
This line appears to be redundant. The code only reaches this point in worktree mode if Git is available (checked at line 303), and REPO_ROOT has already been updated to WORKTREE_PATH at line 371. So this line is just setting SPECS_DIR to $WORKTREE_PATH/specs, which is already the correct value. The condition at line 392 should never be false when this code is reached in worktree mode, making this update unnecessary.
| # Update SPECS_DIR if we're in a worktree | |
| if [ "$SOURCE_MODE" = "worktree" ] && [ "$HAS_GIT" = true ]; then | |
| SPECS_DIR="$REPO_ROOT/specs" | |
| fi |
| git worktree add -b "$BRANCH_NAME" "$WORKTREE_PATH" || { | ||
| >&2 echo "[specify] Error: Failed to create worktree" | ||
| exit 1 | ||
| } | ||
|
|
Copilot
AI
Feb 1, 2026
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.
The error message "Failed to create worktree" is too generic. If the branch name already exists, Git will return a specific error, but this handler suppresses that detail. Users won't know whether the failure is due to:
- Branch name already exists
- Filesystem/permission issues
- Invalid Git state
- Other Git errors
Consider capturing and displaying the actual Git error message to help users diagnose the problem.
| git worktree add -b "$BRANCH_NAME" "$WORKTREE_PATH" || { | |
| >&2 echo "[specify] Error: Failed to create worktree" | |
| exit 1 | |
| } | |
| if ! output=$(git worktree add -b "$BRANCH_NAME" "$WORKTREE_PATH" 2>&1); then | |
| >&2 echo "[specify] Error: Failed to create worktree" | |
| >&2 echo "[specify] Git reported:" | |
| >&2 echo "$output" | |
| exit 1 | |
| fi | |
| # Re-emit git output on success (if any) | |
| if [ -n "$output" ]; then | |
| echo "$output" | |
| fi |
| # Detect Git environment and create config (Feature 001: Worktree Detection) | ||
| # Always create config, even with --no-git (will detect "none" mode) | ||
| original_cwd = Path.cwd() | ||
| os.chdir(project_path) | ||
| try: | ||
| git_env = detect_git_environment() |
Copilot
AI
Feb 1, 2026
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.
Git environment detection happens after template extraction (line 1620), when the script has already changed directory to the project path (line 1618). This means:
- If using
--herein a bare repository, after template extraction, the directory will have working files and won't be detected as bare anymore - The detection is based on the state AFTER initialization, not BEFORE
This could lead to incorrect mode suggestions. For example, if a user runs specify init --here in a bare repository, the template extraction adds working files, making it no longer bare. The detection will then suggest "branch" mode instead of "worktree" mode.
Consider detecting the Git environment BEFORE template extraction, or detecting it relative to the original working directory before changing to project_path.
Fixes #945, #61
Implements three source management modes (branch, worktree, none) with automatic Git environment detection during project initialization.
Key Changes:
specify checkdisplays current mode (Branch/Worktree/None)Testing:
All 10 test scenarios passed including branch workflows, worktree detection, none mode operation, backward compatibility, invalid config handling, and mode indicator consistency.
Specification:
Co-authored-by: Spec-Driven Development Process