Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 24 additions & 10 deletions scripts/bash/create-new-feature.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
set -e

JSON_MODE=false
ALLOW_EXISTING=false
SHORT_NAME=""
BRANCH_NUMBER=""
USE_TIMESTAMP=false
Expand All @@ -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
Expand Down Expand Up @@ -45,10 +49,11 @@ while [ $i -le $# ]; do
USE_TIMESTAMP=true
;;
--help|-h)
echo "Usage: $0 [--json] [--short-name <name>] [--number N] [--timestamp] <feature_description>"
echo "Usage: $0 [--json] [--allow-existing-branch] [--short-name <name>] [--number N] [--timestamp] <feature_description>"
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 <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"
Expand All @@ -69,7 +74,7 @@ done

FEATURE_DESCRIPTION="${ARGS[*]}"
if [ -z "$FEATURE_DESCRIPTION" ]; then
echo "Usage: $0 [--json] [--short-name <name>] [--number N] [--timestamp] <feature_description>" >&2
echo "Usage: $0 [--json] [--allow-existing-branch] [--short-name <name>] [--number N] [--timestamp] <feature_description>" >&2
exit 1
fi

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
30 changes: 21 additions & 9 deletions scripts/powershell/create-new-feature.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
[CmdletBinding()]
param(
[switch]$Json,
[switch]$AllowExistingBranch,
[string]$ShortName,
[Parameter()]
[long]$Number = 0,
Expand All @@ -15,10 +16,11 @@ $ErrorActionPreference = 'Stop'

# Show help if requested
if ($Help) {
Write-Host "Usage: ./create-new-feature.ps1 [-Json] [-ShortName <name>] [-Number N] [-Timestamp] <feature description>"
Write-Host "Usage: ./create-new-feature.ps1 [-Json] [-AllowExistingBranch] [-ShortName <name>] [-Number N] [-Timestamp] <feature description>"
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 <name> 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"
Expand All @@ -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 <name>] [-Number N] [-Timestamp] <feature description>"
Write-Error "Usage: ./create-new-feature.ps1 [-Json] [-AllowExistingBranch] [-ShortName <name>] [-Number N] [-Timestamp] <feature description>"
exit 1
}

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
143 changes: 143 additions & 0 deletions tests/test_timestamp_branches.py
Original file line number Diff line number Diff line change
Expand Up @@ -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", "")
Loading