diff --git a/docs/reference/core.md b/docs/reference/core.md index b70aebe236..685f8aa55e 100644 --- a/docs/reference/core.md +++ b/docs/reference/core.md @@ -61,6 +61,24 @@ specify init my-project --integration copilot --preset compliance > **Symlinked project roots.** `SPECIFY_INIT_DIR` relocates *where* the project is, not *how* a command treats symlinks: each command keeps its existing cwd-path stance. Commands that traverse and write project files through broad input paths (`bundle`, `workflow run `) refuse a symlinked `.specify/` to preserve write confinement. Other project-scoped commands keep their existing behavior when `SPECIFY_INIT_DIR` points at a project root, which may include following a symlinked `.specify/`. +## Naming Features with the Helper Scripts + +When calling the bundled `create-new-feature` helper scripts directly, generated +names retain only ASCII letters and digits. A description entirely in a non-Latin +script, or made only of punctuation, can therefore produce an empty suffix such +as `001-`. The scripts warn on stderr when this happens, including during a dry +run; JSON output remains parseable. + +Keep the original description and supply a readable ASCII short name: + +```bash +bash .specify/scripts/bash/create-new-feature.sh --json --short-name user-auth "添加用户" +``` + +The Python helper also accepts `--short-name`; the PowerShell helper uses +`-ShortName`. A supplied short name is cleaned by the same rules, so it must +contain at least one ASCII letter or digit. + ## Check Installed Tools ```bash diff --git a/scripts/bash/create-new-feature.sh b/scripts/bash/create-new-feature.sh index 06681d9b6e..94294f20cb 100644 --- a/scripts/bash/create-new-feature.sh +++ b/scripts/bash/create-new-feature.sh @@ -265,6 +265,10 @@ else BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION") fi +if [ -z "$BRANCH_SUFFIX" ]; then + echo "[specify] Warning: Feature name is empty after removing unsupported characters. Use --short-name with ASCII letters or digits (for example, user-auth)." >&2 +fi + # Warn if --number and --timestamp are both specified if [ "$USE_TIMESTAMP" = true ] && [ -n "$BRANCH_NUMBER" ]; then >&2 echo "[specify] Warning: --number is ignored when --timestamp is used" diff --git a/scripts/powershell/create-new-feature.ps1 b/scripts/powershell/create-new-feature.ps1 index 9ce2c678a4..6a18546def 100644 --- a/scripts/powershell/create-new-feature.ps1 +++ b/scripts/powershell/create-new-feature.ps1 @@ -185,6 +185,10 @@ if ($ShortName) { $branchSuffix = Get-BranchName -Description $featureDesc } +if (-not $branchSuffix) { + [Console]::Error.WriteLine("[specify] Warning: Feature name is empty after removing unsupported characters. Use -ShortName with ASCII letters or digits (for example, user-auth).") +} + # Treat an explicit empty string as omitted, matching the bash and Python twins. $hasNumber = $PSBoundParameters.ContainsKey('Number') -and $Number -ne '' diff --git a/scripts/python/create_new_feature.py b/scripts/python/create_new_feature.py index f36064afbb..7f1210c225 100644 --- a/scripts/python/create_new_feature.py +++ b/scripts/python/create_new_feature.py @@ -264,6 +264,13 @@ def main(argv: list[str] | None = None) -> int: else: branch_suffix = _generate_branch_name(args.description) + if not branch_suffix: + print( + "[specify] Warning: Feature name is empty after removing unsupported characters. " + "Use --short-name with ASCII letters or digits (for example, user-auth).", + file=sys.stderr, + ) + branch_number = args.branch_number if args.use_timestamp and branch_number: print( diff --git a/tests/test_create_new_feature_python_parity.py b/tests/test_create_new_feature_python_parity.py index 6cc50d80eb..36acefb7bc 100644 --- a/tests/test_create_new_feature_python_parity.py +++ b/tests/test_create_new_feature_python_parity.py @@ -57,6 +57,60 @@ def repo_pair(tmp_path: Path) -> tuple[Path, Path]: return _setup_repo(tmp_path, "proj-a"), _setup_repo(tmp_path, "proj-b") +@pytest.mark.parametrize( + "variant", + [ + pytest.param("bash", marks=requires_bash), + "python", + pytest.param( + "powershell", + marks=pytest.mark.skipif(not HAS_POWERSHELL, reason="no PowerShell available"), + ), + ], +) +@pytest.mark.parametrize("dry_run", [False, True]) +@pytest.mark.parametrize( + "description,short_name,suffix,warns", + [ + ("添加用户", None, "", True), + ("добавить", None, "", True), + ("!!! ??? ***", None, "", True), + ("添加用户", "user-auth", "user-auth", False), + ("Add users", "用户", "", True), + ("Add user authentication", None, "user-authentication", False), + ], +) +def test_empty_feature_name_warning( + repo: Path, + variant: str, + dry_run: bool, + description: str, + short_name: str | None, + suffix: str, + warns: bool, +) -> None: + """Report unusable names without changing JSON or feature creation (#4574).""" + powershell = variant == "powershell" + args = ["-Json" if powershell else "--json"] + if dry_run: + args.append("-DryRun" if powershell else "--dry-run") + if short_name is not None: + args.extend(["-ShortName" if powershell else "--short-name", short_name]) + args.append(description) + command = {"bash": bash_cmd, "python": py_cmd, "powershell": ps_cmd}[variant] + result = run(command(repo, SCRIPT, *args), repo) + + assert result.returncode == 0, result.stderr + output = json_stdout(result) + assert output["BRANCH_NAME"] == f"001-{suffix}" + warning = "Feature name is empty after removing unsupported characters" + assert result.stderr.count(warning) == int(warns) + if warns: + assert ("-ShortName" if powershell else "--short-name") in result.stderr + assert "ASCII letters or digits" in result.stderr + assert (repo / "specs" / f"001-{suffix}" / "spec.md").exists() is not dry_run + + def _run_all_variants_allow_existing( repo: Path, *, number: str, short_name: str ):