Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
fix: use git ls-remote for remote-aware dry-run numbering
Dry-run now queries remote branches via `git ls-remote --heads`
(read-only, no fetch) to account for remote-only branches when
computing the next sequential number. This prevents dry-run from
returning a number that already exists on a remote.

Added test verifying dry-run sees remote-only higher-numbered
branches and adjusts numbering accordingly.

Assisted-By: 🤖 Claude Code
  • Loading branch information
rhuss committed Mar 28, 2026
commit 71e0b8fff3e5779c19bcebf968ece5d3c5fb9a74
28 changes: 27 additions & 1 deletion scripts/bash/create-new-feature.sh
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,28 @@ get_highest_from_branches() {
echo "$highest"
}

# Function to get highest number from remote branches without fetching (side-effect-free)
get_highest_from_remote_refs() {
local highest=0

for remote in $(git remote 2>/dev/null); do
while IFS= read -r line; do
[ -z "$line" ] && continue
# Extract ref name from ls-remote output (hash\trefs/heads/branch-name)
ref="${line##*/}"
if echo "$ref" | grep -Eq '^[0-9]{3,}-' && ! echo "$ref" | grep -Eq '^[0-9]{8}-[0-9]{6}-'; then
number=$(echo "$ref" | grep -Eo '^[0-9]+' || echo "0")
number=$((10#$number))
if [ "$number" -gt "$highest" ]; then
highest=$number
fi
fi
done <<< "$(git ls-remote --heads "$remote" 2>/dev/null || echo "")"
Comment thread
rhuss marked this conversation as resolved.
Outdated
done

echo "$highest"
}

# Function to check existing branches (local and remote) and return next available number
check_existing_branches() {
local specs_dir="$1"
Expand Down Expand Up @@ -259,10 +281,14 @@ else
# Determine branch number
if [ -z "$BRANCH_NUMBER" ]; then
if [ "$DRY_RUN" = true ]; then
# Dry-run: use locally available data only (skip git fetch)
# Dry-run: query remote refs without fetching (side-effect-free)
_highest_branch=0
if [ "$HAS_GIT" = true ]; then
_highest_branch=$(get_highest_from_branches)
_highest_remote=$(get_highest_from_remote_refs)
if [ "$_highest_remote" -gt "$_highest_branch" ]; then
_highest_branch=$_highest_remote
fi
fi
_highest_spec=$(get_highest_from_specs "$SPECS_DIR")
_max_num=$_highest_branch
Expand Down
33 changes: 32 additions & 1 deletion scripts/powershell/create-new-feature.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,35 @@ function Get-HighestNumberFromBranches {
return $highest
}

function Get-HighestNumberFromRemoteRefs {
[long]$highest = 0
try {
$remotes = git remote 2>$null
if ($remotes) {
foreach ($remote in $remotes) {
$refs = git ls-remote --heads $remote 2>$null
if ($LASTEXITCODE -eq 0 -and $refs) {
foreach ($line in $refs) {
# Extract branch name from refs/heads/branch-name
if ($line -match 'refs/heads/(.+)$') {
$ref = $matches[1]
if ($ref -match '^(\d{3,})-' -and $ref -notmatch '^\d{8}-\d{6}-') {
[long]$num = 0
if ([long]::TryParse($matches[1], [ref]$num) -and $num -gt $highest) {
$highest = $num
}
}
}
}
}
}
}
} catch {
Write-Verbose "Could not query remote refs: $_"
}
return $highest
}

function Get-NextBranchNumber {
param(
[string]$SpecsDir
Expand Down Expand Up @@ -208,10 +237,12 @@ if ($Timestamp) {
# Determine branch number
if ($Number -eq 0) {
if ($DryRun) {
# Dry-run: use locally available data only (skip git fetch)
# Dry-run: query remote refs without fetching (side-effect-free)
$highestBranch = 0
if ($hasGit) {
$highestBranch = Get-HighestNumberFromBranches
$highestRemote = Get-HighestNumberFromRemoteRefs
$highestBranch = [Math]::Max($highestBranch, $highestRemote)
}
$highestSpec = Get-HighestNumberFromSpecs -SpecsDir $specsDir
$Number = [Math]::Max($highestBranch, $highestSpec) + 1
Expand Down
54 changes: 54 additions & 0 deletions tests/test_timestamp_branches.py
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,60 @@ def test_dry_run_then_real_run_match(self, git_repo: Path):
real_branch = line.split(":", 1)[1].strip()
assert dry_branch == real_branch, f"dry={dry_branch} != real={real_branch}"

Comment thread
rhuss marked this conversation as resolved.
def test_dry_run_accounts_for_remote_branches(self, git_repo: Path):
"""Dry-run queries remote refs via ls-remote (no fetch) for accurate numbering."""
(git_repo / "specs" / "001-existing").mkdir(parents=True)

# Set up a bare remote and push
remote_dir = git_repo.parent / "remote.git"
subprocess.run(
["git", "init", "--bare", str(remote_dir)],
check=True, capture_output=True,
)
subprocess.run(
["git", "remote", "add", "origin", str(remote_dir)],
check=True, cwd=git_repo, capture_output=True,
)
subprocess.run(
["git", "push", "-u", "origin", "HEAD"],
check=True, cwd=git_repo, capture_output=True,
)

# Clone into a second copy, create a higher-numbered branch, push it
second_clone = git_repo.parent / "second_clone"
subprocess.run(
["git", "clone", str(remote_dir), str(second_clone)],
check=True, capture_output=True,
Comment thread
rhuss marked this conversation as resolved.
Outdated
)
subprocess.run(
["git", "config", "user.email", "test@example.com"],
cwd=second_clone, check=True, capture_output=True,
)
subprocess.run(
["git", "config", "user.name", "Test User"],
cwd=second_clone, check=True, capture_output=True,
)
# Create branch 005 on the remote (higher than local 001)
subprocess.run(
["git", "checkout", "-b", "005-remote-only"],
cwd=second_clone, check=True, capture_output=True,
)
subprocess.run(
["git", "push", "origin", "005-remote-only"],
cwd=second_clone, check=True, capture_output=True,
)

# Primary repo: dry-run should see 005 via ls-remote and return 006
dry_result = run_script(
git_repo, "--dry-run", "--short-name", "remote-test", "Remote test"
)
assert dry_result.returncode == 0, dry_result.stderr
dry_branch = None
for line in dry_result.stdout.splitlines():
if line.startswith("BRANCH_NAME:"):
dry_branch = line.split(":", 1)[1].strip()
assert dry_branch == "006-remote-test", f"expected 006-remote-test, got: {dry_branch}"

def test_dry_run_json_includes_field(self, git_repo: Path):
"""T015: JSON output includes DRY_RUN field when --dry-run is active."""
import json
Expand Down
Loading