mirror of
https://github.com/github/spec-kit.git
synced 2026-07-08 07:04:44 +08:00
Support namespaced git feature branch templates (#3293)
* test: cover namespaced git branch templates
Assisted-by: Codex (model: GPT-5, autonomous)
* feat: support namespaced git branch templates
Assisted-by: Codex (model: GPT-5, autonomous)
* test: cover git branch template edge cases
Assisted-by: Codex (model: GPT-5, autonomous)
* fix: harden git branch template parsing
Assisted-by: Codex (model: GPT-5, autonomous)
* fix: address git branch template review feedback
Address Copilot review feedback for branch_prefix help text, namespaced GIT_BRANCH_NAME fallback behavior, final-segment validation docs, and Bash UTF-8 byte reporting.
Assisted-by: Codex (model: GPT-5, autonomous)
* fix: reject slug-scoped branch templates
Reject branch templates that place {slug} before {number}, because that makes namespace scanning depend on the generated feature slug and can reset numbering per feature name.
Assisted-by: Codex (model: GPT-5, autonomous)
* fix: ignore malformed timestamp refs when numbering
Align branch-number scanning with feature-branch validation so malformed timestamp-looking refs do not inflate sequential numbering. Also updates the stale git-common comment called out in review.
Assisted-by: Codex (model: GPT-5, autonomous)
This commit is contained in:
@@ -122,10 +122,10 @@ def _run_bash(script_name: str, cwd: Path, *args: str, env_extra: dict | None =
|
||||
)
|
||||
|
||||
|
||||
def _run_pwsh(script_name: str, cwd: Path, *args: str) -> subprocess.CompletedProcess:
|
||||
def _run_pwsh(script_name: str, cwd: Path, *args: str, env_extra: dict | None = None) -> subprocess.CompletedProcess:
|
||||
"""Run an extension PowerShell script."""
|
||||
script = cwd / ".specify" / "extensions" / "git" / "scripts" / "powershell" / script_name
|
||||
env = {**os.environ, **_GIT_ENV}
|
||||
env = {**os.environ, **_GIT_ENV, **(env_extra or {})}
|
||||
return subprocess.run(
|
||||
["pwsh", "-NoProfile", "-File", str(script), *args],
|
||||
cwd=cwd,
|
||||
@@ -320,6 +320,15 @@ class TestCreateFeatureBash:
|
||||
assert rt.returncode == 0, rt.stderr
|
||||
assert "HAS_GIT" not in rt.stdout
|
||||
|
||||
def test_help_documents_branch_prefix(self, tmp_path: Path):
|
||||
"""--help documents both template config knobs."""
|
||||
project = _setup_project(tmp_path)
|
||||
result = _run_bash("create-new-feature-branch.sh", project, "--help")
|
||||
|
||||
assert result.returncode == 0
|
||||
assert "branch_template" in result.stdout
|
||||
assert "branch_prefix" in result.stdout
|
||||
|
||||
def test_branch_name_short_word_case_sensitivity(self, tmp_path: Path):
|
||||
"""A short word is dropped from the derived branch name unless it appears
|
||||
as an acronym in UPPERCASE in the description (case-sensitive, must match the
|
||||
@@ -363,6 +372,183 @@ class TestCreateFeatureBash:
|
||||
data = json.loads(result.stdout)
|
||||
assert data["FEATURE_NUM"] == "003"
|
||||
|
||||
def test_branch_template_adds_author_and_app_namespace(self, tmp_path: Path):
|
||||
"""branch_template namespaces generated branch names for monorepos."""
|
||||
project = _setup_project(tmp_path / "app-a")
|
||||
subprocess.run(["git", "config", "user.name", "jdoe"], cwd=project, check=True)
|
||||
_write_config(project, 'branch_template: "{author}/{app}/{number}-{slug}"\n')
|
||||
|
||||
result = _run_bash(
|
||||
"create-new-feature-branch.sh", project,
|
||||
"--json", "--short-name", "guided-tour", "Add guided tour",
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
data = json.loads(result.stdout)
|
||||
assert data["BRANCH_NAME"] == "jdoe/app-a/001-guided-tour"
|
||||
assert data["FEATURE_NUM"] == "001"
|
||||
|
||||
def test_branch_prefix_shorthand_adds_namespace(self, tmp_path: Path):
|
||||
"""branch_prefix expands to a namespace before the default branch shape."""
|
||||
project = _setup_project(tmp_path / "app-a")
|
||||
_write_config(project, 'branch_prefix: "features/{app}"\n')
|
||||
|
||||
result = _run_bash(
|
||||
"create-new-feature-branch.sh", project,
|
||||
"--json", "--short-name", "guided-tour", "Add guided tour",
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
data = json.loads(result.stdout)
|
||||
assert data["BRANCH_NAME"] == "features/app-a/001-guided-tour"
|
||||
assert data["FEATURE_NUM"] == "001"
|
||||
|
||||
def test_branch_template_scopes_number_after_numeric_app_namespace(self, tmp_path: Path):
|
||||
"""Numeric-looking namespace segments must not be parsed as feature numbers."""
|
||||
project = _setup_project(tmp_path / "2026-app")
|
||||
subprocess.run(["git", "config", "user.name", "jdoe"], cwd=project, check=True)
|
||||
_write_config(project, 'branch_template: "{author}/{app}/{number}-{slug}"\n')
|
||||
subprocess.run(["git", "branch", "jdoe/2026-app/007-existing"], cwd=project, check=True)
|
||||
|
||||
result = _run_bash(
|
||||
"create-new-feature-branch.sh", project,
|
||||
"--json", "--dry-run", "--short-name", "next", "Next feature",
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
data = json.loads(result.stdout)
|
||||
assert data["BRANCH_NAME"] == "jdoe/2026-app/008-next"
|
||||
assert data["FEATURE_NUM"] == "008"
|
||||
|
||||
def test_branch_template_ignores_malformed_timestamp_branches_when_numbering(self, tmp_path: Path):
|
||||
"""Malformed timestamp-looking branches must not inflate sequential numbering."""
|
||||
project = _setup_project(tmp_path / "app-a")
|
||||
subprocess.run(["git", "config", "user.name", "jdoe"], cwd=project, check=True)
|
||||
_write_config(project, 'branch_template: "{author}/{app}/{number}-{slug}"\n')
|
||||
subprocess.run(["git", "branch", "jdoe/app-a/007-existing"], cwd=project, check=True)
|
||||
subprocess.run(["git", "branch", "jdoe/app-a/2026031-143022-invalid"], cwd=project, check=True)
|
||||
subprocess.run(["git", "branch", "jdoe/app-a/20260319-143022"], cwd=project, check=True)
|
||||
|
||||
result = _run_bash(
|
||||
"create-new-feature-branch.sh", project,
|
||||
"--json", "--dry-run", "--short-name", "next", "Next feature",
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
data = json.loads(result.stdout)
|
||||
assert data["BRANCH_NAME"] == "jdoe/app-a/008-next"
|
||||
assert data["FEATURE_NUM"] == "008"
|
||||
|
||||
def test_branch_template_scopes_existing_branch_numbers(self, tmp_path: Path):
|
||||
"""Templated branch numbering ignores branches outside the current namespace."""
|
||||
project = _setup_project(tmp_path / "app-a")
|
||||
subprocess.run(["git", "config", "user.name", "jdoe"], cwd=project, check=True)
|
||||
_write_config(project, 'branch_template: "{author}/{app}/{number}-{slug}"\n')
|
||||
subprocess.run(["git", "branch", "jdoe/app-a/007-existing"], cwd=project, check=True)
|
||||
subprocess.run(["git", "branch", "jdoe/app-b/010-other-app"], cwd=project, check=True)
|
||||
|
||||
result = _run_bash(
|
||||
"create-new-feature-branch.sh", project,
|
||||
"--json", "--dry-run", "--short-name", "next", "Next feature",
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
data = json.loads(result.stdout)
|
||||
assert data["BRANCH_NAME"] == "jdoe/app-a/008-next"
|
||||
assert data["FEATURE_NUM"] == "008"
|
||||
|
||||
def test_branch_template_requires_number_token(self, tmp_path: Path):
|
||||
"""Configured templates must include {number} so generated branches validate."""
|
||||
project = _setup_project(tmp_path / "app-a")
|
||||
_write_config(project, 'branch_template: "{author}/{app}/{slug}"\n')
|
||||
|
||||
result = _run_bash(
|
||||
"create-new-feature-branch.sh", project,
|
||||
"--json", "--dry-run", "--short-name", "guided-tour", "Add guided tour",
|
||||
)
|
||||
|
||||
assert result.returncode != 0
|
||||
assert "branch_template" in result.stderr
|
||||
assert "{number}" in result.stderr
|
||||
|
||||
def test_branch_template_requires_feature_segment_to_start_with_number(self, tmp_path: Path):
|
||||
"""Templates must render a final path segment that validation accepts."""
|
||||
project = _setup_project(tmp_path / "app-a")
|
||||
_write_config(project, 'branch_template: "{author}/{app}/feature-{number}"\n')
|
||||
|
||||
result = _run_bash(
|
||||
"create-new-feature-branch.sh", project,
|
||||
"--json", "--dry-run", "--short-name", "guided-tour", "Add guided tour",
|
||||
)
|
||||
|
||||
assert result.returncode != 0
|
||||
assert "branch_template" in result.stderr
|
||||
assert "{number}-" in result.stderr
|
||||
|
||||
def test_branch_template_rejects_slug_before_number(self, tmp_path: Path):
|
||||
"""{slug} before {number} would make branch-number scanning slug-specific."""
|
||||
project = _setup_project(tmp_path / "app-a")
|
||||
_write_config(project, 'branch_template: "{author}/{slug}/{number}-{slug}"\n')
|
||||
|
||||
result = _run_bash(
|
||||
"create-new-feature-branch.sh", project,
|
||||
"--json", "--dry-run", "--short-name", "guided-tour", "Add guided tour",
|
||||
)
|
||||
|
||||
assert result.returncode != 0
|
||||
assert "branch_template" in result.stderr
|
||||
assert "{slug}" in result.stderr
|
||||
assert "{number}" in result.stderr
|
||||
|
||||
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")
|
||||
result = _run_bash(
|
||||
"create-new-feature-branch.sh", project,
|
||||
"--json", "Ignored description",
|
||||
env_extra={"GIT_BRANCH_NAME": "jdoe/app-a/042-custom-branch"},
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
data = json.loads(result.stdout)
|
||||
assert data["BRANCH_NAME"] == "jdoe/app-a/042-custom-branch"
|
||||
assert data["FEATURE_NUM"] == "042"
|
||||
|
||||
def test_git_branch_name_override_ignores_numeric_namespace_segments(self, tmp_path: Path):
|
||||
"""GIT_BRANCH_NAME uses the feature segment, not numeric namespace segments."""
|
||||
project = _setup_project(tmp_path / "2026-app")
|
||||
result = _run_bash(
|
||||
"create-new-feature-branch.sh", project,
|
||||
"--json", "Ignored description",
|
||||
env_extra={"GIT_BRANCH_NAME": "jdoe/2026-app/042-custom-branch"},
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
data = json.loads(result.stdout)
|
||||
assert data["BRANCH_NAME"] == "jdoe/2026-app/042-custom-branch"
|
||||
assert data["FEATURE_NUM"] == "042"
|
||||
|
||||
def test_git_branch_name_override_without_feature_marker_preserves_full_name(self, tmp_path: Path):
|
||||
"""GIT_BRANCH_NAME without a feature marker keeps the historical FEATURE_NUM."""
|
||||
project = _setup_project(tmp_path / "app-a")
|
||||
result = _run_bash(
|
||||
"create-new-feature-branch.sh", project,
|
||||
"--json", "Ignored description",
|
||||
env_extra={"GIT_BRANCH_NAME": "jdoe/app-a/custom-branch"},
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
data = json.loads(result.stdout)
|
||||
assert data["BRANCH_NAME"] == "jdoe/app-a/custom-branch"
|
||||
assert data["FEATURE_NUM"] == "jdoe/app-a/custom-branch"
|
||||
|
||||
def test_truncation_warning_reports_utf8_bytes(self):
|
||||
"""Bash truncation warnings should use the same byte counter as enforcement."""
|
||||
source = (EXT_BASH / "create-new-feature-branch.sh").read_text(encoding="utf-8")
|
||||
|
||||
assert '_byte_length "$ORIGINAL_BRANCH_NAME"' in source
|
||||
assert '_byte_length "$BRANCH_NAME"' in source
|
||||
|
||||
def test_dry_run_counts_branches_checked_out_in_worktrees(self, tmp_path: Path):
|
||||
"""Branches checked out in sibling worktrees still reserve their prefix."""
|
||||
project = _setup_project(tmp_path / "project")
|
||||
@@ -484,6 +670,15 @@ class TestCreateFeaturePowerShell:
|
||||
assert rt.returncode == 0, rt.stderr
|
||||
assert "HAS_GIT" not in rt.stdout
|
||||
|
||||
def test_help_documents_branch_prefix(self, tmp_path: Path):
|
||||
"""-Help documents both template config knobs."""
|
||||
project = _setup_project(tmp_path)
|
||||
result = _run_pwsh("create-new-feature-branch.ps1", project, "-Help")
|
||||
|
||||
assert result.returncode == 0
|
||||
assert "branch_template" in result.stdout
|
||||
assert "branch_prefix" in result.stdout
|
||||
|
||||
def test_branch_name_short_word_case_sensitivity(self, tmp_path: Path):
|
||||
"""PowerShell must match the bash twin: a short word is dropped unless it
|
||||
appears as an acronym in UPPERCASE (case-sensitive -cmatch, not -match)."""
|
||||
@@ -525,6 +720,176 @@ class TestCreateFeaturePowerShell:
|
||||
data = json.loads(result.stdout)
|
||||
assert re.match(r"^\d{8}-\d{6}-feat$", data["BRANCH_NAME"])
|
||||
|
||||
def test_branch_template_adds_author_and_app_namespace(self, tmp_path: Path):
|
||||
"""PowerShell supports branch_template namespaces."""
|
||||
project = _setup_project(tmp_path / "app-a")
|
||||
subprocess.run(["git", "config", "user.name", "jdoe"], cwd=project, check=True)
|
||||
_write_config(project, 'branch_template: "{author}/{app}/{number}-{slug}"\n')
|
||||
|
||||
result = _run_pwsh(
|
||||
"create-new-feature-branch.ps1", project,
|
||||
"-Json", "-ShortName", "guided-tour", "Add guided tour",
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
data = json.loads(result.stdout)
|
||||
assert data["BRANCH_NAME"] == "jdoe/app-a/001-guided-tour"
|
||||
assert data["FEATURE_NUM"] == "001"
|
||||
|
||||
def test_branch_prefix_shorthand_adds_namespace(self, tmp_path: Path):
|
||||
"""PowerShell supports branch_prefix shorthand namespaces."""
|
||||
project = _setup_project(tmp_path / "app-a")
|
||||
_write_config(project, 'branch_prefix: "features/{app}"\n')
|
||||
|
||||
result = _run_pwsh(
|
||||
"create-new-feature-branch.ps1", project,
|
||||
"-Json", "-ShortName", "guided-tour", "Add guided tour",
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
data = json.loads(result.stdout)
|
||||
assert data["BRANCH_NAME"] == "features/app-a/001-guided-tour"
|
||||
assert data["FEATURE_NUM"] == "001"
|
||||
|
||||
def test_branch_template_scopes_number_after_numeric_app_namespace(self, tmp_path: Path):
|
||||
"""PowerShell ignores numeric-looking namespace segments when numbering."""
|
||||
project = _setup_project(tmp_path / "2026-app")
|
||||
subprocess.run(["git", "config", "user.name", "jdoe"], cwd=project, check=True)
|
||||
_write_config(project, 'branch_template: "{author}/{app}/{number}-{slug}"\n')
|
||||
subprocess.run(["git", "branch", "jdoe/2026-app/007-existing"], cwd=project, check=True)
|
||||
|
||||
result = _run_pwsh(
|
||||
"create-new-feature-branch.ps1", project,
|
||||
"-Json", "-DryRun", "-ShortName", "next", "Next feature",
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
data = json.loads(result.stdout)
|
||||
assert data["BRANCH_NAME"] == "jdoe/2026-app/008-next"
|
||||
assert data["FEATURE_NUM"] == "008"
|
||||
|
||||
def test_branch_template_ignores_malformed_timestamp_branches_when_numbering(self, tmp_path: Path):
|
||||
"""PowerShell skips malformed timestamp-looking refs during sequential numbering."""
|
||||
project = _setup_project(tmp_path / "app-a")
|
||||
subprocess.run(["git", "config", "user.name", "jdoe"], cwd=project, check=True)
|
||||
_write_config(project, 'branch_template: "{author}/{app}/{number}-{slug}"\n')
|
||||
subprocess.run(["git", "branch", "jdoe/app-a/007-existing"], cwd=project, check=True)
|
||||
subprocess.run(["git", "branch", "jdoe/app-a/2026031-143022-invalid"], cwd=project, check=True)
|
||||
subprocess.run(["git", "branch", "jdoe/app-a/20260319-143022"], cwd=project, check=True)
|
||||
|
||||
result = _run_pwsh(
|
||||
"create-new-feature-branch.ps1", project,
|
||||
"-Json", "-DryRun", "-ShortName", "next", "Next feature",
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
data = json.loads(result.stdout)
|
||||
assert data["BRANCH_NAME"] == "jdoe/app-a/008-next"
|
||||
assert data["FEATURE_NUM"] == "008"
|
||||
|
||||
def test_branch_template_scopes_existing_branch_numbers(self, tmp_path: Path):
|
||||
"""PowerShell templated numbering ignores branches outside the namespace."""
|
||||
project = _setup_project(tmp_path / "app-a")
|
||||
subprocess.run(["git", "config", "user.name", "jdoe"], cwd=project, check=True)
|
||||
_write_config(project, 'branch_template: "{author}/{app}/{number}-{slug}"\n')
|
||||
subprocess.run(["git", "branch", "jdoe/app-a/007-existing"], cwd=project, check=True)
|
||||
subprocess.run(["git", "branch", "jdoe/app-b/010-other-app"], cwd=project, check=True)
|
||||
|
||||
result = _run_pwsh(
|
||||
"create-new-feature-branch.ps1", project,
|
||||
"-Json", "-DryRun", "-ShortName", "next", "Next feature",
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
data = json.loads(result.stdout)
|
||||
assert data["BRANCH_NAME"] == "jdoe/app-a/008-next"
|
||||
assert data["FEATURE_NUM"] == "008"
|
||||
|
||||
def test_branch_template_requires_number_token(self, tmp_path: Path):
|
||||
"""PowerShell rejects templates without {number}."""
|
||||
project = _setup_project(tmp_path / "app-a")
|
||||
_write_config(project, 'branch_template: "{author}/{app}/{slug}"\n')
|
||||
|
||||
result = _run_pwsh(
|
||||
"create-new-feature-branch.ps1", project,
|
||||
"-Json", "-DryRun", "-ShortName", "guided-tour", "Add guided tour",
|
||||
)
|
||||
|
||||
assert result.returncode != 0
|
||||
assert "branch_template" in result.stderr
|
||||
assert "{number}" in result.stderr
|
||||
|
||||
def test_branch_template_requires_feature_segment_to_start_with_number(self, tmp_path: Path):
|
||||
"""PowerShell rejects templates whose final segment cannot validate."""
|
||||
project = _setup_project(tmp_path / "app-a")
|
||||
_write_config(project, 'branch_template: "{author}/{app}/feature-{number}"\n')
|
||||
|
||||
result = _run_pwsh(
|
||||
"create-new-feature-branch.ps1", project,
|
||||
"-Json", "-DryRun", "-ShortName", "guided-tour", "Add guided tour",
|
||||
)
|
||||
|
||||
assert result.returncode != 0
|
||||
assert "branch_template" in result.stderr
|
||||
assert "{number}-" in result.stderr
|
||||
|
||||
def test_branch_template_rejects_slug_before_number(self, tmp_path: Path):
|
||||
"""PowerShell rejects templates where {slug} scopes number scanning."""
|
||||
project = _setup_project(tmp_path / "app-a")
|
||||
_write_config(project, 'branch_template: "{author}/{slug}/{number}-{slug}"\n')
|
||||
|
||||
result = _run_pwsh(
|
||||
"create-new-feature-branch.ps1", project,
|
||||
"-Json", "-DryRun", "-ShortName", "guided-tour", "Add guided tour",
|
||||
)
|
||||
|
||||
assert result.returncode != 0
|
||||
assert "branch_template" in result.stderr
|
||||
assert "{slug}" in result.stderr
|
||||
assert "{number}" in result.stderr
|
||||
|
||||
def test_git_branch_name_override_extracts_number_after_namespace(self, tmp_path: Path):
|
||||
"""PowerShell GIT_BRANCH_NAME extracts FEATURE_NUM from a namespaced branch."""
|
||||
project = _setup_project(tmp_path / "app-a")
|
||||
result = _run_pwsh(
|
||||
"create-new-feature-branch.ps1", project,
|
||||
"-Json", "Ignored description",
|
||||
env_extra={"GIT_BRANCH_NAME": "jdoe/app-a/042-custom-branch"},
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
data = json.loads(result.stdout)
|
||||
assert data["BRANCH_NAME"] == "jdoe/app-a/042-custom-branch"
|
||||
assert data["FEATURE_NUM"] == "042"
|
||||
|
||||
def test_git_branch_name_override_ignores_numeric_namespace_segments(self, tmp_path: Path):
|
||||
"""PowerShell GIT_BRANCH_NAME ignores numeric namespace segments."""
|
||||
project = _setup_project(tmp_path / "2026-app")
|
||||
result = _run_pwsh(
|
||||
"create-new-feature-branch.ps1", project,
|
||||
"-Json", "Ignored description",
|
||||
env_extra={"GIT_BRANCH_NAME": "jdoe/2026-app/042-custom-branch"},
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
data = json.loads(result.stdout)
|
||||
assert data["BRANCH_NAME"] == "jdoe/2026-app/042-custom-branch"
|
||||
assert data["FEATURE_NUM"] == "042"
|
||||
|
||||
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")
|
||||
result = _run_pwsh(
|
||||
"create-new-feature-branch.ps1", project,
|
||||
"-Json", "Ignored description",
|
||||
env_extra={"GIT_BRANCH_NAME": "jdoe/app-a/custom-branch"},
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
data = json.loads(result.stdout)
|
||||
assert data["BRANCH_NAME"] == "jdoe/app-a/custom-branch"
|
||||
assert data["FEATURE_NUM"] == "jdoe/app-a/custom-branch"
|
||||
|
||||
def test_no_git_graceful_degradation(self, tmp_path: Path):
|
||||
"""create-new-feature-branch.ps1 works without git."""
|
||||
project = _setup_project(tmp_path, git=False)
|
||||
@@ -1011,13 +1376,31 @@ class TestGitCommonBash:
|
||||
)
|
||||
assert result.returncode == 0
|
||||
|
||||
def test_check_feature_branch_rejects_nested_prefix(self, tmp_path: Path):
|
||||
def test_check_feature_branch_accepts_nested_prefix(self, tmp_path: Path):
|
||||
project = _setup_project(tmp_path)
|
||||
script = project / ".specify" / "extensions" / "git" / "scripts" / "bash" / "git-common.sh"
|
||||
result = subprocess.run(
|
||||
["bash", "-c", f'source "{script}" && check_feature_branch "feat/fix/001-x" "true"'],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
assert result.returncode == 0
|
||||
|
||||
def test_check_feature_branch_rejects_nested_prefix_without_number(self, tmp_path: Path):
|
||||
project = _setup_project(tmp_path)
|
||||
script = project / ".specify" / "extensions" / "git" / "scripts" / "bash" / "git-common.sh"
|
||||
result = subprocess.run(
|
||||
["bash", "-c", f'source "{script}" && check_feature_branch "feat/fix/no-number" "true"'],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
assert result.returncode != 0
|
||||
|
||||
def test_check_feature_branch_rejects_numeric_namespace_without_feature_number(self, tmp_path: Path):
|
||||
project = _setup_project(tmp_path)
|
||||
script = project / ".specify" / "extensions" / "git" / "scripts" / "bash" / "git-common.sh"
|
||||
result = subprocess.run(
|
||||
["bash", "-c", f'source "{script}" && check_feature_branch "jdoe/2026-app/no-number" "true"'],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
assert result.returncode != 0
|
||||
|
||||
|
||||
@@ -1037,3 +1420,33 @@ class TestGitCommonPowerShell:
|
||||
text=True,
|
||||
)
|
||||
assert result.returncode == 0
|
||||
|
||||
def test_test_feature_branch_accepts_nested_prefix(self, tmp_path: Path):
|
||||
project = _setup_project(tmp_path)
|
||||
script = project / ".specify" / "extensions" / "git" / "scripts" / "powershell" / "git-common.ps1"
|
||||
result = subprocess.run(
|
||||
[
|
||||
"pwsh",
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
f'. "{script}"; if (Test-FeatureBranch -Branch "jdoe/app-a/001-x" -HasGit $true) {{ exit 0 }} else {{ exit 1 }}',
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
assert result.returncode == 0
|
||||
|
||||
def test_test_feature_branch_rejects_numeric_namespace_without_feature_number(self, tmp_path: Path):
|
||||
project = _setup_project(tmp_path)
|
||||
script = project / ".specify" / "extensions" / "git" / "scripts" / "powershell" / "git-common.ps1"
|
||||
result = subprocess.run(
|
||||
[
|
||||
"pwsh",
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
f'. "{script}"; if (Test-FeatureBranch -Branch "jdoe/2026-app/no-number" -HasGit $true) {{ exit 0 }} else {{ exit 1 }}',
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
assert result.returncode != 0
|
||||
|
||||
Reference in New Issue
Block a user