create-new-feature.sh
9.53 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
#!/usr/bin/env bash
set -e
JSON_MODE=false
SHORT_NAME=""
BRANCH_NUMBER=""
ARGS=()
i=1
while [ $i -le $# ]; do
arg="${!i}"
case "$arg" in
--json)
JSON_MODE=true
;;
--short-name)
if [ $((i + 1)) -gt $# ]; then
echo '错误:--short-name 需要提供值' >&2
exit 1
fi
i=$((i + 1))
next_arg="${!i}"
# 检查下一个参数是否为另一个选项(以 -- 开头)
if [[ "$next_arg" == --* ]]; then
echo '错误:--short-name 需要提供值' >&2
exit 1
fi
SHORT_NAME="$next_arg"
;;
--number)
if [ $((i + 1)) -gt $# ]; then
echo '错误:--number 需要提供值' >&2
exit 1
fi
i=$((i + 1))
next_arg="${!i}"
if [[ "$next_arg" == --* ]]; then
echo '错误:--number 需要提供值' >&2
exit 1
fi
BRANCH_NUMBER="$next_arg"
;;
--help|-h)
echo "用法:$0 [--json] [--short-name <短名>] [--number N] <功能描述>"
echo ""
echo "选项:"
echo " --json 以 JSON 格式输出"
echo " --short-name <短名> 为分支提供自定义短名(2-4 个单词)"
echo " --number N 手动指定分支编号(覆盖自动检测)"
echo " --help, -h 显示帮助信息"
echo ""
echo "示例:"
echo " $0 '新增用户登录与鉴权能力' --short-name 'user-auth'"
echo " $0 '为 API 接入 OAuth2 登录' --number 5"
exit 0
;;
*)
ARGS+=("$arg")
;;
esac
i=$((i + 1))
done
FEATURE_DESCRIPTION="${ARGS[*]}"
if [ -z "$FEATURE_DESCRIPTION" ]; then
echo "用法:$0 [--json] [--short-name <短名>] [--number N] <功能描述>" >&2
exit 1
fi
# 通过查找项目标记来定位仓库根目录
find_repo_root() {
local dir="$1"
while [ "$dir" != "/" ]; do
if [ -d "$dir/.git" ] || [ -d "$dir/.specify" ]; then
echo "$dir"
return 0
fi
dir="$(dirname "$dir")"
done
return 1
}
# 从 specs 目录中获取最大编号
get_highest_from_specs() {
local specs_dir="$1"
local highest=0
if [ -d "$specs_dir" ]; then
for dir in "$specs_dir"/*; do
[ -d "$dir" ] || continue
dirname=$(basename "$dir")
number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0")
number=$((10#$number))
if [ "$number" -gt "$highest" ]; then
highest=$number
fi
done
fi
echo "$highest"
}
# 从 git 分支中获取最大编号
get_highest_from_branches() {
local highest=0
# 获取所有分支(本地 + 远端)
branches=$(git branch -a 2>/dev/null || echo "")
if [ -n "$branches" ]; then
while IFS= read -r branch; do
# 清理分支名:移除前缀标记与远端前缀
clean_branch=$(echo "$branch" | sed 's/^[* ]*//; s|^remotes/[^/]*/||')
# 若分支符合 ###-*,则提取功能编号
if echo "$clean_branch" | grep -q '^[0-9]\{3\}-'; then
number=$(echo "$clean_branch" | grep -o '^[0-9]\{3\}' || echo "0")
number=$((10#$number))
if [ "$number" -gt "$highest" ]; then
highest=$number
fi
fi
done <<< "$branches"
fi
echo "$highest"
}
# 检查现有分支(本地 + 远端)并返回下一个可用编号
check_existing_branches() {
local specs_dir="$1"
# 拉取所有远端以获取最新分支信息(无远端时忽略错误)
git fetch --all --prune 2>/dev/null || true
# 获取所有分支中的最大编号(不局限于匹配 short name)
local highest_branch=$(get_highest_from_branches)
# 获取所有 specs 中的最大编号(不局限于匹配 short name)
local highest_spec=$(get_highest_from_specs "$specs_dir")
# 两者取最大值
local max_num=$highest_branch
if [ "$highest_spec" -gt "$max_num" ]; then
max_num=$highest_spec
fi
# 返回下一个编号
echo $((max_num + 1))
}
# 清理并格式化分支名
clean_branch_name() {
local name="$1"
echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//'
}
# 解析仓库根目录:优先使用 git 信息;如不可用则回退为查找仓库标记,
# 以兼容使用 --no-git 初始化的仓库。
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if git rev-parse --show-toplevel >/dev/null 2>&1; then
REPO_ROOT=$(git rev-parse --show-toplevel)
HAS_GIT=true
else
REPO_ROOT="$(find_repo_root "$SCRIPT_DIR")"
if [ -z "$REPO_ROOT" ]; then
echo "错误:无法确定仓库根目录。请在仓库内运行本脚本。" >&2
exit 1
fi
HAS_GIT=false
fi
cd "$REPO_ROOT"
SPECS_DIR="$REPO_ROOT/specs"
mkdir -p "$SPECS_DIR"
# 生成分支名:包含停用词过滤与长度过滤
generate_branch_name() {
local description="$1"
# 常见停用词(用于过滤)
local stop_words="^(i|a|an|the|to|for|of|in|on|at|by|with|from|is|are|was|were|be|been|being|have|has|had|do|does|did|will|would|should|could|can|may|might|must|shall|this|that|these|those|my|your|our|their|want|need|add|get|set)$"
# 转小写并按单词拆分
local clean_name=$(echo "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g')
# 过滤单词:移除停用词与长度 < 3 的词(除非在原描述中以大写出现,通常为缩写)
local meaningful_words=()
for word in $clean_name; do
# 跳过空词
[ -z "$word" ] && continue
# 保留:不属于停用词,并且(长度 >= 3 或为可能的缩写)
if ! echo "$word" | grep -qiE "$stop_words"; then
if [ ${#word} -ge 3 ]; then
meaningful_words+=("$word")
elif echo "$description" | grep -q "\b${word^^}\b"; then
# 原描述中若以大写出现则保留(很可能是缩写)
meaningful_words+=("$word")
fi
fi
done
# 若有有效词,则取前 3-4 个
if [ ${#meaningful_words[@]} -gt 0 ]; then
local max_words=3
if [ ${#meaningful_words[@]} -eq 4 ]; then max_words=4; fi
local result=""
local count=0
for word in "${meaningful_words[@]}"; do
if [ $count -ge $max_words ]; then break; fi
if [ -n "$result" ]; then result="$result-"; fi
result="$result$word"
count=$((count + 1))
done
echo "$result"
else
# 若未找到有效词:回退为原始逻辑
local cleaned=$(clean_branch_name "$description")
echo "$cleaned" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//'
fi
}
# 生成分支名
if [ -n "$SHORT_NAME" ]; then
# 使用用户提供的 short name,并做清理
BRANCH_SUFFIX=$(clean_branch_name "$SHORT_NAME")
else
# 根据描述智能过滤生成
BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION")
fi
# 确定分支编号
if [ -z "$BRANCH_NUMBER" ]; then
if [ "$HAS_GIT" = true ]; then
# 检查远端已有分支
BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR")
else
# 回退为本地目录检查
HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
BRANCH_NUMBER=$((HIGHEST + 1))
fi
fi
# 强制按十进制解释,避免八进制转换(例如 010 在八进制会变为 8,但这里应是十进制 10)
FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))")
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
# GitHub 对分支名有 244 字节限制:必要时校验并截断
MAX_BRANCH_LENGTH=244
if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then
# 计算需要从后缀中截断的长度
# 分支名包含:功能编号(3)+ 连字符(1)= 4 个字符
MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - 4))
# 尽量按单词边界截断后缀
TRUNCATED_SUFFIX=$(echo "$BRANCH_SUFFIX" | cut -c1-$MAX_SUFFIX_LENGTH)
# 若截断产生了末尾连字符则移除
TRUNCATED_SUFFIX=$(echo "$TRUNCATED_SUFFIX" | sed 's/-$//')
ORIGINAL_BRANCH_NAME="$BRANCH_NAME"
BRANCH_NAME="${FEATURE_NUM}-${TRUNCATED_SUFFIX}"
>&2 echo "[specify] Warning: 分支名超过 GitHub 的 244 字节限制"
>&2 echo "[specify] 原始:$ORIGINAL_BRANCH_NAME(${#ORIGINAL_BRANCH_NAME} 字节)"
>&2 echo "[specify] 已截断为:$BRANCH_NAME(${#BRANCH_NAME} 字节)"
fi
if [ "$HAS_GIT" = true ]; then
git checkout -b "$BRANCH_NAME"
else
>&2 echo "[specify] Warning: 未检测到 Git 仓库;已跳过创建分支 $BRANCH_NAME"
fi
FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
mkdir -p "$FEATURE_DIR"
TEMPLATE="$REPO_ROOT/.specify/templates/spec-template.md"
SPEC_FILE="$FEATURE_DIR/spec.md"
if [ -f "$TEMPLATE" ]; then cp "$TEMPLATE" "$SPEC_FILE"; else touch "$SPEC_FILE"; fi
# 为当前会话设置 SPECIFY_FEATURE 环境变量
export SPECIFY_FEATURE="$BRANCH_NAME"
if $JSON_MODE; then
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$BRANCH_NAME" "$SPEC_FILE" "$FEATURE_NUM"
else
echo "BRANCH_NAME: $BRANCH_NAME"
echo "SPEC_FILE: $SPEC_FILE"
echo "FEATURE_NUM: $FEATURE_NUM"
echo "已设置 SPECIFY_FEATURE 环境变量为:$BRANCH_NAME"
fi