@zjex/git-workflow 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,363 @@
1
+ #!/bin/bash
2
+
3
+ set -e
4
+
5
+ # 颜色定义
6
+ RED='\033[0;31m'
7
+ GREEN='\033[0;32m'
8
+ YELLOW='\033[1;33m'
9
+ BLUE='\033[0;34m'
10
+ CYAN='\033[0;36m'
11
+ NC='\033[0m' # No Color
12
+
13
+ # 打印带颜色的消息
14
+ print_info() {
15
+ echo -e "${BLUE}ℹ ${1}${NC}"
16
+ }
17
+
18
+ print_success() {
19
+ echo -e "${GREEN}✔ ${1}${NC}"
20
+ }
21
+
22
+ print_error() {
23
+ echo -e "${RED}✖ ${1}${NC}"
24
+ }
25
+
26
+ print_warning() {
27
+ echo -e "${YELLOW}⚠ ${1}${NC}"
28
+ }
29
+
30
+ print_step() {
31
+ echo -e "${CYAN}▶ ${1}${NC}"
32
+ }
33
+
34
+ # 错误处理
35
+ trap 'handle_error $? $LINENO' ERR
36
+
37
+ handle_error() {
38
+ print_error "发布失败 (退出码: $1, 行号: $2)"
39
+
40
+ if [[ -n "$NEW_VERSION" ]]; then
41
+ print_warning "正在回滚更改..."
42
+
43
+ # 回滚 package.json
44
+ if [[ -f "package.json.backup" ]]; then
45
+ mv package.json.backup package.json
46
+ print_info "已恢复 package.json"
47
+ fi
48
+
49
+ # 删除本地 tag
50
+ if git tag -l "v${NEW_VERSION}" | grep -q "v${NEW_VERSION}"; then
51
+ git tag -d "v${NEW_VERSION}" 2>/dev/null || true
52
+ print_info "已删除本地 tag"
53
+ fi
54
+
55
+ # 回滚 commit
56
+ if git log -1 --pretty=%B | grep -q "chore(release): v${NEW_VERSION}"; then
57
+ git reset --hard HEAD~1 2>/dev/null || true
58
+ print_info "已回滚 commit"
59
+ fi
60
+ fi
61
+
62
+ exit 1
63
+ }
64
+
65
+ # 检查命令是否存在
66
+ check_command() {
67
+ if ! command -v "$1" &> /dev/null; then
68
+ print_error "未找到命令: $1"
69
+ exit 1
70
+ fi
71
+ }
72
+
73
+ # 检查必要的命令
74
+ check_command git
75
+ check_command node
76
+ check_command npm
77
+
78
+ # Dry-run 模式
79
+ DRY_RUN=false
80
+ if [[ "$1" == "--dry-run" ]]; then
81
+ DRY_RUN=true
82
+ print_warning "Dry-run 模式:仅预览,不会实际执行"
83
+ echo ""
84
+ fi
85
+
86
+ # 检查是否在 git 仓库中
87
+ if ! git rev-parse --git-dir > /dev/null 2>&1; then
88
+ print_error "当前目录不是 git 仓库"
89
+ exit 1
90
+ fi
91
+
92
+ # 检查是否有未提交的更改
93
+ if [[ -n $(git status --porcelain) ]]; then
94
+ print_error "有未提交的更改,请先提交或暂存"
95
+ git status --short
96
+ exit 1
97
+ fi
98
+
99
+ # 检查当前分支
100
+ CURRENT_BRANCH=$(git branch --show-current)
101
+ if [[ "$CURRENT_BRANCH" != "main" && "$CURRENT_BRANCH" != "master" ]]; then
102
+ print_warning "当前分支是 ${CURRENT_BRANCH},建议在 main/master 分支发布"
103
+ read -p "是否继续? (y/N) " -n 1 -r
104
+ echo
105
+ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
106
+ print_info "已取消"
107
+ exit 0
108
+ fi
109
+ fi
110
+
111
+ # 检查 npm 登录状态
112
+ print_step "检查 npm 登录状态..."
113
+ if ! npm whoami &> /dev/null; then
114
+ print_error "未登录 npm,请先执行: npm login"
115
+ exit 1
116
+ fi
117
+ NPM_USER=$(npm whoami)
118
+ print_success "已登录 npm (用户: ${NPM_USER})"
119
+
120
+ # 拉取最新代码
121
+ print_step "拉取最新代码..."
122
+ if [[ "$DRY_RUN" == false ]]; then
123
+ git pull origin "$CURRENT_BRANCH"
124
+ fi
125
+ print_success "代码已更新"
126
+
127
+ # 获取当前版本
128
+ CURRENT_VERSION=$(node -p "require('./package.json').version")
129
+ print_info "当前版本: ${CURRENT_VERSION}"
130
+
131
+ # 检查远程是否已存在该版本的 tag
132
+ check_tag_exists() {
133
+ local tag="v$1"
134
+ if git ls-remote --tags origin | grep -q "refs/tags/${tag}$"; then
135
+ return 0
136
+ else
137
+ return 1
138
+ fi
139
+ }
140
+
141
+ # 计算下一个版本号
142
+ calculate_next_version() {
143
+ local current=$1
144
+ local type=$2
145
+
146
+ IFS='.' read -r major minor patch <<< "$current"
147
+
148
+ case $type in
149
+ patch)
150
+ echo "${major}.${minor}.$((patch + 1))"
151
+ ;;
152
+ minor)
153
+ echo "${major}.$((minor + 1)).0"
154
+ ;;
155
+ major)
156
+ echo "$((major + 1)).0.0"
157
+ ;;
158
+ esac
159
+ }
160
+
161
+ # 验证版本号格式
162
+ validate_version() {
163
+ if [[ ! $1 =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$ ]]; then
164
+ return 1
165
+ fi
166
+ return 0
167
+ }
168
+
169
+ # 选择版本类型
170
+ echo ""
171
+ print_step "选择新版本号"
172
+ echo ""
173
+
174
+ PATCH_VERSION=$(calculate_next_version "$CURRENT_VERSION" "patch")
175
+ MINOR_VERSION=$(calculate_next_version "$CURRENT_VERSION" "minor")
176
+ MAJOR_VERSION=$(calculate_next_version "$CURRENT_VERSION" "major")
177
+
178
+ echo -e " ${GREEN}1)${NC} patch ${CYAN}${CURRENT_VERSION}${NC} → ${GREEN}${PATCH_VERSION}${NC} (bug 修复)"
179
+ echo -e " ${GREEN}2)${NC} minor ${CYAN}${CURRENT_VERSION}${NC} → ${GREEN}${MINOR_VERSION}${NC} (新功能)"
180
+ echo -e " ${GREEN}3)${NC} major ${CYAN}${CURRENT_VERSION}${NC} → ${GREEN}${MAJOR_VERSION}${NC} (破坏性更新)"
181
+ echo -e " ${GREEN}4)${NC} custom (自定义版本号)"
182
+ echo -e " ${RED}5)${NC} cancel (取消发布)"
183
+ echo ""
184
+
185
+ while true; do
186
+ read -p "请选择 (1-5): " -n 1 -r VERSION_TYPE
187
+ echo ""
188
+
189
+ if [[ "$VERSION_TYPE" =~ ^[1-5]$ ]]; then
190
+ break
191
+ else
192
+ print_error "无效的选择,请输入 1-5"
193
+ fi
194
+ done
195
+
196
+ # 备份 package.json
197
+ cp package.json package.json.backup
198
+
199
+ case $VERSION_TYPE in
200
+ 1)
201
+ NEW_VERSION=$PATCH_VERSION
202
+ ;;
203
+ 2)
204
+ NEW_VERSION=$MINOR_VERSION
205
+ ;;
206
+ 3)
207
+ NEW_VERSION=$MAJOR_VERSION
208
+ ;;
209
+ 4)
210
+ while true; do
211
+ read -p "请输入版本号 (如 1.0.0 或 1.0.0-beta.1): " CUSTOM_VERSION
212
+
213
+ if [[ -z "$CUSTOM_VERSION" ]]; then
214
+ print_error "版本号不能为空"
215
+ continue
216
+ fi
217
+
218
+ if ! validate_version "$CUSTOM_VERSION"; then
219
+ print_error "版本号格式无效,请使用语义化版本格式 (如 1.0.0 或 1.0.0-beta.1)"
220
+ continue
221
+ fi
222
+
223
+ NEW_VERSION=$CUSTOM_VERSION
224
+ break
225
+ done
226
+ ;;
227
+ 5)
228
+ rm package.json.backup
229
+ print_info "已取消发布"
230
+ exit 0
231
+ ;;
232
+ esac
233
+
234
+ # 更新 package.json 中的版本号
235
+ if [[ "$DRY_RUN" == false ]]; then
236
+ npm version "$NEW_VERSION" --no-git-tag-version > /dev/null 2>&1 || {
237
+ print_error "更新版本号失败"
238
+ mv package.json.backup package.json
239
+ exit 1
240
+ }
241
+ fi
242
+
243
+ print_success "版本号已更新: ${CURRENT_VERSION} → ${NEW_VERSION}"
244
+
245
+ # 检查版本号是否已存在
246
+ if check_tag_exists "$NEW_VERSION"; then
247
+ print_error "版本 v${NEW_VERSION} 已存在于远程仓库"
248
+ mv package.json.backup package.json
249
+ exit 1
250
+ fi
251
+
252
+ # 运行测试(如果有)
253
+ if grep -q '"test"' package.json; then
254
+ print_step "运行测试..."
255
+ if [[ "$DRY_RUN" == false ]]; then
256
+ npm test || {
257
+ print_error "测试失败"
258
+ mv package.json.backup package.json
259
+ exit 1
260
+ }
261
+ fi
262
+ print_success "测试通过"
263
+ fi
264
+
265
+ # 构建项目
266
+ print_step "构建项目..."
267
+ if [[ "$DRY_RUN" == false ]]; then
268
+ npm run build
269
+ fi
270
+ print_success "构建完成"
271
+
272
+ # 检查构建产物
273
+ if [[ "$DRY_RUN" == false ]]; then
274
+ if [[ ! -f "dist/index.js" ]]; then
275
+ print_error "构建产物不存在: dist/index.js"
276
+ mv package.json.backup package.json
277
+ exit 1
278
+ fi
279
+ print_success "构建产物验证通过"
280
+ fi
281
+
282
+ # 生成 changelog
283
+ print_step "生成 CHANGELOG..."
284
+ if [[ "$DRY_RUN" == false ]]; then
285
+ npm run changelog
286
+ fi
287
+ print_success "CHANGELOG 已更新"
288
+
289
+ # 预览 changelog
290
+ if [[ "$DRY_RUN" == false ]]; then
291
+ echo ""
292
+ print_info "最新的 CHANGELOG 内容:"
293
+ echo "----------------------------------------"
294
+ head -n 30 CHANGELOG.md
295
+ echo "----------------------------------------"
296
+ echo ""
297
+ read -p "是否继续发布? (y/N) " -n 1 -r
298
+ echo
299
+ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
300
+ print_info "已取消"
301
+ mv package.json.backup package.json
302
+ exit 0
303
+ fi
304
+ fi
305
+
306
+ # 删除备份
307
+ rm package.json.backup
308
+
309
+ if [[ "$DRY_RUN" == true ]]; then
310
+ echo ""
311
+ print_success "Dry-run 完成!以下是将要执行的操作:"
312
+ echo ""
313
+ echo " 1. 提交更改: package.json, package-lock.json, CHANGELOG.md"
314
+ echo " 2. Commit 信息: 🔖 chore(release): v${NEW_VERSION}"
315
+ echo " 3. 创建 tag: v${NEW_VERSION}"
316
+ echo " 4. 推送到 GitHub: ${CURRENT_BRANCH} + v${NEW_VERSION}"
317
+ echo " 5. 发布到 npm: @zjex/git-workflow@${NEW_VERSION}"
318
+ echo ""
319
+ print_info "执行 'npm run release' 进行实际发布"
320
+ exit 0
321
+ fi
322
+
323
+ # 最终确认
324
+ echo ""
325
+ print_warning "即将发布版本 v${NEW_VERSION} 到 npm 和 GitHub"
326
+ read -p "确认发布? (y/N) " -n 1 -r
327
+ echo
328
+ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
329
+ print_info "已取消"
330
+ exit 0
331
+ fi
332
+
333
+ # 提交更改
334
+ print_step "提交更改..."
335
+ git add package.json package-lock.json CHANGELOG.md
336
+ git commit -m "🔖 chore(release): v${NEW_VERSION}"
337
+ print_success "更改已提交"
338
+
339
+ # 创建 tag
340
+ print_step "创建 tag: v${NEW_VERSION}..."
341
+ git tag -a "v${NEW_VERSION}" -m "Release v${NEW_VERSION}"
342
+ print_success "Tag 已创建"
343
+
344
+ # 推送到 GitHub
345
+ print_step "推送到 GitHub..."
346
+ git push origin "$CURRENT_BRANCH"
347
+ git push origin "v${NEW_VERSION}"
348
+ print_success "已推送到 GitHub"
349
+
350
+ # 发布到 npm
351
+ print_step "发布到 npm..."
352
+ npm publish
353
+ print_success "已发布到 npm"
354
+
355
+ echo ""
356
+ print_success "🎉 发布成功!"
357
+ echo ""
358
+ echo "版本: v${NEW_VERSION}"
359
+ echo "GitHub: https://github.com/iamzjt-front-end/git-workflow/releases/tag/v${NEW_VERSION}"
360
+ echo "npm: https://www.npmjs.com/package/@zjex/git-workflow/v/${NEW_VERSION}"
361
+ echo ""
362
+ print_info "提示: 可以在 GitHub 上创建 Release 并添加发布说明"
363
+ echo " https://github.com/iamzjt-front-end/git-workflow/releases/new?tag=v${NEW_VERSION}"
@@ -49,6 +49,38 @@ export async function createBranch(
49
49
  ): Promise<void> {
50
50
  const config = getConfig();
51
51
 
52
+ // 检查是否有未提交的更改
53
+ const hasChanges = execOutput("git status --porcelain");
54
+ if (hasChanges) {
55
+ console.log(colors.yellow("检测到未提交的更改:"));
56
+ console.log(colors.dim(hasChanges));
57
+ divider();
58
+
59
+ const shouldStash = await select({
60
+ message: "是否暂存 (stash) 这些更改后继续?",
61
+ choices: [
62
+ { name: "是", value: true },
63
+ { name: "否,取消操作", value: false },
64
+ ],
65
+ theme,
66
+ });
67
+
68
+ if (!shouldStash) {
69
+ console.log(colors.yellow("已取消"));
70
+ return;
71
+ }
72
+
73
+ const stashSpinner = ora("正在暂存更改...").start();
74
+ try {
75
+ exec('git stash push -m "auto stash before branch switch"', true);
76
+ stashSpinner.succeed("更改已暂存,切换分支后可用 gw s 恢复");
77
+ } catch {
78
+ stashSpinner.fail("暂存失败");
79
+ return;
80
+ }
81
+ divider();
82
+ }
83
+
52
84
  const branchName = await getBranchName(type);
53
85
  if (!branchName) return;
54
86
 
@@ -118,15 +150,32 @@ export async function deleteBranch(branchArg?: string): Promise<void> {
118
150
 
119
151
  let branch = branchArg;
120
152
 
153
+ // 如果传入的是 origin/xxx 格式,提取分支名
154
+ if (branch?.startsWith("origin/")) {
155
+ branch = branch.replace("origin/", "");
156
+ }
157
+
121
158
  if (!branch) {
122
- const recentBranches = execOutput(
159
+ // 获取本地分支
160
+ const localBranches = execOutput(
123
161
  "git for-each-ref --sort=-committerdate refs/heads/ --format='%(refname:short)'"
124
162
  )
125
163
  .split("\n")
126
164
  .filter((b) => b && b !== currentBranch);
127
165
 
128
- if (recentBranches.length === 0) {
129
- console.log(colors.yellow("没有可删除的本地分支"));
166
+ // 获取远程分支(排除 HEAD 和已有本地分支的)
167
+ const remoteBranches = execOutput(
168
+ "git for-each-ref --sort=-committerdate refs/remotes/origin/ --format='%(refname:short)'"
169
+ )
170
+ .split("\n")
171
+ .map((b) => b.replace("origin/", ""))
172
+ .filter(
173
+ (b) =>
174
+ b && b !== "HEAD" && b !== currentBranch && !localBranches.includes(b)
175
+ );
176
+
177
+ if (localBranches.length === 0 && remoteBranches.length === 0) {
178
+ console.log(colors.yellow("没有可删除的分支"));
130
179
  return;
131
180
  }
132
181
 
@@ -135,13 +184,25 @@ export async function deleteBranch(branchArg?: string): Promise<void> {
135
184
  value: string;
136
185
  }
137
186
 
138
- const choices: BranchChoice[] = recentBranches.map((b) => {
139
- const hasRemote = execOutput(`git branch -r | grep "origin/${b}$"`);
140
- return {
187
+ const choices: BranchChoice[] = [];
188
+
189
+ // 本地分支
190
+ localBranches.forEach((b) => {
191
+ const hasRemote = execOutput(`git branch -r --list "origin/${b}"`);
192
+ choices.push({
141
193
  name: hasRemote ? `${b} (本地+远程)` : `${b} (仅本地)`,
142
194
  value: b,
143
- };
195
+ });
144
196
  });
197
+
198
+ // 仅远程分支
199
+ remoteBranches.forEach((b) => {
200
+ choices.push({
201
+ name: `${b} (仅远程)`,
202
+ value: `__remote__${b}`,
203
+ });
204
+ });
205
+
145
206
  choices.push({ name: "取消", value: "__cancel__" });
146
207
 
147
208
  branch = await select({
@@ -154,6 +215,36 @@ export async function deleteBranch(branchArg?: string): Promise<void> {
154
215
  console.log(colors.yellow("已取消"));
155
216
  return;
156
217
  }
218
+
219
+ // 处理仅远程分支的情况
220
+ if (branch.startsWith("__remote__")) {
221
+ const remoteBranch = branch.replace("__remote__", "");
222
+
223
+ const confirm = await select({
224
+ message: `确认删除远程分支 origin/${remoteBranch}?`,
225
+ choices: [
226
+ { name: "是", value: true },
227
+ { name: "否", value: false },
228
+ ],
229
+ theme,
230
+ });
231
+
232
+ if (!confirm) {
233
+ console.log(colors.yellow("已取消"));
234
+ return;
235
+ }
236
+
237
+ const spinner = ora(`正在删除远程分支: origin/${remoteBranch}`).start();
238
+ try {
239
+ execSync(`git push origin --delete "${remoteBranch}"`, {
240
+ stdio: "pipe",
241
+ });
242
+ spinner.succeed(`远程分支已删除: origin/${remoteBranch}`);
243
+ } catch {
244
+ spinner.fail("远程分支删除失败");
245
+ }
246
+ return;
247
+ }
157
248
  }
158
249
 
159
250
  if (branch === currentBranch) {
@@ -162,7 +253,7 @@ export async function deleteBranch(branchArg?: string): Promise<void> {
162
253
  }
163
254
 
164
255
  const localExists = execOutput(`git branch --list "${branch}"`);
165
- const hasRemote = execOutput(`git branch -r | grep "origin/${branch}$"`);
256
+ const hasRemote = execOutput(`git branch -r --list "origin/${branch}"`);
166
257
 
167
258
  if (!localExists) {
168
259
  if (hasRemote) {
@@ -170,7 +261,7 @@ export async function deleteBranch(branchArg?: string): Promise<void> {
170
261
  colors.yellow(`本地分支不存在,但远程分支存在: origin/${branch}`)
171
262
  );
172
263
  const deleteRemote = await select({
173
- message: `是否删除远程分支 origin/${branch}?`,
264
+ message: `确认删除远程分支 origin/${branch}?`,
174
265
  choices: [
175
266
  { name: "是", value: true },
176
267
  { name: "否", value: false },
@@ -193,6 +284,23 @@ export async function deleteBranch(branchArg?: string): Promise<void> {
193
284
  return;
194
285
  }
195
286
 
287
+ // 删除本地分支前确认
288
+ const confirmDelete = await select({
289
+ message: `确认删除分支 ${branch}?${
290
+ hasRemote ? " (本地+远程)" : " (仅本地)"
291
+ }`,
292
+ choices: [
293
+ { name: "是", value: true },
294
+ { name: "否", value: false },
295
+ ],
296
+ theme,
297
+ });
298
+
299
+ if (!confirmDelete) {
300
+ console.log(colors.yellow("已取消"));
301
+ return;
302
+ }
303
+
196
304
  const localSpinner = ora(`正在删除本地分支: ${branch}`).start();
197
305
  try {
198
306
  execSync(`git branch -D "${branch}"`, { stdio: "pipe" });
@@ -203,25 +311,12 @@ export async function deleteBranch(branchArg?: string): Promise<void> {
203
311
  }
204
312
 
205
313
  if (hasRemote) {
206
- divider();
207
-
208
- const deleteRemote = await select({
209
- message: `是否同时删除远程分支 origin/${branch}?`,
210
- choices: [
211
- { name: "", value: true },
212
- { name: "否", value: false },
213
- ],
214
- theme,
215
- });
216
-
217
- if (deleteRemote) {
218
- const remoteSpinner = ora(`正在删除远程分支: origin/${branch}`).start();
219
- try {
220
- execSync(`git push origin --delete "${branch}"`, { stdio: "pipe" });
221
- remoteSpinner.succeed(`远程分支已删除: origin/${branch}`);
222
- } catch {
223
- remoteSpinner.fail("远程分支删除失败");
224
- }
314
+ const remoteSpinner = ora(`正在删除远程分支: origin/${branch}`).start();
315
+ try {
316
+ execSync(`git push origin --delete "${branch}"`, { stdio: "pipe" });
317
+ remoteSpinner.succeed(`远程分支已删除: origin/${branch}`);
318
+ } catch {
319
+ remoteSpinner.fail("远程分支删除失败");
225
320
  }
226
321
  }
227
322
  }