@zjex/git-workflow 0.4.3 → 0.4.5

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.
@@ -66,12 +66,17 @@ gw ad HEAD~2 # 上上个 commit
66
66
  ### 步骤 3: 输入新日期
67
67
 
68
68
  ```
69
- 输入日期格式: YYYY-MM-DD (如: 2026-01-19)
69
+ 支持格式:
70
+ YYYY-MM-DD (如: 2026-01-19,默认 00:00:00)
71
+ YYYY-MM-DD HH:mm:ss (如: 2026-01-19 14:30:00)
70
72
 
71
- ? 输入新的日期: 2026-01-15
73
+ ? 输入新的日期: 2026-01-15 14:30:00
72
74
  ```
73
75
 
74
- **日期格式**: `YYYY-MM-DD`,时间默认为 `00:00:00`
76
+ **支持的日期格式**:
77
+
78
+ - `YYYY-MM-DD` - 只指定日期,时间默认为 `00:00:00`
79
+ - `YYYY-MM-DD HH:mm:ss` - 指定完整的日期和时间
75
80
 
76
81
  ### 步骤 4: 确认修改
77
82
 
@@ -79,7 +84,7 @@ gw ad HEAD~2 # 上上个 commit
79
84
  修改预览:
80
85
  Commit: a1b2c3d
81
86
  旧时间: 2026-01-19 14:30:00 +0800
82
- 新时间: 2026-01-15 00:00:00
87
+ 新时间: 2026-01-15 14:30:00
83
88
  修改类型: Author + Committer (两者都修改)
84
89
  ────────────────────────────────────────
85
90
 
@@ -164,9 +169,37 @@ git push --force
164
169
 
165
170
  ### 📅 时间格式
166
171
 
167
- - **输入格式**: `YYYY-MM-DD`(如 `2026-01-19`)
168
- - **存储格式**: `YYYY-MM-DD 00:00:00`(时间默认为 00:00:00)
169
- - **时区**: 使用系统当前时区
172
+ 支持两种输入格式:
173
+
174
+ 1. **仅日期**: `YYYY-MM-DD`(如 `2026-01-19`)
175
+ - 时间默认为 `00:00:00`
176
+ - 适合只关心日期的场景
177
+
178
+ 2. **完整时间**: `YYYY-MM-DD HH:mm:ss`(如 `2026-01-19 14:30:00`)
179
+ - 可以精确指定时分秒
180
+ - 适合需要精确时间的场景
181
+
182
+ **示例**:
183
+
184
+ ```bash
185
+ # 只指定日期
186
+ 输入: 2026-01-19
187
+ 结果: 2026-01-19 00:00:00
188
+
189
+ # 指定完整时间
190
+ 输入: 2026-01-19 14:30:00
191
+ 结果: 2026-01-19 14:30:00
192
+
193
+ # 指定午夜
194
+ 输入: 2026-01-19 00:00:00
195
+ 结果: 2026-01-19 00:00:00
196
+
197
+ # 指定一天结束
198
+ 输入: 2026-01-19 23:59:59
199
+ 结果: 2026-01-19 23:59:59
200
+ ```
201
+
202
+ **时区**: 使用系统当前时区
170
203
 
171
204
  ### 🔄 修改范围
172
205
 
@@ -187,11 +220,21 @@ $ gw ad HEAD
187
220
  ✔ 修改成功
188
221
  ```
189
222
 
190
- ### 示例 2: 修改指定 hash 的 commit
223
+ ### 示例 2: 修改为指定的完整时间
224
+
225
+ ```bash
226
+ $ gw ad HEAD
227
+ 输入新的日期: 2026-01-15 14:30:00
228
+ 确认修改? y
229
+
230
+ ✔ 修改成功
231
+ ```
232
+
233
+ ### 示例 3: 修改指定 hash 的 commit
191
234
 
192
235
  ```bash
193
236
  $ gw ad a1b2c3d
194
- 输入新的日期: 2026-01-10
237
+ 输入新的日期: 2026-01-10 09:00:00
195
238
  确认修改? y
196
239
 
197
240
  ⚠️ 警告: 修改非最新 commit 需要使用 rebase,可能会改变 commit hash
@@ -199,7 +242,7 @@ $ gw ad a1b2c3d
199
242
  ✔ 修改成功
200
243
  ```
201
244
 
202
- ### 示例 3: 交互式选择并修改
245
+ ### 示例 4: 交互式选择并修改
203
246
 
204
247
  ```bash
205
248
  $ gw ad
@@ -208,7 +251,7 @@ $ gw ad
208
251
  ❯ a1b2c3d ✨ feat: 添加登录功能 2026-01-19 14:30:00
209
252
  d4e5f6g 🐛 fix: 修复bug 2026-01-18 10:20:00
210
253
 
211
- 输入新的日期: 2026-01-15
254
+ 输入新的日期: 2026-01-15 16:45:30
212
255
  确认修改? y
213
256
 
214
257
  ✔ 修改成功
@@ -363,11 +406,30 @@ git am < patch.diff
363
406
 
364
407
  ### Q: 为什么时间是 00:00:00?
365
408
 
366
- A: 为了简化操作,只需要输入日期(YYYY-MM-DD),时间默认为 00:00:00。大多数情况下,我们只关心日期,不需要精确到秒。
409
+ A: 当你只输入日期(YYYY-MM-DD)时,时间会默认为 00:00:00。这是为了简化操作,大多数情况下我们只关心日期。
410
+
411
+ 如果需要指定精确时间,可以使用完整格式:
412
+
413
+ ```bash
414
+ # 只输入日期
415
+ 输入: 2026-01-15
416
+ 结果: 2026-01-15 00:00:00
417
+
418
+ # 输入完整时间
419
+ 输入: 2026-01-15 14:30:00
420
+ 结果: 2026-01-15 14:30:00
421
+ ```
367
422
 
368
423
  ### Q: 可以修改时间部分吗?
369
424
 
370
- A: 当前版本只支持修改日期,时间固定为 00:00:00。如需精确时间,可以手动使用 git 命令:
425
+ A: 可以!使用完整格式 `YYYY-MM-DD HH:mm:ss` 即可指定精确时间:
426
+
427
+ ```bash
428
+ $ gw ad
429
+ 输入新的日期: 2026-01-15 14:30:00
430
+ ```
431
+
432
+ 如果需要更灵活的控制,也可以手动使用 git 命令:
371
433
 
372
434
  ```bash
373
435
  # 修改最新 commit 的时间为指定的精确时间
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zjex/git-workflow",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "🚀 极简的 Git 工作流 CLI 工具,让分支管理和版本发布变得轻松愉快",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,14 +19,33 @@ function formatGitDate(date: Date): string {
19
19
 
20
20
  /**
21
21
  * 解析用户输入的日期
22
- * 支持格式:YYYY-MM-DD (默认 00:00:00)
22
+ * 支持格式:
23
+ * - YYYY-MM-DD (默认 00:00:00)
24
+ * - YYYY-MM-DD HH:mm:ss
23
25
  * @param input 用户输入
24
26
  * @returns Date 对象或 null
25
27
  */
26
28
  function parseDate(input: string): Date | null {
27
29
  const trimmed = input.trim();
28
- const dateMatch = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
29
30
 
31
+ // 尝试匹配完整格式: YYYY-MM-DD HH:mm:ss
32
+ const fullMatch = trimmed.match(
33
+ /^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})$/,
34
+ );
35
+ if (fullMatch) {
36
+ const [, year, month, day, hours, minutes, seconds] = fullMatch;
37
+ return new Date(
38
+ parseInt(year),
39
+ parseInt(month) - 1,
40
+ parseInt(day),
41
+ parseInt(hours),
42
+ parseInt(minutes),
43
+ parseInt(seconds),
44
+ );
45
+ }
46
+
47
+ // 尝试匹配简化格式: YYYY-MM-DD (默认 00:00:00)
48
+ const dateMatch = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
30
49
  if (dateMatch) {
31
50
  const [, year, month, day] = dateMatch;
32
51
  return new Date(
@@ -120,7 +139,6 @@ export async function amendDate(commitHash?: string): Promise<void> {
120
139
  value: c,
121
140
  description: c.date,
122
141
  })),
123
- pageSize: 15,
124
142
  theme,
125
143
  });
126
144
  }
@@ -133,7 +151,9 @@ export async function amendDate(commitHash?: string): Promise<void> {
133
151
  divider();
134
152
 
135
153
  // ========== 步骤 2: 输入新日期 ==========
136
- console.log(colors.dim("输入日期格式: YYYY-MM-DD (如: 2026-01-19)"));
154
+ console.log(colors.dim("支持格式:"));
155
+ console.log(colors.dim(" YYYY-MM-DD (如: 2026-01-19,默认 00:00:00)"));
156
+ console.log(colors.dim(" YYYY-MM-DD HH:mm:ss (如: 2026-01-19 14:30:00)"));
137
157
  console.log("");
138
158
 
139
159
  const dateInput = await input({
@@ -141,7 +161,7 @@ export async function amendDate(commitHash?: string): Promise<void> {
141
161
  validate: (value) => {
142
162
  const parsed = parseDate(value);
143
163
  if (!parsed) {
144
- return "日期格式不正确,请使用 YYYY-MM-DD 格式";
164
+ return "日期格式不正确,请使用 YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss 格式";
145
165
  }
146
166
  return true;
147
167
  },
@@ -79,7 +79,6 @@ export async function amend(commitHash?: string): Promise<void> {
79
79
  value: c,
80
80
  description: c.message,
81
81
  })),
82
- pageSize: 15,
83
82
  theme,
84
83
  });
85
84
  }
@@ -7,6 +7,7 @@ import {
7
7
  theme,
8
8
  exec,
9
9
  execOutput,
10
+ execWithSpinner,
10
11
  getMainBranch,
11
12
  divider,
12
13
  type BranchType,
@@ -35,8 +36,8 @@ export async function getBranchName(type: BranchType): Promise<string | null> {
35
36
  // 描述是否必填,默认非必填
36
37
  const requireDescription =
37
38
  type === "feature"
38
- ? config.featureRequireDescription ?? false
39
- : config.hotfixRequireDescription ?? false;
39
+ ? (config.featureRequireDescription ?? false)
40
+ : (config.hotfixRequireDescription ?? false);
40
41
  const descMessage = requireDescription
41
42
  ? "请输入描述:"
42
43
  : "请输入描述 (可跳过):";
@@ -62,7 +63,7 @@ export async function getBranchName(type: BranchType): Promise<string | null> {
62
63
 
63
64
  export async function createBranch(
64
65
  type: BranchType,
65
- baseBranchArg?: string | null
66
+ baseBranchArg?: string | null,
66
67
  ): Promise<void> {
67
68
  const config = getConfig();
68
69
 
@@ -142,12 +143,16 @@ export async function createBranch(
142
143
 
143
144
  if (shouldPush) {
144
145
  const pushSpinner = ora("正在推送到远程...").start();
145
- try {
146
- execSync(`git push -u origin "${branchName}"`, { stdio: "pipe" });
147
- pushSpinner.succeed(`已推送到远程: origin/${branchName}`);
148
- } catch {
149
- pushSpinner.warn(
150
- "远程推送失败,可稍后手动执行: git push -u origin " + branchName
146
+ const success = await execWithSpinner(
147
+ `git push -u origin "${branchName}"`,
148
+ pushSpinner,
149
+ `已推送到远程: origin/${branchName}`,
150
+ "远程推送失败",
151
+ );
152
+
153
+ if (!success) {
154
+ console.log(
155
+ colors.dim(` 可稍后手动执行: git push -u origin ${branchName}`),
151
156
  );
152
157
  }
153
158
  }
@@ -175,20 +180,23 @@ export async function deleteBranch(branchArg?: string): Promise<void> {
175
180
  if (!branch) {
176
181
  // 获取本地分支
177
182
  const localBranches = execOutput(
178
- "git for-each-ref --sort=-committerdate refs/heads/ --format='%(refname:short)'"
183
+ "git for-each-ref --sort=-committerdate refs/heads/ --format='%(refname:short)'",
179
184
  )
180
185
  .split("\n")
181
186
  .filter((b) => b && b !== currentBranch);
182
187
 
183
188
  // 获取远程分支(排除 HEAD 和已有本地分支的)
184
189
  const remoteBranches = execOutput(
185
- "git for-each-ref --sort=-committerdate refs/remotes/origin/ --format='%(refname:short)'"
190
+ "git for-each-ref --sort=-committerdate refs/remotes/origin/ --format='%(refname:short)'",
186
191
  )
187
192
  .split("\n")
188
193
  .map((b) => b.replace("origin/", ""))
189
194
  .filter(
190
195
  (b) =>
191
- b && b !== "HEAD" && b !== currentBranch && !localBranches.includes(b)
196
+ b &&
197
+ b !== "HEAD" &&
198
+ b !== currentBranch &&
199
+ !localBranches.includes(b),
192
200
  );
193
201
 
194
202
  if (localBranches.length === 0 && remoteBranches.length === 0) {
@@ -252,14 +260,12 @@ export async function deleteBranch(branchArg?: string): Promise<void> {
252
260
  }
253
261
 
254
262
  const spinner = ora(`正在删除远程分支: origin/${remoteBranch}`).start();
255
- try {
256
- execSync(`git push origin --delete "${remoteBranch}"`, {
257
- stdio: "pipe",
258
- });
259
- spinner.succeed(`远程分支已删除: origin/${remoteBranch}`);
260
- } catch {
261
- spinner.fail("远程分支删除失败");
262
- }
263
+ await execWithSpinner(
264
+ `git push origin --delete "${remoteBranch}"`,
265
+ spinner,
266
+ `远程分支已删除: origin/${remoteBranch}`,
267
+ "远程分支删除失败",
268
+ );
263
269
  return;
264
270
  }
265
271
  }
@@ -275,7 +281,7 @@ export async function deleteBranch(branchArg?: string): Promise<void> {
275
281
  if (!localExists) {
276
282
  if (hasRemote) {
277
283
  console.log(
278
- colors.yellow(`本地分支不存在,但远程分支存在: origin/${branch}`)
284
+ colors.yellow(`本地分支不存在,但远程分支存在: origin/${branch}`),
279
285
  );
280
286
  const deleteRemote = await select({
281
287
  message: `确认删除远程分支 origin/${branch}?`,
@@ -288,12 +294,12 @@ export async function deleteBranch(branchArg?: string): Promise<void> {
288
294
 
289
295
  if (deleteRemote) {
290
296
  const spinner = ora(`正在删除远程分支: origin/${branch}`).start();
291
- try {
292
- execSync(`git push origin --delete "${branch}"`, { stdio: "pipe" });
293
- spinner.succeed(`远程分支已删除: origin/${branch}`);
294
- } catch {
295
- spinner.fail("远程分支删除失败");
296
- }
297
+ await execWithSpinner(
298
+ `git push origin --delete "${branch}"`,
299
+ spinner,
300
+ `远程分支已删除: origin/${branch}`,
301
+ "远程分支删除失败",
302
+ );
297
303
  }
298
304
  } else {
299
305
  console.log(colors.red(`分支不存在: ${branch}`));
@@ -319,21 +325,24 @@ export async function deleteBranch(branchArg?: string): Promise<void> {
319
325
  }
320
326
 
321
327
  const localSpinner = ora(`正在删除本地分支: ${branch}`).start();
322
- try {
323
- execSync(`git branch -D "${branch}"`, { stdio: "pipe" });
324
- localSpinner.succeed(`本地分支已删除: ${branch}`);
325
- } catch {
326
- localSpinner.fail("本地分支删除失败");
328
+ const localSuccess = await execWithSpinner(
329
+ `git branch -D "${branch}"`,
330
+ localSpinner,
331
+ `本地分支已删除: ${branch}`,
332
+ "本地分支删除失败",
333
+ );
334
+
335
+ if (!localSuccess) {
327
336
  return;
328
337
  }
329
338
 
330
339
  if (hasRemote) {
331
340
  const remoteSpinner = ora(`正在删除远程分支: origin/${branch}`).start();
332
- try {
333
- execSync(`git push origin --delete "${branch}"`, { stdio: "pipe" });
334
- remoteSpinner.succeed(`远程分支已删除: origin/${branch}`);
335
- } catch {
336
- remoteSpinner.fail("远程分支删除失败");
337
- }
341
+ await execWithSpinner(
342
+ `git push origin --delete "${branch}"`,
343
+ remoteSpinner,
344
+ `远程分支已删除: origin/${branch}`,
345
+ "远程分支删除失败",
346
+ );
338
347
  }
339
348
  }
@@ -1,4 +1,4 @@
1
- import { execSync, spawn } from "child_process";
1
+ import { spawn } from "child_process";
2
2
  import { select, input } from "@inquirer/prompts";
3
3
  import ora from "ora";
4
4
  import boxen from "boxen";
@@ -7,6 +7,8 @@ import {
7
7
  theme,
8
8
  divider,
9
9
  execOutput,
10
+ execAsync,
11
+ execWithSpinner,
10
12
  type BranchType,
11
13
  } from "../utils.js";
12
14
  import { getBranchName } from "./branch.js";
@@ -40,7 +42,7 @@ function parseStashList(): StashEntry[] {
40
42
  message = message || "(no message)";
41
43
 
42
44
  const filesRaw = execOutput(
43
- `git stash show stash@{${index}} --name-only 2>/dev/null`
45
+ `git stash show stash@{${index}} --name-only 2>/dev/null`,
44
46
  );
45
47
  const files = filesRaw ? filesRaw.split("\n").filter(Boolean) : [];
46
48
 
@@ -145,10 +147,10 @@ async function showStashActions(entry: StashEntry): Promise<void> {
145
147
 
146
148
  switch (action) {
147
149
  case "apply":
148
- applyStash(entry.index, false);
150
+ await applyStash(entry.index, false);
149
151
  break;
150
152
  case "pop":
151
- applyStash(entry.index, true);
153
+ await applyStash(entry.index, true);
152
154
  break;
153
155
  case "branch":
154
156
  await createBranchFromStash(entry.index);
@@ -193,27 +195,34 @@ async function createStash(): Promise<void> {
193
195
  });
194
196
 
195
197
  const spinner = ora("创建 stash...").start();
196
- try {
197
- let cmd = "git stash push";
198
- if (includeUntracked) cmd += " -u";
199
- if (message) cmd += ` -m "${message.replace(/"/g, '\\"')}"`;
200
- execSync(cmd, { stdio: "pipe" });
201
- spinner.succeed("Stash 创建成功");
198
+ let cmd = "git stash push";
199
+ if (includeUntracked) cmd += " -u";
200
+ if (message) cmd += ` -m "${message.replace(/"/g, '\\"')}"`;
201
+
202
+ const success = await execWithSpinner(
203
+ cmd,
204
+ spinner,
205
+ "Stash 创建成功",
206
+ "Stash 创建失败",
207
+ );
208
+
209
+ if (success) {
202
210
  await stash();
203
- } catch {
204
- spinner.fail("Stash 创建失败");
205
211
  }
206
212
  }
207
213
 
208
- function applyStash(index: number, pop: boolean): void {
214
+ async function applyStash(index: number, pop: boolean): Promise<void> {
209
215
  const action = pop ? "pop" : "apply";
210
216
  const spinner = ora(`${pop ? "弹出" : "应用"} stash...`).start();
211
217
 
212
- try {
213
- execSync(`git stash ${action} stash@{${index}}`, { stdio: "pipe" });
214
- spinner.succeed(`Stash ${pop ? "已弹出" : "已应用"}`);
215
- } catch {
216
- spinner.fail("操作失败,可能存在冲突");
218
+ const success = await execWithSpinner(
219
+ `git stash ${action} stash@{${index}}`,
220
+ spinner,
221
+ `Stash ${pop ? "已弹出" : "已应用"}`,
222
+ "操作失败,可能存在冲突",
223
+ );
224
+
225
+ if (!success) {
217
226
  const status = execOutput("git status --porcelain");
218
227
  if (status.includes("UU") || status.includes("AA")) {
219
228
  console.log(colors.yellow("\n存在冲突,请手动解决后提交"));
@@ -225,7 +234,7 @@ async function showDiff(index: number): Promise<void> {
225
234
  try {
226
235
  // 获取差异内容(不使用颜色,我们自己格式化)
227
236
  const diffOutput = execOutput(
228
- `git stash show -p --no-color stash@{${index}}`
237
+ `git stash show -p --no-color stash@{${index}}`,
229
238
  );
230
239
 
231
240
  if (!diffOutput) {
@@ -424,14 +433,12 @@ async function createBranchFromStash(index: number): Promise<void> {
424
433
  if (!branchName) return;
425
434
 
426
435
  const spinner = ora(`创建分支 ${branchName}...`).start();
427
- try {
428
- execSync(`git stash branch "${branchName}" stash@{${index}}`, {
429
- stdio: "pipe",
430
- });
431
- spinner.succeed(`分支已创建: ${branchName} (stash 已自动弹出)`);
432
- } catch {
433
- spinner.fail("创建分支失败");
434
- }
436
+ await execWithSpinner(
437
+ `git stash branch "${branchName}" stash@{${index}}`,
438
+ spinner,
439
+ `分支已创建: ${branchName} (stash 已自动弹出)`,
440
+ "创建分支失败",
441
+ );
435
442
  }
436
443
 
437
444
  async function dropStash(index: number): Promise<void> {
@@ -450,10 +457,10 @@ async function dropStash(index: number): Promise<void> {
450
457
  }
451
458
 
452
459
  const spinner = ora("删除 stash...").start();
453
- try {
454
- execSync(`git stash drop stash@{${index}}`, { stdio: "pipe" });
455
- spinner.succeed("Stash 已删除");
456
- } catch {
457
- spinner.fail("删除失败");
458
- }
460
+ await execWithSpinner(
461
+ `git stash drop stash@{${index}}`,
462
+ spinner,
463
+ "Stash 已删除",
464
+ "删除失败",
465
+ );
459
466
  }