Skip to content

Commit b92ace5

Browse files
committed
feat(scripts): add --dry-run flag to create-new-feature scripts
Add a --dry-run / -DryRun flag to both bash and PowerShell create-new-feature scripts that computes the next branch name, spec file path, and feature number without creating any branches, directories, or files. This enables external tools to query the next available name before running the full specify workflow. When combined with --json, the output includes a DRY_RUN field. Without --dry-run, behavior is completely unchanged. Closes #1931 Assisted-By: 🤖 Claude Code
1 parent 5be705e commit b92ace5

File tree

3 files changed

+299
-75
lines changed

3 files changed

+299
-75
lines changed

scripts/bash/create-new-feature.sh

Lines changed: 70 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
set -e
44

55
JSON_MODE=false
6+
DRY_RUN=false
67
SHORT_NAME=""
78
BRANCH_NUMBER=""
89
USE_TIMESTAMP=false
@@ -14,6 +15,9 @@ while [ $i -le $# ]; do
1415
--json)
1516
JSON_MODE=true
1617
;;
18+
--dry-run)
19+
DRY_RUN=true
20+
;;
1721
--short-name)
1822
if [ $((i + 1)) -gt $# ]; then
1923
echo 'Error: --short-name requires a value' >&2
@@ -45,10 +49,11 @@ while [ $i -le $# ]; do
4549
USE_TIMESTAMP=true
4650
;;
4751
--help|-h)
48-
echo "Usage: $0 [--json] [--short-name <name>] [--number N] [--timestamp] <feature_description>"
52+
echo "Usage: $0 [--json] [--dry-run] [--short-name <name>] [--number N] [--timestamp] <feature_description>"
4953
echo ""
5054
echo "Options:"
5155
echo " --json Output in JSON format"
56+
echo " --dry-run Compute branch name and paths without creating branches, directories, or files"
5257
echo " --short-name <name> Provide a custom short name (2-4 words) for the branch"
5358
echo " --number N Specify branch number manually (overrides auto-detection)"
5459
echo " --timestamp Use timestamp prefix (YYYYMMDD-HHMMSS) instead of sequential numbering"
@@ -69,7 +74,7 @@ done
6974

7075
FEATURE_DESCRIPTION="${ARGS[*]}"
7176
if [ -z "$FEATURE_DESCRIPTION" ]; then
72-
echo "Usage: $0 [--json] [--short-name <name>] [--number N] [--timestamp] <feature_description>" >&2
77+
echo "Usage: $0 [--json] [--dry-run] [--short-name <name>] [--number N] [--timestamp] <feature_description>" >&2
7378
exit 1
7479
fi
7580

@@ -246,7 +251,19 @@ if [ "$USE_TIMESTAMP" = true ]; then
246251
else
247252
# Determine branch number
248253
if [ -z "$BRANCH_NUMBER" ]; then
249-
if [ "$HAS_GIT" = true ]; then
254+
if [ "$DRY_RUN" = true ]; then
255+
# Dry-run: use locally available data only (skip git fetch)
256+
_highest_branch=0
257+
if [ "$HAS_GIT" = true ]; then
258+
_highest_branch=$(get_highest_from_branches)
259+
fi
260+
_highest_spec=$(get_highest_from_specs "$SPECS_DIR")
261+
_max_num=$_highest_branch
262+
if [ "$_highest_spec" -gt "$_max_num" ]; then
263+
_max_num=$_highest_spec
264+
fi
265+
BRANCH_NUMBER=$((_max_num + 1))
266+
elif [ "$HAS_GIT" = true ]; then
250267
# Check existing branches on remotes
251268
BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR")
252269
else
@@ -283,53 +300,70 @@ if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then
283300
>&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)"
284301
fi
285302

286-
if [ "$HAS_GIT" = true ]; then
287-
if ! git checkout -b "$BRANCH_NAME" 2>/dev/null; then
288-
# Check if branch already exists
289-
if git branch --list "$BRANCH_NAME" | grep -q .; then
290-
if [ "$USE_TIMESTAMP" = true ]; then
291-
>&2 echo "Error: Branch '$BRANCH_NAME' already exists. Rerun to get a new timestamp or use a different --short-name."
303+
FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
304+
SPEC_FILE="$FEATURE_DIR/spec.md"
305+
306+
if [ "$DRY_RUN" != true ]; then
307+
if [ "$HAS_GIT" = true ]; then
308+
if ! git checkout -b "$BRANCH_NAME" 2>/dev/null; then
309+
# Check if branch already exists
310+
if git branch --list "$BRANCH_NAME" | grep -q .; then
311+
if [ "$USE_TIMESTAMP" = true ]; then
312+
>&2 echo "Error: Branch '$BRANCH_NAME' already exists. Rerun to get a new timestamp or use a different --short-name."
313+
else
314+
>&2 echo "Error: Branch '$BRANCH_NAME' already exists. Please use a different feature name or specify a different number with --number."
315+
fi
316+
exit 1
292317
else
293-
>&2 echo "Error: Branch '$BRANCH_NAME' already exists. Please use a different feature name or specify a different number with --number."
318+
>&2 echo "Error: Failed to create git branch '$BRANCH_NAME'. Please check your git configuration and try again."
319+
exit 1
294320
fi
295-
exit 1
296-
else
297-
>&2 echo "Error: Failed to create git branch '$BRANCH_NAME'. Please check your git configuration and try again."
298-
exit 1
299321
fi
322+
else
323+
>&2 echo "[specify] Warning: Git repository not detected; skipped branch creation for $BRANCH_NAME"
300324
fi
301-
else
302-
>&2 echo "[specify] Warning: Git repository not detected; skipped branch creation for $BRANCH_NAME"
303-
fi
304325

305-
FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
306-
mkdir -p "$FEATURE_DIR"
326+
mkdir -p "$FEATURE_DIR"
307327

308-
TEMPLATE=$(resolve_template "spec-template" "$REPO_ROOT") || true
309-
SPEC_FILE="$FEATURE_DIR/spec.md"
310-
if [ -n "$TEMPLATE" ] && [ -f "$TEMPLATE" ]; then
311-
cp "$TEMPLATE" "$SPEC_FILE"
312-
else
313-
echo "Warning: Spec template not found; created empty spec file" >&2
314-
touch "$SPEC_FILE"
315-
fi
328+
TEMPLATE=$(resolve_template "spec-template" "$REPO_ROOT") || true
329+
if [ -n "$TEMPLATE" ] && [ -f "$TEMPLATE" ]; then
330+
cp "$TEMPLATE" "$SPEC_FILE"
331+
else
332+
echo "Warning: Spec template not found; created empty spec file" >&2
333+
touch "$SPEC_FILE"
334+
fi
316335

317-
# Inform the user how to persist the feature variable in their own shell
318-
printf '# To persist: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME" >&2
336+
# Inform the user how to persist the feature variable in their own shell
337+
printf '# To persist: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME" >&2
338+
fi
319339

320340
if $JSON_MODE; then
321341
if command -v jq >/dev/null 2>&1; then
322-
jq -cn \
323-
--arg branch_name "$BRANCH_NAME" \
324-
--arg spec_file "$SPEC_FILE" \
325-
--arg feature_num "$FEATURE_NUM" \
326-
'{BRANCH_NAME:$branch_name,SPEC_FILE:$spec_file,FEATURE_NUM:$feature_num}'
342+
if [ "$DRY_RUN" = true ]; then
343+
jq -cn \
344+
--arg branch_name "$BRANCH_NAME" \
345+
--arg spec_file "$SPEC_FILE" \
346+
--arg feature_num "$FEATURE_NUM" \
347+
'{BRANCH_NAME:$branch_name,SPEC_FILE:$spec_file,FEATURE_NUM:$feature_num,DRY_RUN:true}'
348+
else
349+
jq -cn \
350+
--arg branch_name "$BRANCH_NAME" \
351+
--arg spec_file "$SPEC_FILE" \
352+
--arg feature_num "$FEATURE_NUM" \
353+
'{BRANCH_NAME:$branch_name,SPEC_FILE:$spec_file,FEATURE_NUM:$feature_num}'
354+
fi
327355
else
328-
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$(json_escape "$BRANCH_NAME")" "$(json_escape "$SPEC_FILE")" "$(json_escape "$FEATURE_NUM")"
356+
if [ "$DRY_RUN" = true ]; then
357+
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s","DRY_RUN":true}\n' "$(json_escape "$BRANCH_NAME")" "$(json_escape "$SPEC_FILE")" "$(json_escape "$FEATURE_NUM")"
358+
else
359+
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$(json_escape "$BRANCH_NAME")" "$(json_escape "$SPEC_FILE")" "$(json_escape "$FEATURE_NUM")"
360+
fi
329361
fi
330362
else
331363
echo "BRANCH_NAME: $BRANCH_NAME"
332364
echo "SPEC_FILE: $SPEC_FILE"
333365
echo "FEATURE_NUM: $FEATURE_NUM"
334-
printf '# To persist in your shell: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME"
366+
if [ "$DRY_RUN" != true ]; then
367+
printf '# To persist in your shell: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME"
368+
fi
335369
fi

scripts/powershell/create-new-feature.ps1

Lines changed: 57 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
[CmdletBinding()]
44
param(
55
[switch]$Json,
6+
[switch]$DryRun,
67
[string]$ShortName,
78
[Parameter()]
89
[long]$Number = 0,
@@ -15,10 +16,11 @@ $ErrorActionPreference = 'Stop'
1516

1617
# Show help if requested
1718
if ($Help) {
18-
Write-Host "Usage: ./create-new-feature.ps1 [-Json] [-ShortName <name>] [-Number N] [-Timestamp] <feature description>"
19+
Write-Host "Usage: ./create-new-feature.ps1 [-Json] [-DryRun] [-ShortName <name>] [-Number N] [-Timestamp] <feature description>"
1920
Write-Host ""
2021
Write-Host "Options:"
2122
Write-Host " -Json Output in JSON format"
23+
Write-Host " -DryRun Compute branch name and paths without creating branches, directories, or files"
2224
Write-Host " -ShortName <name> Provide a custom short name (2-4 words) for the branch"
2325
Write-Host " -Number N Specify branch number manually (overrides auto-detection)"
2426
Write-Host " -Timestamp Use timestamp prefix (YYYYMMDD-HHMMSS) instead of sequential numbering"
@@ -33,7 +35,7 @@ if ($Help) {
3335

3436
# Check if feature description provided
3537
if (-not $FeatureDescription -or $FeatureDescription.Count -eq 0) {
36-
Write-Error "Usage: ./create-new-feature.ps1 [-Json] [-ShortName <name>] [-Number N] [-Timestamp] <feature description>"
38+
Write-Error "Usage: ./create-new-feature.ps1 [-Json] [-DryRun] [-ShortName <name>] [-Number N] [-Timestamp] <feature description>"
3739
exit 1
3840
}
3941

@@ -201,7 +203,15 @@ if ($Timestamp) {
201203
} else {
202204
# Determine branch number
203205
if ($Number -eq 0) {
204-
if ($hasGit) {
206+
if ($DryRun) {
207+
# Dry-run: use locally available data only (skip git fetch)
208+
$highestBranch = 0
209+
if ($hasGit) {
210+
$highestBranch = Get-HighestNumberFromBranches
211+
}
212+
$highestSpec = Get-HighestNumberFromSpecs -SpecsDir $specsDir
213+
$Number = [Math]::Max($highestBranch, $highestSpec) + 1
214+
} elseif ($hasGit) {
205215
# Check existing branches on remotes
206216
$Number = Get-NextBranchNumber -SpecsDir $specsDir
207217
} else {
@@ -236,62 +246,70 @@ if ($branchName.Length -gt $maxBranchLength) {
236246
Write-Warning "[specify] Truncated to: $branchName ($($branchName.Length) bytes)"
237247
}
238248

239-
if ($hasGit) {
240-
$branchCreated = $false
241-
try {
242-
git checkout -q -b $branchName 2>$null | Out-Null
243-
if ($LASTEXITCODE -eq 0) {
244-
$branchCreated = $true
249+
$featureDir = Join-Path $specsDir $branchName
250+
$specFile = Join-Path $featureDir 'spec.md'
251+
252+
if (-not $DryRun) {
253+
if ($hasGit) {
254+
$branchCreated = $false
255+
try {
256+
git checkout -q -b $branchName 2>$null | Out-Null
257+
if ($LASTEXITCODE -eq 0) {
258+
$branchCreated = $true
259+
}
260+
} catch {
261+
# Exception during git command
245262
}
246-
} catch {
247-
# Exception during git command
248-
}
249263

250-
if (-not $branchCreated) {
251-
# Check if branch already exists
252-
$existingBranch = git branch --list $branchName 2>$null
253-
if ($existingBranch) {
254-
if ($Timestamp) {
255-
Write-Error "Error: Branch '$branchName' already exists. Rerun to get a new timestamp or use a different -ShortName."
264+
if (-not $branchCreated) {
265+
# Check if branch already exists
266+
$existingBranch = git branch --list $branchName 2>$null
267+
if ($existingBranch) {
268+
if ($Timestamp) {
269+
Write-Error "Error: Branch '$branchName' already exists. Rerun to get a new timestamp or use a different -ShortName."
270+
} else {
271+
Write-Error "Error: Branch '$branchName' already exists. Please use a different feature name or specify a different number with -Number."
272+
}
273+
exit 1
256274
} else {
257-
Write-Error "Error: Branch '$branchName' already exists. Please use a different feature name or specify a different number with -Number."
275+
Write-Error "Error: Failed to create git branch '$branchName'. Please check your git configuration and try again."
276+
exit 1
258277
}
259-
exit 1
260-
} else {
261-
Write-Error "Error: Failed to create git branch '$branchName'. Please check your git configuration and try again."
262-
exit 1
263278
}
279+
} else {
280+
Write-Warning "[specify] Warning: Git repository not detected; skipped branch creation for $branchName"
264281
}
265-
} else {
266-
Write-Warning "[specify] Warning: Git repository not detected; skipped branch creation for $branchName"
267-
}
268282

269-
$featureDir = Join-Path $specsDir $branchName
270-
New-Item -ItemType Directory -Path $featureDir -Force | Out-Null
283+
New-Item -ItemType Directory -Path $featureDir -Force | Out-Null
271284

272-
$template = Resolve-Template -TemplateName 'spec-template' -RepoRoot $repoRoot
273-
$specFile = Join-Path $featureDir 'spec.md'
274-
if ($template -and (Test-Path $template)) {
275-
Copy-Item $template $specFile -Force
276-
} else {
277-
New-Item -ItemType File -Path $specFile | Out-Null
278-
}
285+
$template = Resolve-Template -TemplateName 'spec-template' -RepoRoot $repoRoot
286+
if ($template -and (Test-Path $template)) {
287+
Copy-Item $template $specFile -Force
288+
} else {
289+
New-Item -ItemType File -Path $specFile | Out-Null
290+
}
279291

280-
# Set the SPECIFY_FEATURE environment variable for the current session
281-
$env:SPECIFY_FEATURE = $branchName
292+
# Set the SPECIFY_FEATURE environment variable for the current session
293+
$env:SPECIFY_FEATURE = $branchName
294+
}
282295

283296
if ($Json) {
284-
$obj = [PSCustomObject]@{
297+
$obj = [PSCustomObject]@{
285298
BRANCH_NAME = $branchName
286299
SPEC_FILE = $specFile
287300
FEATURE_NUM = $featureNum
288301
HAS_GIT = $hasGit
289302
}
303+
if ($DryRun) {
304+
$obj | Add-Member -NotePropertyName 'DRY_RUN' -NotePropertyValue $true
305+
}
290306
$obj | ConvertTo-Json -Compress
291307
} else {
292308
Write-Output "BRANCH_NAME: $branchName"
293309
Write-Output "SPEC_FILE: $specFile"
294310
Write-Output "FEATURE_NUM: $featureNum"
295311
Write-Output "HAS_GIT: $hasGit"
296-
Write-Output "SPECIFY_FEATURE environment variable set to: $branchName"
312+
if (-not $DryRun) {
313+
Write-Output "SPECIFY_FEATURE environment variable set to: $branchName"
314+
}
297315
}

0 commit comments

Comments
 (0)