Skip to content

Commit 5ff4955

Browse files
marcelsafinCopilot
andcommitted
fix(scripts): align feature number range
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 62c7633 commit 5ff4955

4 files changed

Lines changed: 93 additions & 5 deletions

File tree

scripts/bash/create-new-feature.sh

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,25 @@ if [ "$USE_TIMESTAMP" = true ]; then
202202
FEATURE_NUM=$(date +%Y%m%d-%H%M%S)
203203
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
204204
else
205+
# Bash arithmetic is signed 64-bit; reject digit strings that would wrap.
206+
if [[ "$BRANCH_NUMBER" =~ ^[0-9]+$ ]]; then
207+
MAX_FEATURE_NUMBER=9223372036854775807
208+
NORMALIZED_BRANCH_NUMBER="${BRANCH_NUMBER#"${BRANCH_NUMBER%%[!0]*}"}"
209+
[ -n "$NORMALIZED_BRANCH_NUMBER" ] || NORMALIZED_BRANCH_NUMBER=0
210+
NUMBER_TOO_LARGE=false
211+
if [ ${#NORMALIZED_BRANCH_NUMBER} -gt ${#MAX_FEATURE_NUMBER} ]; then
212+
NUMBER_TOO_LARGE=true
213+
elif [ ${#NORMALIZED_BRANCH_NUMBER} -eq ${#MAX_FEATURE_NUMBER} ]; then
214+
# Equal-length digit strings must be compared without arithmetic overflow.
215+
# shellcheck disable=SC2071
216+
[[ "$NORMALIZED_BRANCH_NUMBER" > "$MAX_FEATURE_NUMBER" ]] && NUMBER_TOO_LARGE=true
217+
fi
218+
if $NUMBER_TOO_LARGE; then
219+
echo "Error: --number must be between 0 and $MAX_FEATURE_NUMBER, got '$BRANCH_NUMBER'" >&2
220+
exit 1
221+
fi
222+
fi
223+
205224
# Determine branch number from existing feature directories
206225
if [ -z "$BRANCH_NUMBER" ]; then
207226
HIGHEST=$(get_highest_from_specs "$SPECS_DIR")

scripts/powershell/create-new-feature.ps1

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -142,10 +142,11 @@ if ($ShortName) {
142142
$branchSuffix = Get-BranchName -Description $featureDesc
143143
}
144144

145-
# Warn if -Number and -Timestamp are both specified. Use ContainsKey (not
146-
# `-ne 0`) so an explicit `-Number 0` is also detected, matching the bash twin's
147-
# `[ -n "$BRANCH_NUMBER" ]` check.
148-
if ($Timestamp -and $PSBoundParameters.ContainsKey('Number')) {
145+
# Treat an explicit empty string as omitted, matching the bash and Python twins.
146+
$hasNumber = $PSBoundParameters.ContainsKey('Number') -and $Number -ne ''
147+
148+
# Warn if -Number and -Timestamp are both specified.
149+
if ($Timestamp -and $hasNumber) {
149150
Write-Warning "[specify] Warning: -Number is ignored when -Timestamp is used"
150151
$Number = ''
151152
}
@@ -159,7 +160,7 @@ if ($Timestamp) {
159160
# when -Number was not supplied; an explicit value (including 0) is honored,
160161
# matching the bash twin's `[ -z "$BRANCH_NUMBER" ]` check.
161162
[long]$resolvedNumber = 0
162-
if (-not $PSBoundParameters.ContainsKey('Number')) {
163+
if (-not $hasNumber) {
163164
$resolvedNumber = (Get-HighestNumberFromSpecs -SpecsDir $specsDir) + 1
164165
} elseif ($Number -notmatch '^[0-9]+$' -or -not [long]::TryParse($Number, [ref]$resolvedNumber)) {
165166
Write-Error "Error: -Number must be an unsigned integer, got '$Number'"

scripts/python/create_new_feature.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def _json_line(payload: object) -> str:
3232
)
3333

3434
_MAX_BRANCH_LENGTH = 244
35+
_MAX_FEATURE_NUMBER = 2**63 - 1
3536

3637

3738
def _usage(argv0: str) -> str:
@@ -219,6 +220,13 @@ def main(argv: list[str] | None = None) -> int:
219220
)
220221
return 1
221222
number = int(normalized_number, 10)
223+
if number > _MAX_FEATURE_NUMBER:
224+
print(
225+
"Error: --number must be between 0 and "
226+
f"{_MAX_FEATURE_NUMBER}, got '{branch_number}'",
227+
file=sys.stderr,
228+
)
229+
return 1
222230
else:
223231
number = _get_highest_from_specs(specs_dir) + 1
224232
feature_num = f"{number:03d}"

tests/test_create_new_feature_python_parity.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,3 +402,63 @@ def test_all_variants_reject_signed_number(repo: Path, number: str) -> None:
402402
)
403403

404404
assert bash.returncode == ps.returncode == py.returncode == 1
405+
406+
407+
@requires_bash
408+
@pytest.mark.skipif(not HAS_POWERSHELL, reason="no PowerShell available")
409+
@pytest.mark.parametrize("timestamp", [False, True], ids=["numbered", "timestamp"])
410+
def test_all_variants_treat_empty_number_as_omitted(
411+
repo: Path, timestamp: bool
412+
) -> None:
413+
bash_args = ["--json", "--dry-run", "--number", ""]
414+
ps_args = ["-Json", "-DryRun", "-Number", ""]
415+
py_args = ["--json", "--dry-run", "--number", ""]
416+
if timestamp:
417+
bash_args.append("--timestamp")
418+
ps_args.append("-Timestamp")
419+
py_args.append("--timestamp")
420+
bash_args.append("x")
421+
ps_args.append("x")
422+
py_args.append("x")
423+
424+
bash = run(bash_cmd(repo, SCRIPT, *bash_args), repo)
425+
ps = run(ps_cmd(repo, SCRIPT, *ps_args), repo)
426+
py = run(py_cmd(repo, SCRIPT, *py_args), repo)
427+
428+
assert bash.returncode == ps.returncode == py.returncode == 0
429+
assert bash.stderr == ps.stderr == py.stderr == ""
430+
if not timestamp:
431+
assert json_stdout(bash) == json_stdout(ps) == json_stdout(py)
432+
433+
434+
@requires_bash
435+
@pytest.mark.skipif(not HAS_POWERSHELL, reason="no PowerShell available")
436+
@pytest.mark.parametrize(
437+
("number", "returncode"),
438+
[
439+
(str(2**63 - 1), 0),
440+
(str(2**63), 1),
441+
],
442+
ids=["int64_max", "int64_overflow"],
443+
)
444+
def test_all_variants_share_int64_number_range(
445+
repo: Path, number: str, returncode: int
446+
) -> None:
447+
bash = run(
448+
bash_cmd(repo, SCRIPT, "--json", "--dry-run", "--number", number, "x"),
449+
repo,
450+
)
451+
ps = run(
452+
ps_cmd(repo, SCRIPT, "-Json", "-DryRun", "-Number", number, "x"),
453+
repo,
454+
)
455+
py = run(
456+
py_cmd(repo, SCRIPT, "--json", "--dry-run", "--number", number, "x"),
457+
repo,
458+
)
459+
460+
assert bash.returncode == ps.returncode == py.returncode == returncode
461+
if returncode == 0:
462+
assert json_stdout(bash) == json_stdout(ps) == json_stdout(py)
463+
else:
464+
assert bash.stdout == ps.stdout == py.stdout == ""

0 commit comments

Comments
 (0)