diff --git a/scripts/bash/create-new-feature.sh b/scripts/bash/create-new-feature.sh index a393edd32..54ba1dbf5 100644 --- a/scripts/bash/create-new-feature.sh +++ b/scripts/bash/create-new-feature.sh @@ -3,6 +3,7 @@ set -e JSON_MODE=false +ALLOW_EXISTING=false SHORT_NAME="" BRANCH_NUMBER="" USE_TIMESTAMP=false @@ -14,6 +15,9 @@ while [ $i -le $# ]; do --json) JSON_MODE=true ;; + --allow-existing-branch) + ALLOW_EXISTING=true + ;; --short-name) if [ $((i + 1)) -gt $# ]; then echo 'Error: --short-name requires a value' >&2 @@ -45,10 +49,11 @@ while [ $i -le $# ]; do USE_TIMESTAMP=true ;; --help|-h) - echo "Usage: $0 [--json] [--short-name ] [--number N] [--timestamp] " + echo "Usage: $0 [--json] [--allow-existing-branch] [--short-name ] [--number N] [--timestamp] " echo "" echo "Options:" echo " --json Output in JSON format" + echo " --allow-existing-branch Switch to branch if it already exists instead of failing" echo " --short-name Provide a custom short name (2-4 words) for the branch" echo " --number N Specify branch number manually (overrides auto-detection)" echo " --timestamp Use timestamp prefix (YYYYMMDD-HHMMSS) instead of sequential numbering" @@ -69,7 +74,7 @@ done FEATURE_DESCRIPTION="${ARGS[*]}" if [ -z "$FEATURE_DESCRIPTION" ]; then - echo "Usage: $0 [--json] [--short-name ] [--number N] [--timestamp] " >&2 + echo "Usage: $0 [--json] [--allow-existing-branch] [--short-name ] [--number N] [--timestamp] " >&2 exit 1 fi @@ -287,12 +292,19 @@ if [ "$HAS_GIT" = true ]; then if ! git checkout -b "$BRANCH_NAME" 2>/dev/null; then # Check if branch already exists if git branch --list "$BRANCH_NAME" | grep -q .; then - if [ "$USE_TIMESTAMP" = true ]; then + if [ "$ALLOW_EXISTING" = true ]; then + # Switch to the existing branch instead of failing + if ! git checkout "$BRANCH_NAME" 2>/dev/null; then + >&2 echo "Error: Failed to switch to existing branch '$BRANCH_NAME'. Please resolve any local changes or conflicts and try again." + exit 1 + fi + elif [ "$USE_TIMESTAMP" = true ]; then >&2 echo "Error: Branch '$BRANCH_NAME' already exists. Rerun to get a new timestamp or use a different --short-name." + exit 1 else >&2 echo "Error: Branch '$BRANCH_NAME' already exists. Please use a different feature name or specify a different number with --number." + exit 1 fi - exit 1 else >&2 echo "Error: Failed to create git branch '$BRANCH_NAME'. Please check your git configuration and try again." exit 1 @@ -305,13 +317,15 @@ fi FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME" mkdir -p "$FEATURE_DIR" -TEMPLATE=$(resolve_template "spec-template" "$REPO_ROOT") || true SPEC_FILE="$FEATURE_DIR/spec.md" -if [ -n "$TEMPLATE" ] && [ -f "$TEMPLATE" ]; then - cp "$TEMPLATE" "$SPEC_FILE" -else - echo "Warning: Spec template not found; created empty spec file" >&2 - touch "$SPEC_FILE" +if [ ! -f "$SPEC_FILE" ]; then + TEMPLATE=$(resolve_template "spec-template" "$REPO_ROOT") || true + if [ -n "$TEMPLATE" ] && [ -f "$TEMPLATE" ]; then + cp "$TEMPLATE" "$SPEC_FILE" + else + echo "Warning: Spec template not found; created empty spec file" >&2 + touch "$SPEC_FILE" + fi fi # Inform the user how to persist the feature variable in their own shell diff --git a/scripts/powershell/create-new-feature.ps1 b/scripts/powershell/create-new-feature.ps1 index b1ca0ac82..3708ea2db 100644 --- a/scripts/powershell/create-new-feature.ps1 +++ b/scripts/powershell/create-new-feature.ps1 @@ -3,6 +3,7 @@ [CmdletBinding()] param( [switch]$Json, + [switch]$AllowExistingBranch, [string]$ShortName, [Parameter()] [long]$Number = 0, @@ -15,10 +16,11 @@ $ErrorActionPreference = 'Stop' # Show help if requested if ($Help) { - Write-Host "Usage: ./create-new-feature.ps1 [-Json] [-ShortName ] [-Number N] [-Timestamp] " + Write-Host "Usage: ./create-new-feature.ps1 [-Json] [-AllowExistingBranch] [-ShortName ] [-Number N] [-Timestamp] " Write-Host "" Write-Host "Options:" Write-Host " -Json Output in JSON format" + Write-Host " -AllowExistingBranch Switch to branch if it already exists instead of failing" Write-Host " -ShortName Provide a custom short name (2-4 words) for the branch" Write-Host " -Number N Specify branch number manually (overrides auto-detection)" Write-Host " -Timestamp Use timestamp prefix (YYYYMMDD-HHMMSS) instead of sequential numbering" @@ -33,7 +35,7 @@ if ($Help) { # Check if feature description provided if (-not $FeatureDescription -or $FeatureDescription.Count -eq 0) { - Write-Error "Usage: ./create-new-feature.ps1 [-Json] [-ShortName ] [-Number N] [-Timestamp] " + Write-Error "Usage: ./create-new-feature.ps1 [-Json] [-AllowExistingBranch] [-ShortName ] [-Number N] [-Timestamp] " exit 1 } @@ -251,12 +253,20 @@ if ($hasGit) { # Check if branch already exists $existingBranch = git branch --list $branchName 2>$null if ($existingBranch) { - if ($Timestamp) { + if ($AllowExistingBranch) { + # Switch to the existing branch instead of failing + git checkout -q $branchName 2>$null | Out-Null + if ($LASTEXITCODE -ne 0) { + Write-Error "Error: Branch '$branchName' exists but could not be checked out. Resolve any uncommitted changes or conflicts and try again." + exit 1 + } + } elseif ($Timestamp) { Write-Error "Error: Branch '$branchName' already exists. Rerun to get a new timestamp or use a different -ShortName." + exit 1 } else { Write-Error "Error: Branch '$branchName' already exists. Please use a different feature name or specify a different number with -Number." + exit 1 } - exit 1 } else { Write-Error "Error: Failed to create git branch '$branchName'. Please check your git configuration and try again." exit 1 @@ -269,12 +279,14 @@ if ($hasGit) { $featureDir = Join-Path $specsDir $branchName New-Item -ItemType Directory -Path $featureDir -Force | Out-Null -$template = Resolve-Template -TemplateName 'spec-template' -RepoRoot $repoRoot $specFile = Join-Path $featureDir 'spec.md' -if ($template -and (Test-Path $template)) { - Copy-Item $template $specFile -Force -} else { - New-Item -ItemType File -Path $specFile | Out-Null +if (-not (Test-Path -PathType Leaf $specFile)) { + $template = Resolve-Template -TemplateName 'spec-template' -RepoRoot $repoRoot + if ($template -and (Test-Path $template)) { + Copy-Item $template $specFile -Force + } else { + New-Item -ItemType File -Path $specFile | Out-Null + } } # Set the SPECIFY_FEATURE environment variable for the current session diff --git a/tests/test_timestamp_branches.py b/tests/test_timestamp_branches.py index 7e4f88ed0..0c9eb07b4 100644 --- a/tests/test_timestamp_branches.py +++ b/tests/test_timestamp_branches.py @@ -269,3 +269,146 @@ def test_e2e_sequential(self, git_repo: Path): assert (git_repo / "specs" / branch).is_dir() val = source_and_call(f'check_feature_branch "{branch}" "true"') assert val.returncode == 0 + + +# ── Allow Existing Branch Tests ────────────────────────────────────────────── + + +class TestAllowExistingBranch: + def test_allow_existing_switches_to_branch(self, git_repo: Path): + """T006: Pre-create branch, verify script switches to it.""" + subprocess.run( + ["git", "checkout", "-b", "004-pre-exist"], + cwd=git_repo, check=True, capture_output=True, + ) + subprocess.run( + ["git", "checkout", "-"], + cwd=git_repo, check=True, capture_output=True, + ) + result = run_script( + git_repo, "--allow-existing-branch", "--short-name", "pre-exist", + "--number", "4", "Pre-existing feature", + ) + assert result.returncode == 0, result.stderr + current = subprocess.run( + ["git", "rev-parse", "--abbrev-ref", "HEAD"], + cwd=git_repo, capture_output=True, text=True, + ).stdout.strip() + assert current == "004-pre-exist", f"expected 004-pre-exist, got {current}" + + def test_allow_existing_already_on_branch(self, git_repo: Path): + """T007: Verify success when already on the target branch.""" + subprocess.run( + ["git", "checkout", "-b", "005-already-on"], + cwd=git_repo, check=True, capture_output=True, + ) + result = run_script( + git_repo, "--allow-existing-branch", "--short-name", "already-on", + "--number", "5", "Already on branch", + ) + assert result.returncode == 0, result.stderr + + def test_allow_existing_creates_spec_dir(self, git_repo: Path): + """T008: Verify spec directory created on existing branch.""" + subprocess.run( + ["git", "checkout", "-b", "006-spec-dir"], + cwd=git_repo, check=True, capture_output=True, + ) + subprocess.run( + ["git", "checkout", "-"], + cwd=git_repo, check=True, capture_output=True, + ) + result = run_script( + git_repo, "--allow-existing-branch", "--short-name", "spec-dir", + "--number", "6", "Spec dir feature", + ) + assert result.returncode == 0, result.stderr + assert (git_repo / "specs" / "006-spec-dir").is_dir() + assert (git_repo / "specs" / "006-spec-dir" / "spec.md").exists() + + def test_without_flag_still_errors(self, git_repo: Path): + """T009: Verify backwards compatibility (error without flag).""" + subprocess.run( + ["git", "checkout", "-b", "007-no-flag"], + cwd=git_repo, check=True, capture_output=True, + ) + subprocess.run( + ["git", "checkout", "-"], + cwd=git_repo, check=True, capture_output=True, + ) + result = run_script( + git_repo, "--short-name", "no-flag", "--number", "7", "No flag feature", + ) + assert result.returncode != 0, "should fail without --allow-existing-branch" + assert "already exists" in result.stderr + + def test_allow_existing_no_overwrite_spec(self, git_repo: Path): + """T010: Pre-create spec.md with content, verify it is preserved.""" + subprocess.run( + ["git", "checkout", "-b", "008-no-overwrite"], + cwd=git_repo, check=True, capture_output=True, + ) + spec_dir = git_repo / "specs" / "008-no-overwrite" + spec_dir.mkdir(parents=True) + spec_file = spec_dir / "spec.md" + spec_file.write_text("# My custom spec content\n") + subprocess.run( + ["git", "checkout", "-"], + cwd=git_repo, check=True, capture_output=True, + ) + result = run_script( + git_repo, "--allow-existing-branch", "--short-name", "no-overwrite", + "--number", "8", "No overwrite feature", + ) + assert result.returncode == 0, result.stderr + assert spec_file.read_text() == "# My custom spec content\n" + + def test_allow_existing_creates_branch_if_not_exists(self, git_repo: Path): + """T011: Verify normal creation when branch doesn't exist.""" + result = run_script( + git_repo, "--allow-existing-branch", "--short-name", "new-branch", + "New branch feature", + ) + assert result.returncode == 0, result.stderr + current = subprocess.run( + ["git", "rev-parse", "--abbrev-ref", "HEAD"], + cwd=git_repo, capture_output=True, text=True, + ).stdout.strip() + assert "new-branch" in current + + def test_allow_existing_with_json(self, git_repo: Path): + """T012: Verify JSON output is correct.""" + import json + + subprocess.run( + ["git", "checkout", "-b", "009-json-test"], + cwd=git_repo, check=True, capture_output=True, + ) + subprocess.run( + ["git", "checkout", "-"], + cwd=git_repo, check=True, capture_output=True, + ) + result = run_script( + git_repo, "--allow-existing-branch", "--json", "--short-name", "json-test", + "--number", "9", "JSON test", + ) + assert result.returncode == 0, result.stderr + data = json.loads(result.stdout) + assert data["BRANCH_NAME"] == "009-json-test" + + def test_allow_existing_no_git(self, no_git_dir: Path): + """T013: Verify flag is silently ignored in non-git repos.""" + result = run_script( + no_git_dir, "--allow-existing-branch", "--short-name", "no-git", + "No git feature", + ) + assert result.returncode == 0, result.stderr + + +class TestAllowExistingBranchPowerShell: + def test_powershell_supports_allow_existing_branch_flag(self): + """Static guard: PS script exposes and uses -AllowExistingBranch.""" + contents = CREATE_FEATURE_PS.read_text(encoding="utf-8") + assert "-AllowExistingBranch" in contents + # Ensure the flag is referenced in script logic, not just declared + assert "AllowExistingBranch" in contents.replace("-AllowExistingBranch", "")