@zjex/git-workflow 0.0.1

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,227 @@
1
+ import { execSync } from "child_process";
2
+ import { select, input } from "@inquirer/prompts";
3
+ import ora from "ora";
4
+ import {
5
+ colors,
6
+ TODAY,
7
+ theme,
8
+ exec,
9
+ execOutput,
10
+ getMainBranch,
11
+ divider,
12
+ type BranchType,
13
+ } from "../utils.js";
14
+ import { getConfig } from "../config.js";
15
+
16
+ export async function getBranchName(type: BranchType): Promise<string | null> {
17
+ const config = getConfig();
18
+
19
+ const idLabel =
20
+ type === "feature" ? config.featureIdLabel : config.hotfixIdLabel;
21
+ const branchPrefix =
22
+ type === "feature" ? config.featurePrefix : config.hotfixPrefix;
23
+
24
+ const idMessage = config.requireId
25
+ ? `请输入${idLabel}:`
26
+ : `请输入${idLabel} (可跳过):`;
27
+
28
+ const id = await input({ message: idMessage, theme });
29
+
30
+ if (config.requireId && !id) {
31
+ console.log(colors.red(`${idLabel}不能为空`));
32
+ return null;
33
+ }
34
+
35
+ const description = await input({ message: "请输入描述:", theme });
36
+ if (!description) {
37
+ console.log(colors.red("描述不能为空"));
38
+ return null;
39
+ }
40
+
41
+ return id
42
+ ? `${branchPrefix}/${TODAY}-${id}-${description}`
43
+ : `${branchPrefix}/${TODAY}-${description}`;
44
+ }
45
+
46
+ export async function createBranch(
47
+ type: BranchType,
48
+ baseBranchArg?: string | null
49
+ ): Promise<void> {
50
+ const config = getConfig();
51
+
52
+ const branchName = await getBranchName(type);
53
+ if (!branchName) return;
54
+
55
+ divider();
56
+
57
+ // 优先使用参数,其次配置文件,最后自动检测
58
+ let baseBranch: string;
59
+ if (baseBranchArg) {
60
+ baseBranch = `origin/${baseBranchArg}`;
61
+ } else if (config.baseBranch) {
62
+ baseBranch = `origin/${config.baseBranch}`;
63
+ } else {
64
+ baseBranch = getMainBranch();
65
+ }
66
+
67
+ const spinner = ora(`正在从 ${baseBranch} 创建分支...`).start();
68
+
69
+ try {
70
+ exec(`git fetch origin ${baseBranch.replace("origin/", "")}`, true);
71
+ exec(`git checkout -b "${branchName}" ${baseBranch}`);
72
+ spinner.succeed(`分支创建成功: ${branchName}`);
73
+
74
+ divider();
75
+
76
+ // 根据配置决定是否询问推送
77
+ let shouldPush: boolean;
78
+ if (config.autoPush !== undefined) {
79
+ shouldPush = config.autoPush;
80
+ if (shouldPush) {
81
+ console.log(colors.dim("(自动推送已启用)"));
82
+ }
83
+ } else {
84
+ shouldPush = await select({
85
+ message: "是否推送到远程?",
86
+ choices: [
87
+ { name: "是", value: true },
88
+ { name: "否", value: false },
89
+ ],
90
+ theme,
91
+ });
92
+ }
93
+
94
+ if (shouldPush) {
95
+ const pushSpinner = ora("正在推送到远程...").start();
96
+ try {
97
+ execSync(`git push -u origin "${branchName}"`, { stdio: "pipe" });
98
+ pushSpinner.succeed(`已推送到远程: origin/${branchName}`);
99
+ } catch {
100
+ pushSpinner.warn(
101
+ "远程推送失败,可稍后手动执行: git push -u origin " + branchName
102
+ );
103
+ }
104
+ }
105
+ } catch {
106
+ spinner.fail("分支创建失败");
107
+ }
108
+ }
109
+
110
+ export async function deleteBranch(branchArg?: string): Promise<void> {
111
+ const fetchSpinner = ora("正在获取分支信息...").start();
112
+ exec("git fetch --all --prune", true);
113
+ fetchSpinner.succeed("分支信息已更新");
114
+
115
+ divider();
116
+
117
+ const currentBranch = execOutput("git branch --show-current");
118
+
119
+ let branch = branchArg;
120
+
121
+ if (!branch) {
122
+ const recentBranches = execOutput(
123
+ "git for-each-ref --sort=-committerdate refs/heads/ --format='%(refname:short)'"
124
+ )
125
+ .split("\n")
126
+ .filter((b) => b && b !== currentBranch);
127
+
128
+ if (recentBranches.length === 0) {
129
+ console.log(colors.yellow("没有可删除的本地分支"));
130
+ return;
131
+ }
132
+
133
+ interface BranchChoice {
134
+ name: string;
135
+ value: string;
136
+ }
137
+
138
+ const choices: BranchChoice[] = recentBranches.map((b) => {
139
+ const hasRemote = execOutput(`git branch -r | grep "origin/${b}$"`);
140
+ return {
141
+ name: hasRemote ? `${b} (本地+远程)` : `${b} (仅本地)`,
142
+ value: b,
143
+ };
144
+ });
145
+ choices.push({ name: "取消", value: "__cancel__" });
146
+
147
+ branch = await select({
148
+ message: "选择要删除的分支 (按最近使用排序):",
149
+ choices,
150
+ theme,
151
+ });
152
+
153
+ if (branch === "__cancel__") {
154
+ console.log(colors.yellow("已取消"));
155
+ return;
156
+ }
157
+ }
158
+
159
+ if (branch === currentBranch) {
160
+ console.log(colors.red("不能删除当前所在分支"));
161
+ return;
162
+ }
163
+
164
+ const localExists = execOutput(`git branch --list "${branch}"`);
165
+ const hasRemote = execOutput(`git branch -r | grep "origin/${branch}$"`);
166
+
167
+ if (!localExists) {
168
+ if (hasRemote) {
169
+ console.log(
170
+ colors.yellow(`本地分支不存在,但远程分支存在: origin/${branch}`)
171
+ );
172
+ const deleteRemote = await select({
173
+ message: `是否删除远程分支 origin/${branch}?`,
174
+ choices: [
175
+ { name: "是", value: true },
176
+ { name: "否", value: false },
177
+ ],
178
+ theme,
179
+ });
180
+
181
+ if (deleteRemote) {
182
+ const spinner = ora(`正在删除远程分支: origin/${branch}`).start();
183
+ try {
184
+ execSync(`git push origin --delete "${branch}"`, { stdio: "pipe" });
185
+ spinner.succeed(`远程分支已删除: origin/${branch}`);
186
+ } catch {
187
+ spinner.fail("远程分支删除失败");
188
+ }
189
+ }
190
+ } else {
191
+ console.log(colors.red(`分支不存在: ${branch}`));
192
+ }
193
+ return;
194
+ }
195
+
196
+ const localSpinner = ora(`正在删除本地分支: ${branch}`).start();
197
+ try {
198
+ execSync(`git branch -D "${branch}"`, { stdio: "pipe" });
199
+ localSpinner.succeed(`本地分支已删除: ${branch}`);
200
+ } catch {
201
+ localSpinner.fail("本地分支删除失败");
202
+ return;
203
+ }
204
+
205
+ 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
+ }
225
+ }
226
+ }
227
+ }
@@ -0,0 +1,49 @@
1
+ import { TODAY } from "../utils.js";
2
+
3
+ export function showHelp(): string {
4
+ return `
5
+ 分支命令:
6
+ gw feature [--base <branch>] 创建 feature 分支
7
+ gw feat [--base <branch>] 同上 (简写)
8
+ gw f [--base <branch>] 同上 (简写)
9
+
10
+ gw hotfix [--base <branch>] 创建 hotfix 分支
11
+ gw fix [--base <branch>] 同上 (简写)
12
+ gw h [--base <branch>] 同上 (简写)
13
+
14
+ gw delete [branch] 删除本地/远程分支
15
+ gw del [branch] 同上 (简写)
16
+ gw d [branch] 同上 (简写)
17
+
18
+ Tag 命令:
19
+ gw tags [prefix] 列出所有 tag,可按前缀过滤
20
+ gw ts [prefix] 同上 (简写)
21
+
22
+ gw tag [prefix] 交互式选择版本类型并创建 tag
23
+ gw t [prefix] 同上 (简写)
24
+
25
+ 发布命令:
26
+ gw release 交互式选择版本号并更新 package.json
27
+ gw r 同上 (简写)
28
+
29
+ 配置命令:
30
+ gw init 初始化配置文件 .gwrc.json
31
+
32
+ Stash 命令:
33
+ gw stash 交互式管理 stash
34
+ gw s 同上 (简写)
35
+
36
+ 示例:
37
+ gw feat 基于 main/master 创建 feature 分支
38
+ gw feat --base develop 基于 develop 分支创建 feature 分支
39
+ gw fix --base release 基于 release 分支创建 hotfix 分支
40
+ gw del 交互式选择并删除分支
41
+ gw del feature/xxx 直接删除指定分支
42
+ gw tags v 列出所有 v 开头的 tag
43
+ gw tag 交互式创建 tag
44
+
45
+ 分支命名格式:
46
+ feature/${TODAY}-<Story ID>-<描述>
47
+ hotfix/${TODAY}-<Issue ID>-<描述>
48
+ `;
49
+ }
@@ -0,0 +1,103 @@
1
+ import { existsSync, writeFileSync } from "fs";
2
+ import { select, input, confirm } from "@inquirer/prompts";
3
+ import { colors, theme, divider } from "../utils.js";
4
+ import type { GwConfig } from "../config.js";
5
+
6
+ const CONFIG_FILE = ".gwrc.json";
7
+
8
+ export async function init(): Promise<void> {
9
+ if (existsSync(CONFIG_FILE)) {
10
+ const overwrite = await confirm({
11
+ message: `${CONFIG_FILE} 已存在,是否覆盖?`,
12
+ default: false,
13
+ theme,
14
+ });
15
+ if (!overwrite) {
16
+ console.log(colors.yellow("已取消"));
17
+ return;
18
+ }
19
+ }
20
+
21
+ console.log(colors.dim("配置 git-workflow,直接回车使用默认值\n"));
22
+
23
+ const config: Partial<GwConfig> = {};
24
+
25
+ // 基础分支
26
+ const baseBranch = await input({
27
+ message: "默认基础分支 (留空自动检测 main/master):",
28
+ theme,
29
+ });
30
+ if (baseBranch) config.baseBranch = baseBranch;
31
+
32
+ divider();
33
+
34
+ // 分支前缀
35
+ const featurePrefix = await input({
36
+ message: "Feature 分支前缀:",
37
+ default: "feature",
38
+ theme,
39
+ });
40
+ if (featurePrefix !== "feature") config.featurePrefix = featurePrefix;
41
+
42
+ const hotfixPrefix = await input({
43
+ message: "Hotfix 分支前缀:",
44
+ default: "hotfix",
45
+ theme,
46
+ });
47
+ if (hotfixPrefix !== "hotfix") config.hotfixPrefix = hotfixPrefix;
48
+
49
+ divider();
50
+
51
+ // ID 配置
52
+ const requireId = await confirm({
53
+ message: "是否要求必填 ID (Story ID / Issue ID)?",
54
+ default: false,
55
+ theme,
56
+ });
57
+ if (requireId) config.requireId = true;
58
+
59
+ const featureIdLabel = await input({
60
+ message: "Feature 分支 ID 标签:",
61
+ default: "Story ID",
62
+ theme,
63
+ });
64
+ if (featureIdLabel !== "Story ID") config.featureIdLabel = featureIdLabel;
65
+
66
+ const hotfixIdLabel = await input({
67
+ message: "Hotfix 分支 ID 标签:",
68
+ default: "Issue ID",
69
+ theme,
70
+ });
71
+ if (hotfixIdLabel !== "Issue ID") config.hotfixIdLabel = hotfixIdLabel;
72
+
73
+ divider();
74
+
75
+ // Tag 配置
76
+ const defaultTagPrefix = await input({
77
+ message: "默认 Tag 前缀 (留空则每次选择):",
78
+ theme,
79
+ });
80
+ if (defaultTagPrefix) config.defaultTagPrefix = defaultTagPrefix;
81
+
82
+ // 自动推送
83
+ const autoPushChoice = await select({
84
+ message: "创建分支后是否自动推送?",
85
+ choices: [
86
+ { name: "每次询问", value: "ask" },
87
+ { name: "自动推送", value: "yes" },
88
+ { name: "不推送", value: "no" },
89
+ ],
90
+ theme,
91
+ });
92
+ if (autoPushChoice === "yes") config.autoPush = true;
93
+ if (autoPushChoice === "no") config.autoPush = false;
94
+
95
+ divider();
96
+
97
+ // 写入配置
98
+ const content = JSON.stringify(config, null, 2);
99
+ writeFileSync(CONFIG_FILE, content + "\n");
100
+
101
+ console.log(colors.green(`✓ 配置已保存到 ${CONFIG_FILE}`));
102
+ console.log(colors.dim("\n" + content));
103
+ }
@@ -0,0 +1,118 @@
1
+ import { readFileSync, writeFileSync } from "fs";
2
+ import { select } from "@inquirer/prompts";
3
+ import { colors, theme, divider } from "../utils.js";
4
+
5
+ interface VersionChoice {
6
+ name: string;
7
+ value: string;
8
+ }
9
+
10
+ function getPackageVersion(): string {
11
+ const pkg = JSON.parse(readFileSync("package.json", "utf-8"));
12
+ return pkg.version || "0.0.0";
13
+ }
14
+
15
+ function setPackageVersion(version: string): void {
16
+ const pkg = JSON.parse(readFileSync("package.json", "utf-8"));
17
+ pkg.version = version;
18
+ writeFileSync("package.json", JSON.stringify(pkg, null, "\t") + "\n");
19
+ }
20
+
21
+ function generateChoices(current: string): VersionChoice[] {
22
+ const preReleaseMatch = current.match(
23
+ /^(\d+)\.(\d+)\.(\d+)-([a-zA-Z]+)\.(\d+)$/
24
+ );
25
+ const match = current.match(/^(\d+)\.(\d+)\.(\d+)$/);
26
+
27
+ if (preReleaseMatch) {
28
+ const [, majorStr, minorStr, patchStr, preTag, preNumStr] = preReleaseMatch;
29
+ const major = Number(majorStr);
30
+ const minor = Number(minorStr);
31
+ const patch = Number(patchStr);
32
+ const preNum = Number(preNumStr);
33
+ const baseVersion = `${major}.${minor}.${patch}`;
34
+
35
+ return [
36
+ {
37
+ name: `pre → ${baseVersion}-${preTag}.${preNum + 1}`,
38
+ value: `${baseVersion}-${preTag}.${preNum + 1}`,
39
+ },
40
+ { name: `release → ${baseVersion}`, value: baseVersion },
41
+ {
42
+ name: `patch → ${major}.${minor}.${patch + 1}`,
43
+ value: `${major}.${minor}.${patch + 1}`,
44
+ },
45
+ {
46
+ name: `minor → ${major}.${minor + 1}.0`,
47
+ value: `${major}.${minor + 1}.0`,
48
+ },
49
+ { name: `major → ${major + 1}.0.0`, value: `${major + 1}.0.0` },
50
+ ];
51
+ }
52
+
53
+ if (match) {
54
+ const [, majorStr, minorStr, patchStr] = match;
55
+ const major = Number(majorStr);
56
+ const minor = Number(minorStr);
57
+ const patch = Number(patchStr);
58
+
59
+ return [
60
+ {
61
+ name: `patch → ${major}.${minor}.${patch + 1}`,
62
+ value: `${major}.${minor}.${patch + 1}`,
63
+ },
64
+ {
65
+ name: `minor → ${major}.${minor + 1}.0`,
66
+ value: `${major}.${minor + 1}.0`,
67
+ },
68
+ { name: `major → ${major + 1}.0.0`, value: `${major + 1}.0.0` },
69
+ {
70
+ name: `alpha → ${major}.${minor}.${patch + 1}-alpha.1`,
71
+ value: `${major}.${minor}.${patch + 1}-alpha.1`,
72
+ },
73
+ {
74
+ name: `beta → ${major}.${minor}.${patch + 1}-beta.1`,
75
+ value: `${major}.${minor}.${patch + 1}-beta.1`,
76
+ },
77
+ {
78
+ name: `rc → ${major}.${minor}.${patch + 1}-rc.1`,
79
+ value: `${major}.${minor}.${patch + 1}-rc.1`,
80
+ },
81
+ ];
82
+ }
83
+
84
+ // fallback
85
+ return [
86
+ { name: `patch → 0.0.1`, value: "0.0.1" },
87
+ { name: `minor → 0.1.0`, value: "0.1.0" },
88
+ { name: `major → 1.0.0`, value: "1.0.0" },
89
+ ];
90
+ }
91
+
92
+ export async function release(): Promise<void> {
93
+ const currentVersion = getPackageVersion();
94
+ console.log(colors.yellow(`当前版本: ${currentVersion}`));
95
+
96
+ divider();
97
+
98
+ const choices = generateChoices(currentVersion);
99
+ choices.push({ name: "取消", value: "__cancel__" });
100
+
101
+ const nextVersion = await select({
102
+ message: "选择新版本:",
103
+ choices,
104
+ theme,
105
+ });
106
+
107
+ if (nextVersion === "__cancel__") {
108
+ console.log(colors.yellow("已取消"));
109
+ return;
110
+ }
111
+
112
+ setPackageVersion(nextVersion);
113
+
114
+ divider();
115
+ console.log(
116
+ colors.green(`✓ 版本号已更新: ${currentVersion} → ${nextVersion}`)
117
+ );
118
+ }