Skip to content
Open
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
1 change: 1 addition & 0 deletions extensions/git/commands/speckit.git.feature.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ You **MUST** consider the user input before proceeding (if not empty).
If the user explicitly provided `GIT_BRANCH_NAME` (e.g., via environment variable, argument, or in their request), pass it through to the script by setting the `GIT_BRANCH_NAME` environment variable before invoking the script. When `GIT_BRANCH_NAME` is set:
- The script uses the exact value as the branch name, bypassing all prefix/suffix generation
- `--short-name`, `--number`, and `--timestamp` flags are ignored
- If the value names the branch that is already checked out, the script succeeds without switching or creating a branch
- `FEATURE_NUM` is extracted when the final path segment starts with a numeric or timestamp feature marker (for example `042-name`, `feat/042-name`, or `jdoe/app/042-name`), otherwise set to the full branch name

## Prerequisites
Expand Down
4 changes: 3 additions & 1 deletion extensions/git/scripts/bash/create-new-feature-branch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -569,7 +569,9 @@ if [ "$DRY_RUN" != true ]; then
if ! branch_create_error=$(git checkout -q -b "$BRANCH_NAME" 2>&1); then
current_branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
if git branch --list "$BRANCH_NAME" | grep -q .; then
if [ "$ALLOW_EXISTING" = true ]; then
# An explicit GIT_BRANCH_NAME naming the checked-out branch is
# already satisfied, so treat it like --allow-existing-branch.
if [ "$ALLOW_EXISTING" = true ] || { [ -n "${GIT_BRANCH_NAME:-}" ] && [ "$current_branch" = "$BRANCH_NAME" ]; }; then
if [ "$current_branch" = "$BRANCH_NAME" ]; then
:
elif ! switch_branch_error=$(git checkout -q "$BRANCH_NAME" 2>&1); then
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,9 @@ if (-not $DryRun) {
try { $currentBranch = (git rev-parse --abbrev-ref HEAD 2>$null).Trim() } catch {}
$existingBranch = git branch --list $branchName 2>$null
if ($existingBranch) {
if ($AllowExistingBranch) {
# An explicit GIT_BRANCH_NAME naming the checked-out branch is
# already satisfied, so treat it like -AllowExistingBranch.
if ($AllowExistingBranch -or ($env:GIT_BRANCH_NAME -and $currentBranch -ceq $branchName)) {
if ($currentBranch -eq $branchName) {
# Already on the target branch
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,11 @@ def build_branch_name(feature_num: str, branch_suffix: str) -> str:
_git_lines(repo_root, "branch", "--list", branch_name)
)
if branch_exists:
if args.allow_existing:
# An explicit GIT_BRANCH_NAME naming the checked-out branch is
# already satisfied, so treat it like --allow-existing-branch.
if args.allow_existing or (
env_branch_name and current_branch == branch_name
):
if current_branch != branch_name:
switch = subprocess.run(
["git", "checkout", "-q", branch_name],
Expand Down
44 changes: 44 additions & 0 deletions tests/extensions/git/test_git_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,28 @@ def test_branch_template_rejects_slug_before_number(self, tmp_path: Path):
assert "{slug}" in result.stderr
assert "{number}" in result.stderr

def test_git_branch_name_override_naming_current_branch_is_noop(self, tmp_path: Path):
"""GIT_BRANCH_NAME equal to the checked-out branch exits 0 without switching."""
project = _setup_project(tmp_path)
subprocess.run(
["git", "checkout", "-q", "-b", "feature/PROJ-42-login"],
cwd=project, check=True,
)
result = _run_bash(
"create-new-feature-branch.sh", project,
"--json", "--short-name", "login", "Login page",
env_extra={"GIT_BRANCH_NAME": "feature/PROJ-42-login"},
)

assert result.returncode == 0, result.stderr
data = json.loads(result.stdout)
assert data["BRANCH_NAME"] == "feature/PROJ-42-login"
head = subprocess.run(
["git", "rev-parse", "--abbrev-ref", "HEAD"],
cwd=project, capture_output=True, text=True, check=True,
).stdout.strip()
assert head == "feature/PROJ-42-login"

def test_git_branch_name_override_extracts_number_after_namespace(self, tmp_path: Path):
"""GIT_BRANCH_NAME extracts FEATURE_NUM from a namespaced branch."""
project = _setup_project(tmp_path / "app-a")
Expand Down Expand Up @@ -920,6 +942,28 @@ def test_git_branch_name_override_ignores_numeric_namespace_segments(self, tmp_p
assert data["BRANCH_NAME"] == "jdoe/2026-app/042-custom-branch"
assert data["FEATURE_NUM"] == "042"

def test_git_branch_name_override_naming_current_branch_is_noop(self, tmp_path: Path):
"""PowerShell GIT_BRANCH_NAME equal to the checked-out branch exits 0 without switching."""
project = _setup_project(tmp_path)
subprocess.run(
["git", "checkout", "-q", "-b", "feature/PROJ-42-login"],
cwd=project, check=True,
)
result = _run_pwsh(
"create-new-feature-branch.ps1", project,
"-Json", "-ShortName", "login", "Login page",
env_extra={"GIT_BRANCH_NAME": "feature/PROJ-42-login"},
)

assert result.returncode == 0, result.stderr
data = json.loads(result.stdout)
assert data["BRANCH_NAME"] == "feature/PROJ-42-login"
head = subprocess.run(
["git", "rev-parse", "--abbrev-ref", "HEAD"],
cwd=project, capture_output=True, text=True, check=True,
).stdout.strip()
assert head == "feature/PROJ-42-login"

def test_git_branch_name_override_without_feature_marker_preserves_full_name(self, tmp_path: Path):
"""PowerShell keeps the full override name when no feature marker exists."""
project = _setup_project(tmp_path / "app-a")
Expand Down
32 changes: 32 additions & 0 deletions tests/extensions/git/test_git_extension_python_parity.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,38 @@ def test_git_branch_name_override(self, tmp_path: Path):
_assert_parity(b, p)
assert json.loads(p.stdout) == {"BRANCH_NAME": "team/042-exact-name", "FEATURE_NUM": "042"}

def test_git_branch_name_override_naming_current_branch_is_noop(
self, tmp_path: Path
):
bash_proj, py_proj = _twin_projects(tmp_path)
env = {"GIT_BRANCH_NAME": "feature/PROJ-42-login"}
for proj in (bash_proj, py_proj):
subprocess.run(
["git", "checkout", "-q", "-b", "feature/PROJ-42-login"],
cwd=proj, check=True,
)
b = _run_bash("create-new-feature-branch.sh", bash_proj, "--json", "desc word", env_extra=env)
p = _run_py("create-new-feature-branch", py_proj, "--json", "desc word", env_extra=env)
_assert_parity(b, p)
assert p.returncode == 0, p.stderr
assert json.loads(p.stdout) == {
"BRANCH_NAME": "feature/PROJ-42-login",
"FEATURE_NUM": "feature/PROJ-42-login",
}

def test_git_branch_name_override_naming_other_existing_branch_fails(
self, tmp_path: Path
):
bash_proj, py_proj = _twin_projects(tmp_path)
env = {"GIT_BRANCH_NAME": "feature/other"}
for proj in (bash_proj, py_proj):
subprocess.run(["git", "branch", "feature/other"], cwd=proj, check=True)
b = _run_bash("create-new-feature-branch.sh", bash_proj, "--json", "desc word", env_extra=env)
p = _run_py("create-new-feature-branch", py_proj, "--json", "desc word", env_extra=env)
_assert_parity(b, p)
assert p.returncode == 1
assert "already exists" in p.stderr

def test_git_branch_name_override_persist_hint_matches_bash(
self, tmp_path: Path
):
Expand Down