Skip to content

Commit 65ecd53

Browse files
amweissclaudeCopilot
authored
feat: add timestamp-based branch naming option for specify init (#1911)
* feat: add timestamp-based branch naming option for specify init Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Copilot feedback * Fix test * Copilot feedback * Update tests/test_branch_numbering.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent d2559d7 commit 65ecd53

File tree

9 files changed

+491
-62
lines changed

9 files changed

+491
-62
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ The `specify` command supports the following options:
222222
| `--debug` | Flag | Enable detailed debug output for troubleshooting |
223223
| `--github-token` | Option | GitHub token for API requests (or set GH_TOKEN/GITHUB_TOKEN env variable) |
224224
| `--ai-skills` | Flag | Install Prompt.MD templates as agent skills in agent-specific `skills/` directory (requires `--ai`) |
225+
| `--branch-numbering` | Option | Branch numbering strategy: `sequential` (default — `001`, `002`, `003`) or `timestamp` (`YYYYMMDD-HHMMSS`). Timestamp mode is useful for distributed teams to avoid numbering conflicts |
225226

226227
### Examples
227228

@@ -296,6 +297,9 @@ specify init my-project --ai claude --ai-skills
296297
# Initialize in current directory with agent skills
297298
specify init --here --ai gemini --ai-skills
298299

300+
# Use timestamp-based branch numbering (useful for distributed teams)
301+
specify init my-project --ai claude --branch-numbering timestamp
302+
299303
# Check system requirements
300304
specify check
301305
```

scripts/bash/common.sh

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,27 @@ get_current_branch() {
3333
if [[ -d "$specs_dir" ]]; then
3434
local latest_feature=""
3535
local highest=0
36+
local latest_timestamp=""
3637

3738
for dir in "$specs_dir"/*; do
3839
if [[ -d "$dir" ]]; then
3940
local dirname=$(basename "$dir")
40-
if [[ "$dirname" =~ ^([0-9]{3})- ]]; then
41+
if [[ "$dirname" =~ ^([0-9]{8}-[0-9]{6})- ]]; then
42+
# Timestamp-based branch: compare lexicographically
43+
local ts="${BASH_REMATCH[1]}"
44+
if [[ "$ts" > "$latest_timestamp" ]]; then
45+
latest_timestamp="$ts"
46+
latest_feature=$dirname
47+
fi
48+
elif [[ "$dirname" =~ ^([0-9]{3})- ]]; then
4149
local number=${BASH_REMATCH[1]}
4250
number=$((10#$number))
4351
if [[ "$number" -gt "$highest" ]]; then
4452
highest=$number
45-
latest_feature=$dirname
53+
# Only update if no timestamp branch found yet
54+
if [[ -z "$latest_timestamp" ]]; then
55+
latest_feature=$dirname
56+
fi
4657
fi
4758
fi
4859
fi
@@ -72,9 +83,9 @@ check_feature_branch() {
7283
return 0
7384
fi
7485

75-
if [[ ! "$branch" =~ ^[0-9]{3}- ]]; then
86+
if [[ ! "$branch" =~ ^[0-9]{3}- ]] && [[ ! "$branch" =~ ^[0-9]{8}-[0-9]{6}- ]]; then
7687
echo "ERROR: Not on a feature branch. Current branch: $branch" >&2
77-
echo "Feature branches should be named like: 001-feature-name" >&2
88+
echo "Feature branches should be named like: 001-feature-name or 20260319-143022-feature-name" >&2
7889
return 1
7990
fi
8091

@@ -90,15 +101,18 @@ find_feature_dir_by_prefix() {
90101
local branch_name="$2"
91102
local specs_dir="$repo_root/specs"
92103

93-
# Extract numeric prefix from branch (e.g., "004" from "004-whatever")
94-
if [[ ! "$branch_name" =~ ^([0-9]{3})- ]]; then
95-
# If branch doesn't have numeric prefix, fall back to exact match
104+
# Extract prefix from branch (e.g., "004" from "004-whatever" or "20260319-143022" from timestamp branches)
105+
local prefix=""
106+
if [[ "$branch_name" =~ ^([0-9]{8}-[0-9]{6})- ]]; then
107+
prefix="${BASH_REMATCH[1]}"
108+
elif [[ "$branch_name" =~ ^([0-9]{3})- ]]; then
109+
prefix="${BASH_REMATCH[1]}"
110+
else
111+
# If branch doesn't have a recognized prefix, fall back to exact match
96112
echo "$specs_dir/$branch_name"
97113
return
98114
fi
99115

100-
local prefix="${BASH_REMATCH[1]}"
101-
102116
# Search for directories in specs/ that start with this prefix
103117
local matches=()
104118
if [[ -d "$specs_dir" ]]; then
@@ -119,7 +133,7 @@ find_feature_dir_by_prefix() {
119133
else
120134
# Multiple matches - this shouldn't happen with proper naming convention
121135
echo "ERROR: Multiple spec directories found with prefix '$prefix': ${matches[*]}" >&2
122-
echo "Please ensure only one spec directory exists per numeric prefix." >&2
136+
echo "Please ensure only one spec directory exists per prefix." >&2
123137
return 1
124138
fi
125139
}

scripts/bash/create-new-feature.sh

Lines changed: 53 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@ set -e
55
JSON_MODE=false
66
SHORT_NAME=""
77
BRANCH_NUMBER=""
8+
USE_TIMESTAMP=false
89
ARGS=()
910
i=1
1011
while [ $i -le $# ]; do
1112
arg="${!i}"
1213
case "$arg" in
13-
--json)
14-
JSON_MODE=true
14+
--json)
15+
JSON_MODE=true
1516
;;
1617
--short-name)
1718
if [ $((i + 1)) -gt $# ]; then
@@ -40,30 +41,35 @@ while [ $i -le $# ]; do
4041
fi
4142
BRANCH_NUMBER="$next_arg"
4243
;;
43-
--help|-h)
44-
echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>"
44+
--timestamp)
45+
USE_TIMESTAMP=true
46+
;;
47+
--help|-h)
48+
echo "Usage: $0 [--json] [--short-name <name>] [--number N] [--timestamp] <feature_description>"
4549
echo ""
4650
echo "Options:"
4751
echo " --json Output in JSON format"
4852
echo " --short-name <name> Provide a custom short name (2-4 words) for the branch"
4953
echo " --number N Specify branch number manually (overrides auto-detection)"
54+
echo " --timestamp Use timestamp prefix (YYYYMMDD-HHMMSS) instead of sequential numbering"
5055
echo " --help, -h Show this help message"
5156
echo ""
5257
echo "Examples:"
5358
echo " $0 'Add user authentication system' --short-name 'user-auth'"
5459
echo " $0 'Implement OAuth2 integration for API' --number 5"
60+
echo " $0 --timestamp --short-name 'user-auth' 'Add user authentication'"
5561
exit 0
5662
;;
57-
*)
58-
ARGS+=("$arg")
63+
*)
64+
ARGS+=("$arg")
5965
;;
6066
esac
6167
i=$((i + 1))
6268
done
6369

6470
FEATURE_DESCRIPTION="${ARGS[*]}"
6571
if [ -z "$FEATURE_DESCRIPTION" ]; then
66-
echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>" >&2
72+
echo "Usage: $0 [--json] [--short-name <name>] [--number N] [--timestamp] <feature_description>" >&2
6773
exit 1
6874
fi
6975

@@ -96,10 +102,13 @@ get_highest_from_specs() {
96102
for dir in "$specs_dir"/*; do
97103
[ -d "$dir" ] || continue
98104
dirname=$(basename "$dir")
99-
number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0")
100-
number=$((10#$number))
101-
if [ "$number" -gt "$highest" ]; then
102-
highest=$number
105+
# Only match sequential prefixes (###-*), skip timestamp dirs
106+
if echo "$dirname" | grep -q '^[0-9]\{3\}-'; then
107+
number=$(echo "$dirname" | grep -o '^[0-9]\{3\}')
108+
number=$((10#$number))
109+
if [ "$number" -gt "$highest" ]; then
110+
highest=$number
111+
fi
103112
fi
104113
done
105114
fi
@@ -242,29 +251,42 @@ else
242251
BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION")
243252
fi
244253

245-
# Determine branch number
246-
if [ -z "$BRANCH_NUMBER" ]; then
247-
if [ "$HAS_GIT" = true ]; then
248-
# Check existing branches on remotes
249-
BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR")
250-
else
251-
# Fall back to local directory check
252-
HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
253-
BRANCH_NUMBER=$((HIGHEST + 1))
254-
fi
254+
# Warn if --number and --timestamp are both specified
255+
if [ "$USE_TIMESTAMP" = true ] && [ -n "$BRANCH_NUMBER" ]; then
256+
>&2 echo "[specify] Warning: --number is ignored when --timestamp is used"
257+
BRANCH_NUMBER=""
255258
fi
256259

257-
# Force base-10 interpretation to prevent octal conversion (e.g., 010 → 8 in octal, but should be 10 in decimal)
258-
FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))")
259-
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
260+
# Determine branch prefix
261+
if [ "$USE_TIMESTAMP" = true ]; then
262+
FEATURE_NUM=$(date +%Y%m%d-%H%M%S)
263+
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
264+
else
265+
# Determine branch number
266+
if [ -z "$BRANCH_NUMBER" ]; then
267+
if [ "$HAS_GIT" = true ]; then
268+
# Check existing branches on remotes
269+
BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR")
270+
else
271+
# Fall back to local directory check
272+
HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
273+
BRANCH_NUMBER=$((HIGHEST + 1))
274+
fi
275+
fi
276+
277+
# Force base-10 interpretation to prevent octal conversion (e.g., 010 → 8 in octal, but should be 10 in decimal)
278+
FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))")
279+
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
280+
fi
260281

261282
# GitHub enforces a 244-byte limit on branch names
262283
# Validate and truncate if necessary
263284
MAX_BRANCH_LENGTH=244
264285
if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then
265286
# Calculate how much we need to trim from suffix
266-
# Account for: feature number (3) + hyphen (1) = 4 chars
267-
MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - 4))
287+
# Account for prefix length: timestamp (15) + hyphen (1) = 16, or sequential (3) + hyphen (1) = 4
288+
PREFIX_LENGTH=$(( ${#FEATURE_NUM} + 1 ))
289+
MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - PREFIX_LENGTH))
268290

269291
# Truncate suffix at word boundary if possible
270292
TRUNCATED_SUFFIX=$(echo "$BRANCH_SUFFIX" | cut -c1-$MAX_SUFFIX_LENGTH)
@@ -283,7 +305,11 @@ if [ "$HAS_GIT" = true ]; then
283305
if ! git checkout -b "$BRANCH_NAME" 2>/dev/null; then
284306
# Check if branch already exists
285307
if git branch --list "$BRANCH_NAME" | grep -q .; then
286-
>&2 echo "Error: Branch '$BRANCH_NAME' already exists. Please use a different feature name or specify a different number with --number."
308+
if [ "$USE_TIMESTAMP" = true ]; then
309+
>&2 echo "Error: Branch '$BRANCH_NAME' already exists. Rerun to get a new timestamp or use a different --short-name."
310+
else
311+
>&2 echo "Error: Branch '$BRANCH_NAME' already exists. Please use a different feature name or specify a different number with --number."
312+
fi
287313
exit 1
288314
else
289315
>&2 echo "Error: Failed to create git branch '$BRANCH_NAME'. Please check your git configuration and try again."

scripts/powershell/common.ps1

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,28 @@ function Get-CurrentBranch {
3838
if (Test-Path $specsDir) {
3939
$latestFeature = ""
4040
$highest = 0
41-
41+
$latestTimestamp = ""
42+
4243
Get-ChildItem -Path $specsDir -Directory | ForEach-Object {
43-
if ($_.Name -match '^(\d{3})-') {
44+
if ($_.Name -match '^(\d{8}-\d{6})-') {
45+
# Timestamp-based branch: compare lexicographically
46+
$ts = $matches[1]
47+
if ($ts -gt $latestTimestamp) {
48+
$latestTimestamp = $ts
49+
$latestFeature = $_.Name
50+
}
51+
} elseif ($_.Name -match '^(\d{3})-') {
4452
$num = [int]$matches[1]
4553
if ($num -gt $highest) {
4654
$highest = $num
47-
$latestFeature = $_.Name
55+
# Only update if no timestamp branch found yet
56+
if (-not $latestTimestamp) {
57+
$latestFeature = $_.Name
58+
}
4859
}
4960
}
5061
}
51-
62+
5263
if ($latestFeature) {
5364
return $latestFeature
5465
}
@@ -79,9 +90,9 @@ function Test-FeatureBranch {
7990
return $true
8091
}
8192

82-
if ($Branch -notmatch '^[0-9]{3}-') {
93+
if ($Branch -notmatch '^[0-9]{3}-' -and $Branch -notmatch '^\d{8}-\d{6}-') {
8394
Write-Output "ERROR: Not on a feature branch. Current branch: $Branch"
84-
Write-Output "Feature branches should be named like: 001-feature-name"
95+
Write-Output "Feature branches should be named like: 001-feature-name or 20260319-143022-feature-name"
8596
return $false
8697
}
8798
return $true

0 commit comments

Comments
 (0)