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
18 changes: 18 additions & 0 deletions docs/reference/core.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <file>`) 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
Expand Down
4 changes: 4 additions & 0 deletions scripts/bash/create-new-feature.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 4 additions & 0 deletions scripts/powershell/create-new-feature.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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 ''

Expand Down
7 changes: 7 additions & 0 deletions scripts/python/create_new_feature.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
54 changes: 54 additions & 0 deletions tests/test_create_new_feature_python_parity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
):
Expand Down