@zjex/git-workflow 0.2.20 → 0.2.21

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,169 @@
1
+ # 代码文档说明
2
+
3
+ ## 已添加详细注释的文件
4
+
5
+ ### ✅ src/commands/commit.ts
6
+
7
+ **功能**: 交互式提交命令,支持 AI 自动生成和手动编写
8
+
9
+ **注释内容**:
10
+
11
+ - 模块级注释:说明 Conventional Commits 和 Gitmoji 规范
12
+ - 函数注释:每个函数都有 JSDoc 注释
13
+ - 步骤标记:用 `==========` 分隔不同的处理步骤
14
+ - 行内注释:关键逻辑都有说明
15
+
16
+ **关键函数**:
17
+
18
+ - `commit()`: 主函数,处理整个提交流程
19
+ - `buildManualCommitMessage()`: 手动构建 commit message
20
+ - `parseGitStatus()`: 解析 git 状态
21
+ - `formatFileStatus()`: 格式化文件状态显示
22
+
23
+ ### ✅ src/index.ts
24
+
25
+ **功能**: 主入口文件,CLI 应用初始化和命令注册
26
+
27
+ **注释内容**:
28
+
29
+ - 模块级注释:说明文件职责
30
+ - 全局错误处理:Ctrl+C、未捕获异常、Promise 拒绝
31
+ - 版本信息:构建时注入
32
+ - 交互式主菜单:ASCII Logo 和命令选择
33
+ - CLI 命令注册:所有命令的注册和配置
34
+
35
+ ## 其他文件状态
36
+
37
+ ### ✅ 无错误的文件(已通过 TypeScript 检查)
38
+
39
+ - src/ai-service.ts
40
+ - src/config.ts
41
+ - src/utils.ts
42
+ - src/update-notifier.ts
43
+ - src/commands/branch.ts
44
+ - src/commands/help.ts
45
+ - src/commands/init.ts
46
+ - src/commands/release.ts
47
+ - src/commands/stash.ts
48
+ - src/commands/tag.ts
49
+ - src/commands/update.ts
50
+
51
+ ## 代码质量改进
52
+
53
+ ### 1. 类型安全
54
+
55
+ - 所有函数都有明确的返回类型
56
+ - 使用 TypeScript 的类型推导
57
+ - 接口定义清晰(如 `FileStatus`)
58
+
59
+ ### 2. 错误处理
60
+
61
+ - 全局错误捕获
62
+ - 优雅的 Ctrl+C 处理
63
+ - 详细的错误提示信息
64
+ - 提供手动执行命令
65
+
66
+ ### 3. 用户体验
67
+
68
+ - 提交前检查暂存文件
69
+ - 清晰的步骤分隔
70
+ - 彩色输出和 emoji
71
+ - 交互式选择界面
72
+
73
+ ### 4. 代码组织
74
+
75
+ - 模块化设计
76
+ - 单一职责原则
77
+ - 清晰的函数命名
78
+ - 详细的注释说明
79
+
80
+ ## 关键改进点
81
+
82
+ ### commit.ts
83
+
84
+ 1. **修复了 `message` 变量未初始化问题**
85
+
86
+ - 初始化为空字符串
87
+ - 确保所有代码路径都会赋值
88
+
89
+ 2. **添加提交前检查**
90
+
91
+ - 再次验证是否有暂存文件
92
+ - 避免空提交
93
+
94
+ 3. **改进错误提示**
95
+
96
+ - 显示完整错误信息
97
+ - 提供手动执行命令
98
+
99
+ 4. **修复 refactor 对齐问题**
100
+ - 针对 ♻️ emoji 特殊处理
101
+ - 动态调整间距
102
+
103
+ ### index.ts
104
+
105
+ 1. **完善错误处理**
106
+
107
+ - 捕获所有类型的错误
108
+ - 优雅退出
109
+
110
+ 2. **添加模块说明**
111
+ - 清晰的文件职责
112
+ - 详细的功能说明
113
+
114
+ ## 建议
115
+
116
+ 如果需要给其他文件添加详细注释,可以按照以下优先级:
117
+
118
+ 1. **高优先级**(核心业务逻辑):
119
+
120
+ - src/ai-service.ts - AI commit 生成
121
+ - src/config.ts - 配置管理
122
+ - src/commands/tag.ts - Tag 管理
123
+
124
+ 2. **中优先级**(常用功能):
125
+
126
+ - src/commands/branch.ts - 分支管理
127
+ - src/commands/stash.ts - Stash 管理
128
+ - src/update-notifier.ts - 更新检查
129
+
130
+ 3. **低优先级**(辅助功能):
131
+ - src/utils.ts - 工具函数
132
+ - src/commands/help.ts - 帮助信息
133
+ - src/commands/init.ts - 初始化配置
134
+
135
+ ## 注释规范
136
+
137
+ ### JSDoc 注释格式
138
+
139
+ ```typescript
140
+ /**
141
+ * 函数简短描述
142
+ *
143
+ * 详细说明(可选)
144
+ *
145
+ * @param paramName 参数说明
146
+ * @returns 返回值说明
147
+ */
148
+ ```
149
+
150
+ ### 行内注释
151
+
152
+ ```typescript
153
+ // 简短说明当前代码的作用
154
+ const result = doSomething();
155
+ ```
156
+
157
+ ### 步骤标记
158
+
159
+ ```typescript
160
+ // ========== 步骤 1: 处理输入 ==========
161
+ // ========== 步骤 2: 验证数据 ==========
162
+ ```
163
+
164
+ ## 总结
165
+
166
+ ✅ 核心文件已添加详细注释
167
+ ✅ 所有文件通过 TypeScript 检查
168
+ ✅ 代码质量和可维护性显著提升
169
+ ✅ 用户体验得到改善
package/dist/index.js CHANGED
@@ -978,38 +978,55 @@ async function updateTag() {
978
978
  divider();
979
979
  const choices = tags.map((tag) => ({ name: tag, value: tag }));
980
980
  choices.push({ name: "\u53D6\u6D88", value: "__cancel__" });
981
- const tagToUpdate = await select2({
982
- message: "\u9009\u62E9\u8981\u4FEE\u6539\u7684 tag:",
981
+ const oldTag = await select2({
982
+ message: "\u9009\u62E9\u8981\u91CD\u547D\u540D\u7684 tag:",
983
983
  choices,
984
984
  theme
985
985
  });
986
- if (tagToUpdate === "__cancel__") {
986
+ if (oldTag === "__cancel__") {
987
987
  console.log(colors.yellow("\u5DF2\u53D6\u6D88"));
988
988
  return;
989
989
  }
990
- const newMessage = await input2({
991
- message: "\u8F93\u5165\u65B0\u7684 tag \u6D88\u606F:",
992
- default: `Release ${tagToUpdate}`,
990
+ console.log("");
991
+ console.log(colors.dim(`\u5F53\u524D tag: ${oldTag}`));
992
+ console.log("");
993
+ const newTag = await input2({
994
+ message: "\u8F93\u5165\u65B0\u7684 tag \u540D\u79F0:",
995
+ default: oldTag,
993
996
  theme
994
997
  });
995
- if (!newMessage) {
998
+ if (!newTag || newTag === oldTag) {
996
999
  console.log(colors.yellow("\u5DF2\u53D6\u6D88"));
997
1000
  return;
998
1001
  }
1002
+ const existingTags = execOutput("git tag -l").split("\n").filter(Boolean);
1003
+ if (existingTags.includes(newTag)) {
1004
+ console.log(colors.red(`Tag ${newTag} \u5DF2\u5B58\u5728\uFF0C\u65E0\u6CD5\u91CD\u547D\u540D`));
1005
+ return;
1006
+ }
999
1007
  divider();
1000
- const spinner = ora2(`\u6B63\u5728\u66F4\u65B0 tag: ${tagToUpdate}`).start();
1008
+ const spinner = ora2(`\u6B63\u5728\u91CD\u547D\u540D tag: ${oldTag} \u2192 ${newTag}`).start();
1001
1009
  try {
1002
- execSync3(`git tag -d "${tagToUpdate}"`, { stdio: "pipe" });
1003
- execSync3(`git tag -a "${tagToUpdate}" -m "${newMessage}"`, {
1004
- stdio: "pipe"
1005
- });
1006
- spinner.succeed(`Tag \u5DF2\u66F4\u65B0: ${tagToUpdate}`);
1007
- } catch {
1008
- spinner.fail("tag \u66F4\u65B0\u5931\u8D25");
1010
+ const commit2 = execOutput(`git rev-list -n 1 "${oldTag}"`).trim();
1011
+ const message = execOutput(
1012
+ `git tag -l --format='%(contents)' "${oldTag}"`
1013
+ ).trim();
1014
+ if (message) {
1015
+ execSync3(`git tag -a "${newTag}" "${commit2}" -m "${message}"`, {
1016
+ stdio: "pipe"
1017
+ });
1018
+ } else {
1019
+ execSync3(`git tag "${newTag}" "${commit2}"`, { stdio: "pipe" });
1020
+ }
1021
+ execSync3(`git tag -d "${oldTag}"`, { stdio: "pipe" });
1022
+ spinner.succeed(`Tag \u5DF2\u91CD\u547D\u540D: ${oldTag} \u2192 ${newTag}`);
1023
+ } catch (error) {
1024
+ spinner.fail("tag \u91CD\u547D\u540D\u5931\u8D25");
1025
+ console.log(colors.red(String(error)));
1009
1026
  return;
1010
1027
  }
1011
1028
  const pushRemote = await select2({
1012
- message: "\u662F\u5426\u63A8\u9001\u5230\u8FDC\u7A0B\uFF08\u4F1A\u5F3A\u5236\u8986\u76D6\uFF09?",
1029
+ message: "\u662F\u5426\u540C\u6B65\u5230\u8FDC\u7A0B?",
1013
1030
  choices: [
1014
1031
  { name: "\u662F", value: true },
1015
1032
  { name: "\u5426", value: false }
@@ -1017,13 +1034,16 @@ async function updateTag() {
1017
1034
  theme
1018
1035
  });
1019
1036
  if (pushRemote) {
1020
- const pushSpinner = ora2("\u6B63\u5728\u63A8\u9001\u5230\u8FDC\u7A0B...").start();
1037
+ const pushSpinner = ora2("\u6B63\u5728\u540C\u6B65\u5230\u8FDC\u7A0B...").start();
1021
1038
  try {
1022
- execSync3(`git push origin "${tagToUpdate}" --force`, { stdio: "pipe" });
1023
- pushSpinner.succeed(`Tag \u5DF2\u63A8\u9001: ${tagToUpdate}`);
1039
+ execSync3(`git push origin "${newTag}"`, { stdio: "pipe" });
1040
+ execSync3(`git push origin --delete "${oldTag}"`, { stdio: "pipe" });
1041
+ pushSpinner.succeed(`\u8FDC\u7A0B tag \u5DF2\u540C\u6B65: ${oldTag} \u2192 ${newTag}`);
1024
1042
  } catch {
1025
1043
  pushSpinner.warn(
1026
- `\u8FDC\u7A0B\u63A8\u9001\u5931\u8D25\uFF0C\u53EF\u7A0D\u540E\u624B\u52A8\u6267\u884C: git push origin ${tagToUpdate} --force`
1044
+ `\u8FDC\u7A0B\u540C\u6B65\u5931\u8D25\uFF0C\u53EF\u7A0D\u540E\u624B\u52A8\u6267\u884C:
1045
+ git push origin ${newTag}
1046
+ git push origin --delete ${oldTag}`
1027
1047
  );
1028
1048
  }
1029
1049
  }
@@ -1882,8 +1902,7 @@ var DEFAULT_COMMIT_TYPES = [
1882
1902
  { type: "test", emoji: "\u2705", description: "\u6D4B\u8BD5\u76F8\u5173" },
1883
1903
  { type: "build", emoji: "\u{1F4E6}", description: "\u6784\u5EFA/\u4F9D\u8D56\u76F8\u5173" },
1884
1904
  { type: "ci", emoji: "\u{1F477}", description: "CI/CD \u76F8\u5173" },
1885
- { type: "chore", emoji: "\u{1F527}", description: "\u5176\u4ED6\u6742\u9879" },
1886
- { type: "revert", emoji: "\u23EA", description: "\u56DE\u9000\u63D0\u4EA4" }
1905
+ { type: "chore", emoji: "\u{1F527}", description: "\u5176\u4ED6\u6742\u9879" }
1887
1906
  ];
1888
1907
  function getCommitTypes(config2) {
1889
1908
  const customEmojis = config2.commitEmojis || {};
@@ -1915,11 +1934,17 @@ function parseGitStatus() {
1915
1934
  function formatFileStatus(status) {
1916
1935
  const statusMap = {
1917
1936
  M: colors.yellow("M"),
1937
+ // 修改
1918
1938
  A: colors.green("A"),
1939
+ // 新增
1919
1940
  D: colors.red("D"),
1941
+ // 删除
1920
1942
  R: colors.yellow("R"),
1943
+ // 重命名
1921
1944
  C: colors.yellow("C"),
1945
+ // 复制
1922
1946
  "?": colors.green("?")
1947
+ // 未跟踪
1923
1948
  };
1924
1949
  return statusMap[status] || status;
1925
1950
  }
@@ -1994,7 +2019,7 @@ async function commit() {
1994
2019
  theme
1995
2020
  });
1996
2021
  }
1997
- let message;
2022
+ let message = "";
1998
2023
  if (commitMode === "ai") {
1999
2024
  const spinner2 = ora4("AI \u6B63\u5728\u5206\u6790\u4EE3\u7801\u53D8\u66F4...").start();
2000
2025
  try {
@@ -2049,6 +2074,17 @@ async function commit() {
2049
2074
  }
2050
2075
  const spinner = ora4("\u6B63\u5728\u63D0\u4EA4...").start();
2051
2076
  try {
2077
+ const finalStatus = parseGitStatus();
2078
+ if (finalStatus.staged.length === 0) {
2079
+ spinner.fail("\u6CA1\u6709\u6682\u5B58\u7684\u6587\u4EF6\u53EF\u4EE5\u63D0\u4EA4");
2080
+ console.log("");
2081
+ console.log(colors.yellow("\u8BF7\u5148\u6682\u5B58\u6587\u4EF6:"));
2082
+ console.log(colors.cyan(" git add <file>"));
2083
+ console.log(colors.dim(" \u6216"));
2084
+ console.log(colors.cyan(" git add -A"));
2085
+ console.log("");
2086
+ return;
2087
+ }
2052
2088
  const escapedMessage = message.replace(/"/g, '\\"');
2053
2089
  execSync5(`git commit -m "${escapedMessage}"`, { stdio: "pipe" });
2054
2090
  spinner.succeed("\u63D0\u4EA4\u6210\u529F");
@@ -2056,19 +2092,31 @@ async function commit() {
2056
2092
  console.log(colors.dim(`commit: ${commitHash}`));
2057
2093
  } catch (error) {
2058
2094
  spinner.fail("\u63D0\u4EA4\u5931\u8D25");
2095
+ console.log("");
2059
2096
  if (error instanceof Error) {
2060
- console.log(colors.red(error.message));
2097
+ console.log(colors.red("\u9519\u8BEF\u4FE1\u606F:"));
2098
+ console.log(colors.dim(` ${error.message}`));
2061
2099
  }
2100
+ console.log("");
2101
+ console.log(colors.yellow("\u4F60\u53EF\u4EE5\u624B\u52A8\u6267\u884C\u4EE5\u4E0B\u547D\u4EE4:"));
2102
+ console.log(colors.cyan(` git commit -m "${message}"`));
2103
+ console.log("");
2062
2104
  }
2063
2105
  }
2064
2106
  async function buildManualCommitMessage(config2) {
2065
2107
  const commitTypes = getCommitTypes(config2);
2066
2108
  const typeChoice = await select6({
2067
2109
  message: "\u9009\u62E9\u63D0\u4EA4\u7C7B\u578B:",
2068
- choices: commitTypes.map((t) => ({
2069
- name: `${t.emoji} ${t.type.padEnd(10)} ${colors.dim(t.description)}`,
2070
- value: t
2071
- })),
2110
+ choices: commitTypes.map((t) => {
2111
+ const typeText = t.type.padEnd(10);
2112
+ const spacing = t.type === "refactor" ? " " : " ";
2113
+ return {
2114
+ name: `${t.emoji}${spacing}${typeText} ${colors.dim(t.description)}`,
2115
+ value: t
2116
+ };
2117
+ }),
2118
+ pageSize: commitTypes.length,
2119
+ // 显示所有选项,不滚动
2072
2120
  theme
2073
2121
  });
2074
2122
  const scope = await input5({
@@ -2161,7 +2209,7 @@ Tag \u547D\u4EE4:
2161
2209
  gw tag:delete \u5220\u9664 tag
2162
2210
  gw td \u540C\u4E0A (\u522B\u540D)
2163
2211
 
2164
- gw tag:update \u4FEE\u6539 tag \u6D88\u606F
2212
+ gw tag:update \u91CD\u547D\u540D tag
2165
2213
  gw tu \u540C\u4E0A (\u522B\u540D)
2166
2214
 
2167
2215
  \u53D1\u5E03\u547D\u4EE4:
@@ -2365,7 +2413,7 @@ process.on("SIGTERM", () => {
2365
2413
  console.log("");
2366
2414
  process.exit(0);
2367
2415
  });
2368
- var version = true ? "0.2.20" : "0.0.0-dev";
2416
+ var version = true ? "0.2.21" : "0.0.0-dev";
2369
2417
  async function mainMenu() {
2370
2418
  console.log(
2371
2419
  colors.green(`
@@ -2407,7 +2455,7 @@ async function mainMenu() {
2407
2455
  value: "tag-delete"
2408
2456
  },
2409
2457
  {
2410
- name: `[7] \u270F\uFE0F \u4FEE\u6539 tag ${colors.dim("gw tu")}`,
2458
+ name: `[7] \u270F\uFE0F \u91CD\u547D\u540D tag ${colors.dim("gw tu")}`,
2411
2459
  value: "tag-update"
2412
2460
  },
2413
2461
  {
@@ -2517,7 +2565,7 @@ cli.command("tag:delete", "\u5220\u9664 tag").alias("td").action(async () => {
2517
2565
  checkGitRepo();
2518
2566
  return deleteTag();
2519
2567
  });
2520
- cli.command("tag:update", "\u4FEE\u6539 tag \u6D88\u606F").alias("tu").action(async () => {
2568
+ cli.command("tag:update", "\u91CD\u547D\u540D tag").alias("tu").action(async () => {
2521
2569
  await checkForUpdates(version, "@zjex/git-workflow");
2522
2570
  checkGitRepo();
2523
2571
  return updateTag();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zjex/git-workflow",
3
- "version": "0.2.20",
3
+ "version": "0.2.21",
4
4
  "description": "🚀 极简的 Git 工作流 CLI 工具,让分支管理和版本发布变得轻松愉快",
5
5
  "type": "module",
6
6
  "bin": {
@@ -5,7 +5,11 @@ import { colors, theme, execOutput, divider } from "../utils.js";
5
5
  import { getConfig } from "../config.js";
6
6
  import { generateAICommitMessage, isAICommitAvailable } from "../ai-service.js";
7
7
 
8
- // Conventional Commits 类型 + Gitmoji
8
+ /**
9
+ * Conventional Commits 类型定义 + Gitmoji
10
+ * 遵循 https://www.conventionalcommits.org/ 规范
11
+ * 使用 https://gitmoji.dev/ emoji
12
+ */
9
13
  const DEFAULT_COMMIT_TYPES = [
10
14
  { type: "feat", emoji: "✨", description: "新功能" },
11
15
  { type: "fix", emoji: "🐛", description: "修复 Bug" },
@@ -17,11 +21,15 @@ const DEFAULT_COMMIT_TYPES = [
17
21
  { type: "build", emoji: "📦", description: "构建/依赖相关" },
18
22
  { type: "ci", emoji: "👷", description: "CI/CD 相关" },
19
23
  { type: "chore", emoji: "🔧", description: "其他杂项" },
20
- { type: "revert", emoji: "⏪", description: "回退提交" },
21
24
  ] as const;
22
25
 
23
26
  type CommitType = (typeof DEFAULT_COMMIT_TYPES)[number]["type"];
24
27
 
28
+ /**
29
+ * 获取提交类型列表(支持自定义 emoji)
30
+ * @param config 用户配置
31
+ * @returns 提交类型列表
32
+ */
25
33
  function getCommitTypes(config: ReturnType<typeof getConfig>) {
26
34
  const customEmojis = config.commitEmojis || {};
27
35
  return DEFAULT_COMMIT_TYPES.map((item) => ({
@@ -30,11 +38,18 @@ function getCommitTypes(config: ReturnType<typeof getConfig>) {
30
38
  }));
31
39
  }
32
40
 
41
+ /**
42
+ * 文件状态接口
43
+ */
33
44
  interface FileStatus {
34
- status: string;
35
- file: string;
45
+ status: string; // M=修改, A=新增, D=删除, ?=未跟踪
46
+ file: string; // 文件路径
36
47
  }
37
48
 
49
+ /**
50
+ * 解析 git status 输出
51
+ * @returns 已暂存和未暂存的文件列表
52
+ */
38
53
  function parseGitStatus(): { staged: FileStatus[]; unstaged: FileStatus[] } {
39
54
  const output = execOutput("git status --porcelain");
40
55
  if (!output) return { staged: [], unstaged: [] };
@@ -44,9 +59,9 @@ function parseGitStatus(): { staged: FileStatus[]; unstaged: FileStatus[] } {
44
59
 
45
60
  for (const line of output.split("\n")) {
46
61
  if (!line) continue;
47
- const indexStatus = line[0];
48
- const workTreeStatus = line[1];
49
- const file = line.slice(3);
62
+ const indexStatus = line[0]; // 暂存区状态
63
+ const workTreeStatus = line[1]; // 工作区状态
64
+ const file = line.slice(3); // 文件路径
50
65
 
51
66
  // 已暂存的更改 (index 有状态)
52
67
  if (indexStatus !== " " && indexStatus !== "?") {
@@ -63,23 +78,33 @@ function parseGitStatus(): { staged: FileStatus[]; unstaged: FileStatus[] } {
63
78
  return { staged, unstaged };
64
79
  }
65
80
 
81
+ /**
82
+ * 格式化文件状态显示(带颜色)
83
+ * @param status 文件状态
84
+ * @returns 带颜色的状态字符串
85
+ */
66
86
  function formatFileStatus(status: string): string {
67
87
  const statusMap: Record<string, string> = {
68
- M: colors.yellow("M"),
69
- A: colors.green("A"),
70
- D: colors.red("D"),
71
- R: colors.yellow("R"),
72
- C: colors.yellow("C"),
73
- "?": colors.green("?"),
88
+ M: colors.yellow("M"), // 修改
89
+ A: colors.green("A"), // 新增
90
+ D: colors.red("D"), // 删除
91
+ R: colors.yellow("R"), // 重命名
92
+ C: colors.yellow("C"), // 复制
93
+ "?": colors.green("?"), // 未跟踪
74
94
  };
75
95
  return statusMap[status] || status;
76
96
  }
77
97
 
98
+ /**
99
+ * 交互式提交命令
100
+ * 支持 AI 自动生成和手动编写两种模式
101
+ * 遵循 Conventional Commits 规范
102
+ */
78
103
  export async function commit(): Promise<void> {
79
104
  const config = getConfig();
80
105
  let { staged, unstaged } = parseGitStatus();
81
106
 
82
- // 如果有未暂存的更改,根据配置决定是否自动暂存
107
+ // ========== 步骤 1: 处理未暂存的文件 ==========
83
108
  if (unstaged.length > 0) {
84
109
  const autoStage = config.autoStage ?? true;
85
110
 
@@ -131,7 +156,7 @@ export async function commit(): Promise<void> {
131
156
  }
132
157
  }
133
158
 
134
- // 没有暂存的更改
159
+ // ========== 步骤 2: 检查是否有文件可提交 ==========
135
160
  if (staged.length === 0) {
136
161
  console.log(colors.yellow("工作区干净,没有需要提交的更改"));
137
162
  return;
@@ -144,7 +169,7 @@ export async function commit(): Promise<void> {
144
169
  }
145
170
  divider();
146
171
 
147
- // 询问用户选择手动还是 AI 生成
172
+ // ========== 步骤 3: 选择提交方式(AI 或手动)==========
148
173
  const aiAvailable = isAICommitAvailable(config);
149
174
  let commitMode: "ai" | "manual" = "manual";
150
175
 
@@ -167,10 +192,12 @@ export async function commit(): Promise<void> {
167
192
  });
168
193
  }
169
194
 
170
- let message: string;
195
+ // 初始化 commit message 变量
196
+ let message: string = "";
171
197
 
198
+ // ========== 步骤 4: 生成 commit message ==========
199
+ // AI 生成模式
172
200
  if (commitMode === "ai") {
173
- // AI 生成模式
174
201
  const spinner = ora("AI 正在分析代码变更...").start();
175
202
 
176
203
  try {
@@ -208,11 +235,12 @@ export async function commit(): Promise<void> {
208
235
  }
209
236
  }
210
237
 
238
+ // 手动输入模式
211
239
  if (commitMode === "manual") {
212
- // 手动输入模式(原有逻辑)
213
240
  message = await buildManualCommitMessage(config);
214
241
  }
215
242
 
243
+ // ========== 步骤 5: 预览并确认提交 ==========
216
244
  divider();
217
245
  console.log("提交信息预览:");
218
246
  console.log(colors.green(message));
@@ -232,9 +260,23 @@ export async function commit(): Promise<void> {
232
260
  return;
233
261
  }
234
262
 
263
+ // ========== 步骤 6: 执行提交 ==========
235
264
  const spinner = ora("正在提交...").start();
236
265
 
237
266
  try {
267
+ // 提交前再次检查是否有暂存的文件
268
+ const finalStatus = parseGitStatus();
269
+ if (finalStatus.staged.length === 0) {
270
+ spinner.fail("没有暂存的文件可以提交");
271
+ console.log("");
272
+ console.log(colors.yellow("请先暂存文件:"));
273
+ console.log(colors.cyan(" git add <file>"));
274
+ console.log(colors.dim(" 或"));
275
+ console.log(colors.cyan(" git add -A"));
276
+ console.log("");
277
+ return;
278
+ }
279
+
238
280
  // 使用 -m 参数,需要转义引号
239
281
  const escapedMessage = message.replace(/"/g, '\\"');
240
282
  execSync(`git commit -m "${escapedMessage}"`, { stdio: "pipe" });
@@ -245,14 +287,26 @@ export async function commit(): Promise<void> {
245
287
  console.log(colors.dim(`commit: ${commitHash}`));
246
288
  } catch (error) {
247
289
  spinner.fail("提交失败");
290
+ console.log("");
291
+
292
+ // 显示详细错误信息
248
293
  if (error instanceof Error) {
249
- console.log(colors.red(error.message));
294
+ console.log(colors.red("错误信息:"));
295
+ console.log(colors.dim(` ${error.message}`));
250
296
  }
297
+
298
+ console.log("");
299
+ console.log(colors.yellow("你可以手动执行以下命令:"));
300
+ console.log(colors.cyan(` git commit -m "${message}"`));
301
+ console.log("");
251
302
  }
252
303
  }
253
304
 
254
305
  /**
255
306
  * 手动构建 commit message
307
+ * 通过交互式问答收集信息,构建符合 Conventional Commits 规范的提交信息
308
+ * @param config 用户配置
309
+ * @returns 完整的 commit message
256
310
  */
257
311
  async function buildManualCommitMessage(
258
312
  config: ReturnType<typeof getConfig>
@@ -260,23 +314,30 @@ async function buildManualCommitMessage(
260
314
  // 获取提交类型(支持自定义 emoji)
261
315
  const commitTypes = getCommitTypes(config);
262
316
 
263
- // 选择提交类型
317
+ // ========== 1. 选择提交类型 ==========
264
318
  const typeChoice = await select({
265
319
  message: "选择提交类型:",
266
- choices: commitTypes.map((t) => ({
267
- name: `${t.emoji} ${t.type.padEnd(10)} ${colors.dim(t.description)}`,
268
- value: t,
269
- })),
320
+ choices: commitTypes.map((t) => {
321
+ // 使用固定宽度格式化,不依赖 emoji 宽度
322
+ const typeText = t.type.padEnd(10);
323
+ // 针对 refactor 特殊处理,因为 ♻️ emoji 在不同终端宽度不一致
324
+ const spacing = t.type === "refactor" ? " " : " ";
325
+ return {
326
+ name: `${t.emoji}${spacing}${typeText} ${colors.dim(t.description)}`,
327
+ value: t,
328
+ };
329
+ }),
330
+ pageSize: commitTypes.length, // 显示所有选项,不滚动
270
331
  theme,
271
332
  });
272
333
 
273
- // 输入 scope (可选)
334
+ // ========== 2. 输入 scope (可选) ==========
274
335
  const scope = await input({
275
336
  message: "输入影响范围 scope (可跳过):",
276
337
  theme,
277
338
  });
278
339
 
279
- // 输入简短描述
340
+ // ========== 3. 输入简短描述 (必填) ==========
280
341
  const subject = await input({
281
342
  message: "输入简短描述:",
282
343
  validate: (value) => {
@@ -287,13 +348,13 @@ async function buildManualCommitMessage(
287
348
  theme,
288
349
  });
289
350
 
290
- // 输入详细描述 (可选)
351
+ // ========== 4. 输入详细描述 (可选) ==========
291
352
  const body = await input({
292
353
  message: "输入详细描述 (可跳过):",
293
354
  theme,
294
355
  });
295
356
 
296
- // 是否有破坏性变更
357
+ // ========== 5. 是否有破坏性变更 ==========
297
358
  const hasBreaking = await select({
298
359
  message: "是否包含破坏性变更 (BREAKING CHANGE)?",
299
360
  choices: [
@@ -312,13 +373,13 @@ async function buildManualCommitMessage(
312
373
  });
313
374
  }
314
375
 
315
- // 关联 Issue (可选)
376
+ // ========== 6. 关联 Issue (可选) ==========
316
377
  const issues = await input({
317
378
  message: "关联 Issue (如 #123, 可跳过):",
318
379
  theme,
319
380
  });
320
381
 
321
- // 构建 commit message
382
+ // ========== 7. 构建 commit message ==========
322
383
  const { type, emoji } = typeChoice;
323
384
  const scopePart = scope ? `(${scope})` : "";
324
385
  const breakingMark = hasBreaking ? "!" : "";
@@ -330,7 +391,7 @@ async function buildManualCommitMessage(
330
391
  // Header: [emoji] type(scope)!: subject
331
392
  let message = `${emojiPrefix}${type}${scopePart}${breakingMark}: ${subject}`;
332
393
 
333
- // Body
394
+ // Body (可选)
334
395
  if (body || hasBreaking || issues) {
335
396
  message += "\n";
336
397
 
@@ -25,7 +25,7 @@ Tag 命令:
25
25
  gw tag:delete 删除 tag
26
26
  gw td 同上 (别名)
27
27
 
28
- gw tag:update 修改 tag 消息
28
+ gw tag:update 重命名 tag
29
29
  gw tu 同上 (别名)
30
30
 
31
31
  发布命令:
@@ -417,7 +417,7 @@ export async function deleteTag(): Promise<void> {
417
417
  }
418
418
 
419
419
  /**
420
- * 修改 tag(重新打标签)
420
+ * 修改 tag 名称(重命名 tag)
421
421
  */
422
422
  export async function updateTag(): Promise<void> {
423
423
  const fetchSpinner = ora("正在获取 tags...").start();
@@ -438,47 +438,71 @@ export async function updateTag(): Promise<void> {
438
438
  const choices = tags.map((tag) => ({ name: tag, value: tag }));
439
439
  choices.push({ name: "取消", value: "__cancel__" });
440
440
 
441
- const tagToUpdate = await select({
442
- message: "选择要修改的 tag:",
441
+ const oldTag = await select({
442
+ message: "选择要重命名的 tag:",
443
443
  choices,
444
444
  theme,
445
445
  });
446
446
 
447
- if (tagToUpdate === "__cancel__") {
447
+ if (oldTag === "__cancel__") {
448
448
  console.log(colors.yellow("已取消"));
449
449
  return;
450
450
  }
451
451
 
452
- const newMessage = await input({
453
- message: "输入新的 tag 消息:",
454
- default: `Release ${tagToUpdate}`,
452
+ console.log("");
453
+ console.log(colors.dim(`当前 tag: ${oldTag}`));
454
+ console.log("");
455
+
456
+ const newTag = await input({
457
+ message: "输入新的 tag 名称:",
458
+ default: oldTag,
455
459
  theme,
456
460
  });
457
461
 
458
- if (!newMessage) {
462
+ if (!newTag || newTag === oldTag) {
459
463
  console.log(colors.yellow("已取消"));
460
464
  return;
461
465
  }
462
466
 
467
+ // 检查新 tag 是否已存在
468
+ const existingTags = execOutput("git tag -l").split("\n").filter(Boolean);
469
+ if (existingTags.includes(newTag)) {
470
+ console.log(colors.red(`Tag ${newTag} 已存在,无法重命名`));
471
+ return;
472
+ }
473
+
463
474
  divider();
464
475
 
465
- const spinner = ora(`正在更新 tag: ${tagToUpdate}`).start();
476
+ const spinner = ora(`正在重命名 tag: ${oldTag} → ${newTag}`).start();
466
477
 
467
478
  try {
479
+ // 获取旧 tag 的 commit 和消息
480
+ const commit = execOutput(`git rev-list -n 1 "${oldTag}"`).trim();
481
+ const message = execOutput(
482
+ `git tag -l --format='%(contents)' "${oldTag}"`
483
+ ).trim();
484
+
485
+ // 创建新 tag(指向同一个 commit)
486
+ if (message) {
487
+ execSync(`git tag -a "${newTag}" "${commit}" -m "${message}"`, {
488
+ stdio: "pipe",
489
+ });
490
+ } else {
491
+ execSync(`git tag "${newTag}" "${commit}"`, { stdio: "pipe" });
492
+ }
493
+
468
494
  // 删除旧 tag
469
- execSync(`git tag -d "${tagToUpdate}"`, { stdio: "pipe" });
470
- // 创建新 tag(在同一个 commit 上)
471
- execSync(`git tag -a "${tagToUpdate}" -m "${newMessage}"`, {
472
- stdio: "pipe",
473
- });
474
- spinner.succeed(`Tag 已更新: ${tagToUpdate}`);
475
- } catch {
476
- spinner.fail("tag 更新失败");
495
+ execSync(`git tag -d "${oldTag}"`, { stdio: "pipe" });
496
+
497
+ spinner.succeed(`Tag 已重命名: ${oldTag} ${newTag}`);
498
+ } catch (error) {
499
+ spinner.fail("tag 重命名失败");
500
+ console.log(colors.red(String(error)));
477
501
  return;
478
502
  }
479
503
 
480
504
  const pushRemote = await select({
481
- message: "是否推送到远程(会强制覆盖)?",
505
+ message: "是否同步到远程?",
482
506
  choices: [
483
507
  { name: "是", value: true },
484
508
  { name: "否", value: false },
@@ -487,13 +511,16 @@ export async function updateTag(): Promise<void> {
487
511
  });
488
512
 
489
513
  if (pushRemote) {
490
- const pushSpinner = ora("正在推送到远程...").start();
514
+ const pushSpinner = ora("正在同步到远程...").start();
491
515
  try {
492
- execSync(`git push origin "${tagToUpdate}" --force`, { stdio: "pipe" });
493
- pushSpinner.succeed(`Tag 已推送: ${tagToUpdate}`);
516
+ // 推送新 tag
517
+ execSync(`git push origin "${newTag}"`, { stdio: "pipe" });
518
+ // 删除远程旧 tag
519
+ execSync(`git push origin --delete "${oldTag}"`, { stdio: "pipe" });
520
+ pushSpinner.succeed(`远程 tag 已同步: ${oldTag} → ${newTag}`);
494
521
  } catch {
495
522
  pushSpinner.warn(
496
- `远程推送失败,可稍后手动执行: git push origin ${tagToUpdate} --force`
523
+ `远程同步失败,可稍后手动执行:\n git push origin ${newTag}\n git push origin --delete ${oldTag}`
497
524
  );
498
525
  }
499
526
  }
package/src/index.ts CHANGED
@@ -1,3 +1,13 @@
1
+ /**
2
+ * @zjex/git-workflow - Git 工作流 CLI 工具
3
+ *
4
+ * 主入口文件,负责:
5
+ * 1. 初始化 CLI 应用
6
+ * 2. 注册所有命令
7
+ * 3. 处理全局错误和信号
8
+ * 4. 显示交互式主菜单
9
+ */
10
+
1
11
  // @ts-nocheck shebang handled by tsup banner
2
12
 
3
13
  import { cac } from "cac";
@@ -14,7 +24,12 @@ import { showHelp } from "./commands/help.js";
14
24
  import { checkForUpdates } from "./update-notifier.js";
15
25
  import { update } from "./commands/update.js";
16
26
 
17
- // 捕获 Ctrl+C 退出,静默处理
27
+ // ========== 全局错误处理 ==========
28
+
29
+ /**
30
+ * 捕获未捕获的异常
31
+ * 主要用于优雅处理用户按 Ctrl+C 退出的情况
32
+ */
18
33
  process.on("uncaughtException", (err) => {
19
34
  if (err instanceof ExitPromptError) {
20
35
  console.log(""); // 输出空行,让界面更整洁
@@ -24,7 +39,9 @@ process.on("uncaughtException", (err) => {
24
39
  process.exit(1);
25
40
  });
26
41
 
27
- // 捕获未处理的 Promise 拒绝
42
+ /**
43
+ * 捕获未处理的 Promise 拒绝
44
+ */
28
45
  process.on("unhandledRejection", (reason) => {
29
46
  if (reason instanceof ExitPromptError) {
30
47
  console.log("");
@@ -34,29 +51,50 @@ process.on("unhandledRejection", (reason) => {
34
51
  process.exit(1);
35
52
  });
36
53
 
37
- // 捕获 SIGINT 信号 (Ctrl+C)
54
+ /**
55
+ * 捕获 SIGINT 信号 (Ctrl+C)
56
+ * 确保用户按 Ctrl+C 时能优雅退出
57
+ */
38
58
  process.on("SIGINT", () => {
39
59
  console.log("");
40
60
  process.exit(0);
41
61
  });
42
62
 
43
- // 捕获 SIGTERM 信号
63
+ /**
64
+ * 捕获 SIGTERM 信号
65
+ * 处理进程终止信号
66
+ */
44
67
  process.on("SIGTERM", () => {
45
68
  console.log("");
46
69
  process.exit(0);
47
70
  });
48
71
 
72
+ // ========== 版本信息 ==========
73
+
74
+ /**
75
+ * 版本号由构建工具注入
76
+ * 开发环境下使用 0.0.0-dev
77
+ */
49
78
  declare const __VERSION__: string | undefined;
50
79
 
51
- // 开发环境下从 package.json 读取版本号
80
+ /**
81
+ * 当前版本号
82
+ * 生产环境:从构建时注入的 __VERSION__ 获取
83
+ * 开发环境:使用 0.0.0-dev
84
+ */
52
85
  const version: string =
53
86
  typeof __VERSION__ !== "undefined" && __VERSION__ !== ""
54
87
  ? __VERSION__
55
88
  : "0.0.0-dev";
56
89
 
57
- // 交互式主菜单
90
+ // ========== 交互式主菜单 ==========
91
+
92
+ /**
93
+ * 显示交互式主菜单
94
+ * 提供所有可用命令的可视化选择界面
95
+ */
58
96
  async function mainMenu(): Promise<void> {
59
- // ASCII Art Logo
97
+ // 显示 ASCII Art Logo
60
98
  console.log(
61
99
  colors.green(`
62
100
  ███████╗ ██╗███████╗██╗ ██╗
@@ -97,7 +135,7 @@ async function mainMenu(): Promise<void> {
97
135
  value: "tag-delete",
98
136
  },
99
137
  {
100
- name: `[7] ✏️ 修改 tag ${colors.dim("gw tu")}`,
138
+ name: `[7] ✏️ 重命名 tag ${colors.dim("gw tu")}`,
101
139
  value: "tag-update",
102
140
  },
103
141
  {
@@ -174,9 +212,20 @@ async function mainMenu(): Promise<void> {
174
212
  }
175
213
  }
176
214
 
215
+ // ========== CLI 应用初始化 ==========
216
+
217
+ /**
218
+ * 创建 CLI 应用实例
219
+ * 使用 cac (Command And Conquer) 库
220
+ */
177
221
  const cli = cac("gw");
178
222
 
179
- // 默认命令 - 显示交互式菜单
223
+ // ========== 命令注册 ==========
224
+
225
+ /**
226
+ * 默认命令 - 显示交互式菜单
227
+ * 运行 `gw` 时触发,会检查更新(交互式模式)
228
+ */
180
229
  cli.command("", "显示交互式菜单").action(async () => {
181
230
  await checkForUpdates(version, "@zjex/git-workflow", true);
182
231
  return mainMenu();
@@ -242,7 +291,7 @@ cli
242
291
  });
243
292
 
244
293
  cli
245
- .command("tag:update", "修改 tag 消息")
294
+ .command("tag:update", "重命名 tag")
246
295
  .alias("tu")
247
296
  .action(async () => {
248
297
  await checkForUpdates(version, "@zjex/git-workflow");