From 8e14ab19354c60572244acd9afc5365998596077 Mon Sep 17 00:00:00 2001 From: "Sakoda, Taro (cub)" <35255268+t-sakoda@users.noreply.github.com> Date: Thu, 2 Apr 2026 21:44:26 +0900 Subject: [PATCH] fix: support feature branch numbers with 4+ digits (#2040) * fix: support feature branch numbers with 4+ digits in common.sh and common.ps1 The sequential feature number pattern was hardcoded to exactly 3 digits (`{3}`), causing branches like `1234-feature-name` to be rejected. Changed to `{3,}` (3 or more digits) to support growing projects. Also added a guard to exclude malformed timestamp patterns from being accepted as sequential prefixes. Closes #344 Co-Authored-By: Claude Opus 4.6 (1M context) * fix: narrow timestamp guard and use [long] to prevent overflow - Change [int] to [long] in PowerShell Get-CurrentBranch to avoid overflow for large feature numbers (>2,147,483,647) - Narrow malformed-timestamp exclusion from ^[0-9]+-[0-9]{6}- to ^[0-9]{7}-[0-9]{6}- so valid sequential branches like 004-123456-fix-bug are not rejected Co-Authored-By: Claude Opus 4.6 (1M context) * test: add regression tests for 4+ digit feature branch support Cover check_feature_branch and find_feature_dir_by_prefix with 4-digit sequential prefixes, as requested in PR review #2040. Co-Authored-By: Claude Opus 4.6 (1M context) * fix: reject timestamp-like branches without trailing slug Branches like "20260319-143022" (no "-" suffix) were incorrectly accepted as sequential prefixes. Add explicit rejection for 7-or-8 digit date + 6-digit time patterns with no trailing slug, in both common.sh and common.ps1. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- scripts/bash/common.sh | 14 ++++++++++---- scripts/powershell/common.ps1 | 12 ++++++++---- tests/test_timestamp_branches.py | 24 ++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/scripts/bash/common.sh b/scripts/bash/common.sh index 416fcadfc..5e45e8708 100644 --- a/scripts/bash/common.sh +++ b/scripts/bash/common.sh @@ -78,7 +78,7 @@ get_current_branch() { latest_timestamp="$ts" latest_feature=$dirname fi - elif [[ "$dirname" =~ ^([0-9]{3})- ]]; then + elif [[ "$dirname" =~ ^([0-9]{3,})- ]]; then local number=${BASH_REMATCH[1]} number=$((10#$number)) if [[ "$number" -gt "$highest" ]]; then @@ -124,9 +124,15 @@ check_feature_branch() { return 0 fi - if [[ ! "$branch" =~ ^[0-9]{3}- ]] && [[ ! "$branch" =~ ^[0-9]{8}-[0-9]{6}- ]]; then + # Accept sequential prefix (3+ digits) but exclude malformed timestamps + # Malformed: 7-or-8 digit date + 6-digit time with no trailing slug (e.g. "2026031-143022" or "20260319-143022") + local is_sequential=false + if [[ "$branch" =~ ^[0-9]{3,}- ]] && [[ ! "$branch" =~ ^[0-9]{7}-[0-9]{6}- ]] && [[ ! "$branch" =~ ^[0-9]{7,8}-[0-9]{6}$ ]]; then + is_sequential=true + fi + if [[ "$is_sequential" != "true" ]] && [[ ! "$branch" =~ ^[0-9]{8}-[0-9]{6}- ]]; then echo "ERROR: Not on a feature branch. Current branch: $branch" >&2 - echo "Feature branches should be named like: 001-feature-name or 20260319-143022-feature-name" >&2 + echo "Feature branches should be named like: 001-feature-name, 1234-feature-name, or 20260319-143022-feature-name" >&2 return 1 fi @@ -146,7 +152,7 @@ find_feature_dir_by_prefix() { local prefix="" if [[ "$branch_name" =~ ^([0-9]{8}-[0-9]{6})- ]]; then prefix="${BASH_REMATCH[1]}" - elif [[ "$branch_name" =~ ^([0-9]{3})- ]]; then + elif [[ "$branch_name" =~ ^([0-9]{3,})- ]]; then prefix="${BASH_REMATCH[1]}" else # If branch doesn't have a recognized prefix, fall back to exact match diff --git a/scripts/powershell/common.ps1 b/scripts/powershell/common.ps1 index 7a96d3fac..8c8c801ee 100644 --- a/scripts/powershell/common.ps1 +++ b/scripts/powershell/common.ps1 @@ -83,8 +83,8 @@ function Get-CurrentBranch { $latestTimestamp = $ts $latestFeature = $_.Name } - } elseif ($_.Name -match '^(\d{3})-') { - $num = [int]$matches[1] + } elseif ($_.Name -match '^(\d{3,})-') { + $num = [long]$matches[1] if ($num -gt $highest) { $highest = $num # Only update if no timestamp branch found yet @@ -139,9 +139,13 @@ function Test-FeatureBranch { return $true } - if ($Branch -notmatch '^[0-9]{3}-' -and $Branch -notmatch '^\d{8}-\d{6}-') { + # Accept sequential prefix (3+ digits) but exclude malformed timestamps + # Malformed: 7-or-8 digit date + 6-digit time with no trailing slug (e.g. "2026031-143022" or "20260319-143022") + $hasMalformedTimestamp = ($Branch -match '^[0-9]{7}-[0-9]{6}-') -or ($Branch -match '^(?:\d{7}|\d{8})-\d{6}$') + $isSequential = ($Branch -match '^[0-9]{3,}-') -and (-not $hasMalformedTimestamp) + if (-not $isSequential -and $Branch -notmatch '^\d{8}-\d{6}-') { Write-Output "ERROR: Not on a feature branch. Current branch: $Branch" - Write-Output "Feature branches should be named like: 001-feature-name or 20260319-143022-feature-name" + Write-Output "Feature branches should be named like: 001-feature-name, 1234-feature-name, or 20260319-143022-feature-name" return $false } return $true diff --git a/tests/test_timestamp_branches.py b/tests/test_timestamp_branches.py index 0c9eb07b4..9e36f1756 100644 --- a/tests/test_timestamp_branches.py +++ b/tests/test_timestamp_branches.py @@ -186,11 +186,26 @@ class TestCheckFeatureBranch: result = source_and_call('check_feature_branch "main" "true"') assert result.returncode != 0 + def test_accepts_four_digit_sequential_branch(self): + """check_feature_branch accepts 4+ digit sequential branch.""" + result = source_and_call('check_feature_branch "1234-feat" "true"') + assert result.returncode == 0 + def test_rejects_partial_timestamp(self): """Test 9: check_feature_branch rejects 7-digit date.""" result = source_and_call('check_feature_branch "2026031-143022-feat" "true"') assert result.returncode != 0 + def test_rejects_timestamp_without_slug(self): + """check_feature_branch rejects timestamp-like branch missing trailing slug.""" + result = source_and_call('check_feature_branch "20260319-143022" "true"') + assert result.returncode != 0 + + def test_rejects_7digit_timestamp_without_slug(self): + """check_feature_branch rejects 7-digit date + 6-digit time without slug.""" + result = source_and_call('check_feature_branch "2026031-143022" "true"') + assert result.returncode != 0 + # ── find_feature_dir_by_prefix Tests ───────────────────────────────────────── @@ -214,6 +229,15 @@ class TestFindFeatureDirByPrefix: assert result.returncode == 0 assert result.stdout.strip() == f"{tmp_path}/specs/20260319-143022-original-feat" + def test_four_digit_sequential_prefix(self, tmp_path: Path): + """find_feature_dir_by_prefix resolves 4+ digit sequential prefix.""" + (tmp_path / "specs" / "1000-original-feat").mkdir(parents=True) + result = source_and_call( + f'find_feature_dir_by_prefix "{tmp_path}" "1000-different-name"' + ) + assert result.returncode == 0 + assert result.stdout.strip() == f"{tmp_path}/specs/1000-original-feat" + # ── get_current_branch Tests ─────────────────────────────────────────────────