gflows 0.1.12 → 0.1.14
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.
- package/README.md +328 -341
- package/package.json +6 -2
- package/src/cli.ts +64 -21
- package/src/commands/bump.ts +81 -31
- package/src/commands/completion.ts +21 -30
- package/src/commands/delete.ts +10 -30
- package/src/commands/finish.ts +34 -58
- package/src/commands/help.ts +1 -1
- package/src/commands/init.ts +8 -13
- package/src/commands/list.ts +10 -27
- package/src/commands/start.ts +15 -18
- package/src/commands/status.ts +11 -28
- package/src/commands/switch.ts +12 -23
- package/src/config.ts +43 -23
- package/src/constants.ts +1 -1
- package/src/errors.ts +4 -2
- package/src/git.ts +20 -28
- package/src/index.ts +59 -63
- package/src/out.ts +10 -2
- package/src/types.ts +1 -7
package/src/commands/finish.ts
CHANGED
|
@@ -5,13 +5,8 @@
|
|
|
5
5
|
* @module commands/finish
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import
|
|
8
|
+
import { getBranchTypeMeta, resolveConfig } from "../config.js";
|
|
9
9
|
import { EXIT_USER, VERSION_REGEX } from "../constants.js";
|
|
10
|
-
import {
|
|
11
|
-
getBranchTypeMeta,
|
|
12
|
-
getPrefixForType,
|
|
13
|
-
resolveConfig,
|
|
14
|
-
} from "../config.js";
|
|
15
10
|
import { BranchNotFoundError } from "../errors.js";
|
|
16
11
|
import {
|
|
17
12
|
assertNoRebaseOrMerge,
|
|
@@ -27,6 +22,7 @@ import {
|
|
|
27
22
|
tagExists,
|
|
28
23
|
} from "../git.js";
|
|
29
24
|
import { hint, success } from "../out.js";
|
|
25
|
+
import type { BranchType, ParsedArgs } from "../types.js";
|
|
30
26
|
|
|
31
27
|
/** Normalizes version to vX.Y.Z for tag name. */
|
|
32
28
|
function normalizeTagVersion(version: string): string {
|
|
@@ -39,7 +35,7 @@ function normalizeTagVersion(version: string): string {
|
|
|
39
35
|
*/
|
|
40
36
|
async function getWorkflowBranches(
|
|
41
37
|
cwd: string,
|
|
42
|
-
prefixes: Record<BranchType, string
|
|
38
|
+
prefixes: Record<BranchType, string>,
|
|
43
39
|
): Promise<string[]> {
|
|
44
40
|
const all = await branchList(cwd, { dryRun: false, verbose: false });
|
|
45
41
|
const workflow: string[] = [];
|
|
@@ -59,16 +55,9 @@ async function getWorkflowBranches(
|
|
|
59
55
|
*/
|
|
60
56
|
function parseBranchTypeAndVersion(
|
|
61
57
|
branchName: string,
|
|
62
|
-
prefixes: Record<BranchType, string
|
|
58
|
+
prefixes: Record<BranchType, string>,
|
|
63
59
|
): { type: BranchType; version?: string } | null {
|
|
64
|
-
for (const type of [
|
|
65
|
-
"release",
|
|
66
|
-
"hotfix",
|
|
67
|
-
"feature",
|
|
68
|
-
"bugfix",
|
|
69
|
-
"chore",
|
|
70
|
-
"spike",
|
|
71
|
-
] as BranchType[]) {
|
|
60
|
+
for (const type of ["release", "hotfix", "feature", "bugfix", "chore", "spike"] as BranchType[]) {
|
|
72
61
|
const prefix = prefixes[type];
|
|
73
62
|
if (prefix && branchName.startsWith(prefix)) {
|
|
74
63
|
const suffix = branchName.slice(prefix.length);
|
|
@@ -93,7 +82,7 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
93
82
|
const config = resolveConfig(
|
|
94
83
|
repoRoot,
|
|
95
84
|
{ main: args.main, dev: args.dev, remote: args.remote },
|
|
96
|
-
{ verbose: args.verbose }
|
|
85
|
+
{ verbose: args.verbose },
|
|
97
86
|
);
|
|
98
87
|
|
|
99
88
|
const opts: { dryRun: boolean; verbose: boolean } = {
|
|
@@ -106,9 +95,7 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
106
95
|
// Resolve branch to finish (and guard main/dev early so we can error before picker)
|
|
107
96
|
let branchToFinish: string;
|
|
108
97
|
const explicitBranch =
|
|
109
|
-
typeof args.branch === "string" && args.branch.trim() !== ""
|
|
110
|
-
? args.branch.trim()
|
|
111
|
-
: undefined;
|
|
98
|
+
typeof args.branch === "string" && args.branch.trim() !== "" ? args.branch.trim() : undefined;
|
|
112
99
|
|
|
113
100
|
if (explicitBranch) {
|
|
114
101
|
branchToFinish = explicitBranch;
|
|
@@ -127,7 +114,7 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
127
114
|
const current = await getCurrentBranch(repoRoot, opts);
|
|
128
115
|
if (!current) {
|
|
129
116
|
console.error(
|
|
130
|
-
"gflows finish: HEAD is detached. Checkout a branch or specify one with -B <name>."
|
|
117
|
+
"gflows finish: HEAD is detached. Checkout a branch or specify one with -B <name>.",
|
|
131
118
|
);
|
|
132
119
|
process.exit(EXIT_USER);
|
|
133
120
|
}
|
|
@@ -137,7 +124,7 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
137
124
|
// Refuse finish on main or dev
|
|
138
125
|
if (branchToFinish === config.main || branchToFinish === config.dev) {
|
|
139
126
|
console.error(
|
|
140
|
-
`gflows finish: cannot finish the long-lived branch '${branchToFinish}'. Finish a workflow branch (feature, bugfix, etc.) instead
|
|
127
|
+
`gflows finish: cannot finish the long-lived branch '${branchToFinish}'. Finish a workflow branch (feature, bugfix, etc.) instead.`,
|
|
141
128
|
);
|
|
142
129
|
process.exit(2);
|
|
143
130
|
}
|
|
@@ -147,13 +134,13 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
147
134
|
const type: BranchType | undefined = args.type ?? parsed?.type ?? undefined;
|
|
148
135
|
if (!type) {
|
|
149
136
|
console.error(
|
|
150
|
-
`gflows finish: cannot determine branch type for '${branchToFinish}'. Specify type (e.g. gflows finish feature) or use a branch name with a known prefix
|
|
137
|
+
`gflows finish: cannot determine branch type for '${branchToFinish}'. Specify type (e.g. gflows finish feature) or use a branch name with a known prefix.`,
|
|
151
138
|
);
|
|
152
139
|
process.exit(EXIT_USER);
|
|
153
140
|
}
|
|
154
141
|
if (parsed && parsed.type !== type) {
|
|
155
142
|
console.error(
|
|
156
|
-
`gflows finish: branch '${branchToFinish}' matches type '${parsed.type}', but '${type}' was specified
|
|
143
|
+
`gflows finish: branch '${branchToFinish}' matches type '${parsed.type}', but '${type}' was specified.`,
|
|
157
144
|
);
|
|
158
145
|
process.exit(EXIT_USER);
|
|
159
146
|
}
|
|
@@ -175,7 +162,7 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
175
162
|
}
|
|
176
163
|
} else if (meta.tagOnFinish && !version) {
|
|
177
164
|
console.error(
|
|
178
|
-
`gflows finish: release/hotfix branch '${branchToFinish}' has no valid version segment. Use format release/vX.Y.Z or hotfix/vX.Y.Z
|
|
165
|
+
`gflows finish: release/hotfix branch '${branchToFinish}' has no valid version segment. Use format release/vX.Y.Z or hotfix/vX.Y.Z.`,
|
|
179
166
|
);
|
|
180
167
|
process.exit(EXIT_USER);
|
|
181
168
|
}
|
|
@@ -184,38 +171,33 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
184
171
|
const branches = await branchList(repoRoot, { ...opts, dryRun: false });
|
|
185
172
|
if (!branches.includes(branchToFinish)) {
|
|
186
173
|
throw new BranchNotFoundError(
|
|
187
|
-
`Branch '${branchToFinish}' not found. Specify an existing local branch with -B <name
|
|
174
|
+
`Branch '${branchToFinish}' not found. Specify an existing local branch with -B <name>.`,
|
|
188
175
|
);
|
|
189
176
|
}
|
|
190
177
|
|
|
191
178
|
const noFf = args.noFf;
|
|
179
|
+
if (meta.mergeTarget === "dev") {
|
|
180
|
+
await checkout(repoRoot, config.dev, opts);
|
|
181
|
+
await merge(repoRoot, branchToFinish, { ...opts, noFf });
|
|
182
|
+
} else {
|
|
183
|
+
// main-then-dev: merge into main first, then merge main into dev
|
|
184
|
+
await checkout(repoRoot, config.main, opts);
|
|
185
|
+
await merge(repoRoot, branchToFinish, { ...opts, noFf });
|
|
192
186
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
await
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if (meta.tagOnFinish && version && !args.noTag) {
|
|
203
|
-
const tagName = normalizeTagVersion(version);
|
|
204
|
-
await tag(repoRoot, tagName, {
|
|
205
|
-
...opts,
|
|
206
|
-
sign: args.signTag,
|
|
207
|
-
tagMessage: args.tagMessage,
|
|
208
|
-
});
|
|
209
|
-
if (!args.quiet && !args.dryRun) {
|
|
210
|
-
success(`gflows: created tag '${tagName}'.`);
|
|
211
|
-
}
|
|
187
|
+
if (meta.tagOnFinish && version && !args.noTag) {
|
|
188
|
+
const tagName = normalizeTagVersion(version);
|
|
189
|
+
await tag(repoRoot, tagName, {
|
|
190
|
+
...opts,
|
|
191
|
+
sign: args.signTag,
|
|
192
|
+
tagMessage: args.tagMessage,
|
|
193
|
+
});
|
|
194
|
+
if (!args.quiet && !args.dryRun) {
|
|
195
|
+
success(`gflows: created tag '${tagName}'.`);
|
|
212
196
|
}
|
|
213
|
-
|
|
214
|
-
await checkout(repoRoot, config.dev, opts);
|
|
215
|
-
await merge(repoRoot, config.main, { ...opts, noFf });
|
|
216
197
|
}
|
|
217
|
-
|
|
218
|
-
|
|
198
|
+
|
|
199
|
+
await checkout(repoRoot, config.dev, opts);
|
|
200
|
+
await merge(repoRoot, config.main, { ...opts, noFf });
|
|
219
201
|
}
|
|
220
202
|
|
|
221
203
|
// Optional: delete the finished branch
|
|
@@ -263,16 +245,10 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
263
245
|
if (meta.mergeTarget === "main-then-dev") {
|
|
264
246
|
refsToPush.push(config.main);
|
|
265
247
|
}
|
|
266
|
-
const pushCode = await push(
|
|
267
|
-
repoRoot,
|
|
268
|
-
remote,
|
|
269
|
-
refsToPush,
|
|
270
|
-
didCreateTag,
|
|
271
|
-
opts
|
|
272
|
-
);
|
|
248
|
+
const pushCode = await push(repoRoot, remote, refsToPush, didCreateTag, opts);
|
|
273
249
|
if (pushCode !== 0) {
|
|
274
250
|
console.error(
|
|
275
|
-
"gflows: merge and tag succeeded locally, but push failed. Retry with `git push` or `gflows finish ... --push`."
|
|
251
|
+
"gflows: merge and tag succeeded locally, but push failed. Retry with `git push` or `gflows finish ... --push`.",
|
|
276
252
|
);
|
|
277
253
|
process.exit(2);
|
|
278
254
|
}
|
package/src/commands/help.ts
CHANGED
|
@@ -32,7 +32,7 @@ Types: feature (-f), bugfix (-b), chore (-c), release (-r), hotfix (-x), spike (
|
|
|
32
32
|
|
|
33
33
|
Common flags:
|
|
34
34
|
-p, --push Push after init/start/finish
|
|
35
|
-
-P, --no-push Do not push (
|
|
35
|
+
-P, --no-push Do not push (init/start: push is default; finish: prompts when neither -p nor -P)
|
|
36
36
|
--main <name> Main branch (init: persist to .gflows.json)
|
|
37
37
|
--dev <name> Dev branch (init: persist to .gflows.json)
|
|
38
38
|
-R, --remote <name> Remote for push (init: persist to .gflows.json)
|
package/src/commands/init.ts
CHANGED
|
@@ -6,13 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { resolveConfig, writeConfigFile } from "../config.js";
|
|
8
8
|
import { BranchNotFoundError, NotRepoError } from "../errors.js";
|
|
9
|
-
import {
|
|
10
|
-
branchList,
|
|
11
|
-
push,
|
|
12
|
-
resolveRepoRoot,
|
|
13
|
-
revParse,
|
|
14
|
-
runGit,
|
|
15
|
-
} from "../git.js";
|
|
9
|
+
import { branchList, push, resolveRepoRoot, revParse, runGit } from "../git.js";
|
|
16
10
|
import { banner, hint, success } from "../out.js";
|
|
17
11
|
import type { ParsedArgs } from "../types.js";
|
|
18
12
|
|
|
@@ -33,7 +27,7 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
33
27
|
dev: args.dev,
|
|
34
28
|
remote: args.remote,
|
|
35
29
|
},
|
|
36
|
-
{ verbose: args.verbose }
|
|
30
|
+
{ verbose: args.verbose },
|
|
37
31
|
);
|
|
38
32
|
|
|
39
33
|
if (!args.quiet) {
|
|
@@ -44,7 +38,7 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
44
38
|
` dev ${config.dev}`,
|
|
45
39
|
` remote ${config.remote}`,
|
|
46
40
|
"",
|
|
47
|
-
"→ Dev from main. Use --push to
|
|
41
|
+
"→ Dev from main. Use --no-push to skip pushing.",
|
|
48
42
|
]);
|
|
49
43
|
}
|
|
50
44
|
|
|
@@ -60,7 +54,7 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
60
54
|
if (err instanceof NotRepoError) throw err;
|
|
61
55
|
if (err instanceof BranchNotFoundError) {
|
|
62
56
|
throw new BranchNotFoundError(
|
|
63
|
-
`Main branch '${config.main}' does not exist. Create an initial commit and the main branch first
|
|
57
|
+
`Main branch '${config.main}' does not exist. Create an initial commit and the main branch first.`,
|
|
64
58
|
);
|
|
65
59
|
}
|
|
66
60
|
throw err;
|
|
@@ -76,12 +70,12 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
76
70
|
}
|
|
77
71
|
}
|
|
78
72
|
|
|
79
|
-
const doPush =
|
|
73
|
+
const doPush = !args.noPush;
|
|
80
74
|
if (doPush) {
|
|
81
75
|
const pushCode = await push(repoRoot, config.remote, [config.dev], false, opts);
|
|
82
76
|
if (pushCode !== 0) {
|
|
83
77
|
throw new Error(
|
|
84
|
-
`Push failed. Local branch '${config.dev}' was created. Retry with \`git push ${config.remote} ${config.dev}\` or \`gflows init
|
|
78
|
+
`Push failed. Local branch '${config.dev}' was created. Retry with \`git push ${config.remote} ${config.dev}\` or \`gflows init\`.`,
|
|
85
79
|
);
|
|
86
80
|
}
|
|
87
81
|
if (!args.quiet && !args.dryRun) {
|
|
@@ -89,7 +83,8 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
89
83
|
}
|
|
90
84
|
}
|
|
91
85
|
|
|
92
|
-
const hasConfigFlags =
|
|
86
|
+
const hasConfigFlags =
|
|
87
|
+
args.main !== undefined || args.dev !== undefined || args.remote !== undefined;
|
|
93
88
|
if (!args.dryRun && hasConfigFlags) {
|
|
94
89
|
writeConfigFile(repoRoot, {
|
|
95
90
|
...(args.main !== undefined && { main: args.main }),
|
package/src/commands/list.ts
CHANGED
|
@@ -5,22 +5,13 @@
|
|
|
5
5
|
* @module commands/list
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { BranchType } from "../types.js";
|
|
9
|
-
import type { ParsedArgs } from "../types.js";
|
|
10
|
-
import type { ResolvedConfig } from "../types.js";
|
|
11
8
|
import { resolveConfig } from "../config.js";
|
|
12
9
|
import { NotRepoError } from "../errors.js";
|
|
13
10
|
import { branchList, fetch, resolveRepoRoot } from "../git.js";
|
|
14
11
|
import { hint } from "../out.js";
|
|
12
|
+
import type { BranchType, ParsedArgs, ResolvedConfig } from "../types.js";
|
|
15
13
|
|
|
16
|
-
const BRANCH_TYPES: BranchType[] = [
|
|
17
|
-
"feature",
|
|
18
|
-
"bugfix",
|
|
19
|
-
"chore",
|
|
20
|
-
"release",
|
|
21
|
-
"hotfix",
|
|
22
|
-
"spike",
|
|
23
|
-
];
|
|
14
|
+
const BRANCH_TYPES: BranchType[] = ["feature", "bugfix", "chore", "release", "hotfix", "spike"];
|
|
24
15
|
|
|
25
16
|
/**
|
|
26
17
|
* Returns branch names that match any workflow prefix. If typeFilter is set,
|
|
@@ -29,13 +20,11 @@ const BRANCH_TYPES: BranchType[] = [
|
|
|
29
20
|
function filterWorkflowBranches(
|
|
30
21
|
allBranches: string[],
|
|
31
22
|
config: ResolvedConfig,
|
|
32
|
-
typeFilter: BranchType | undefined
|
|
23
|
+
typeFilter: BranchType | undefined,
|
|
33
24
|
): string[] {
|
|
34
25
|
const { main, dev, prefixes } = config;
|
|
35
26
|
const prefixesToMatch =
|
|
36
|
-
typeFilter !== undefined
|
|
37
|
-
? [prefixes[typeFilter]]
|
|
38
|
-
: BRANCH_TYPES.map((t) => prefixes[t]);
|
|
27
|
+
typeFilter !== undefined ? [prefixes[typeFilter]] : BRANCH_TYPES.map((t) => prefixes[t]);
|
|
39
28
|
|
|
40
29
|
return allBranches.filter((b) => {
|
|
41
30
|
if (b === main || b === dev) return false;
|
|
@@ -61,7 +50,7 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
61
50
|
const config = resolveConfig(
|
|
62
51
|
root,
|
|
63
52
|
{ main: args.main, dev: args.dev, remote: args.remote },
|
|
64
|
-
{ verbose: !!verbose }
|
|
53
|
+
{ verbose: !!verbose },
|
|
65
54
|
);
|
|
66
55
|
|
|
67
56
|
if (includeRemote && !dryRun) {
|
|
@@ -76,23 +65,17 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
76
65
|
verbose: !!verbose,
|
|
77
66
|
});
|
|
78
67
|
|
|
79
|
-
const workflowBranches = filterWorkflowBranches(
|
|
80
|
-
allBranches,
|
|
81
|
-
config,
|
|
82
|
-
typeFilter
|
|
83
|
-
);
|
|
68
|
+
const workflowBranches = filterWorkflowBranches(allBranches, config, typeFilter);
|
|
84
69
|
|
|
85
|
-
|
|
70
|
+
// Show main and dev first (when present), then workflow branches
|
|
71
|
+
const mainAndDev = [config.main, config.dev].filter((b) => allBranches.includes(b));
|
|
72
|
+
const sorted = [...mainAndDev, ...[...workflowBranches].sort()];
|
|
86
73
|
|
|
87
74
|
for (const b of sorted) {
|
|
88
75
|
console.log(b);
|
|
89
76
|
}
|
|
90
77
|
|
|
91
|
-
if (!quiet && sorted.length
|
|
92
|
-
console.error("No workflow branches found.");
|
|
93
|
-
// Hint: suggest creating first workflow branch
|
|
94
|
-
hint("Run gflows start <type> <name> to create a workflow branch.");
|
|
95
|
-
} else if (!quiet && sorted.length > 0) {
|
|
78
|
+
if (!quiet && sorted.length > 0) {
|
|
96
79
|
// Hint: suggest switching to a listed branch
|
|
97
80
|
hint("Use gflows switch <branch> to switch to a branch.");
|
|
98
81
|
}
|
package/src/commands/start.ts
CHANGED
|
@@ -4,11 +4,9 @@
|
|
|
4
4
|
* @module commands/start
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { BranchType, ParsedArgs } from "../types.js";
|
|
8
|
-
import { EXIT_USER, VERSION_REGEX } from "../constants.js";
|
|
9
7
|
import { getPrefixForType, resolveConfig } from "../config.js";
|
|
8
|
+
import { EXIT_USER, VERSION_REGEX } from "../constants.js";
|
|
10
9
|
import { BranchNotFoundError, DirtyWorkingTreeError, InvalidVersionError } from "../errors.js";
|
|
11
|
-
import { hint, success } from "../out.js";
|
|
12
10
|
import {
|
|
13
11
|
assertNoRebaseOrMerge,
|
|
14
12
|
assertNotDetached,
|
|
@@ -21,16 +19,13 @@ import {
|
|
|
21
19
|
runGit,
|
|
22
20
|
validateBranchName,
|
|
23
21
|
} from "../git.js";
|
|
22
|
+
import { hint, success } from "../out.js";
|
|
23
|
+
import type { BranchType, ParsedArgs } from "../types.js";
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Returns the base branch name for the given type and fromMain flag (main vs dev).
|
|
27
27
|
*/
|
|
28
|
-
function getBaseBranch(
|
|
29
|
-
type: BranchType,
|
|
30
|
-
fromMain: boolean,
|
|
31
|
-
main: string,
|
|
32
|
-
dev: string
|
|
33
|
-
): string {
|
|
28
|
+
function getBaseBranch(type: BranchType, fromMain: boolean, main: string, dev: string): string {
|
|
34
29
|
if (type === "hotfix") return main;
|
|
35
30
|
if (type === "bugfix" && fromMain) return main;
|
|
36
31
|
return dev;
|
|
@@ -45,7 +40,9 @@ function getBaseBranch(
|
|
|
45
40
|
*/
|
|
46
41
|
export async function run(args: ParsedArgs): Promise<void> {
|
|
47
42
|
if (!args.type || args.name === undefined || args.name === "") {
|
|
48
|
-
console.error(
|
|
43
|
+
console.error(
|
|
44
|
+
"gflows start: requires type and name (e.g. gflows start feature my-feat). Use 'gflows help' for usage.",
|
|
45
|
+
);
|
|
49
46
|
process.exit(EXIT_USER);
|
|
50
47
|
}
|
|
51
48
|
|
|
@@ -55,7 +52,7 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
55
52
|
const config = resolveConfig(
|
|
56
53
|
repoRoot,
|
|
57
54
|
{ main: args.main, dev: args.dev, remote: args.remote },
|
|
58
|
-
{ verbose: args.verbose }
|
|
55
|
+
{ verbose: args.verbose },
|
|
59
56
|
);
|
|
60
57
|
|
|
61
58
|
const opts = {
|
|
@@ -79,7 +76,7 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
79
76
|
if (type === "release" || type === "hotfix") {
|
|
80
77
|
if (!VERSION_REGEX.test(name)) {
|
|
81
78
|
throw new InvalidVersionError(
|
|
82
|
-
`Invalid version '${name}'. Use format vX.Y.Z or X.Y.Z (e.g. v1.2.0)
|
|
79
|
+
`Invalid version '${name}'. Use format vX.Y.Z or X.Y.Z (e.g. v1.2.0).`,
|
|
83
80
|
);
|
|
84
81
|
}
|
|
85
82
|
} else {
|
|
@@ -89,10 +86,10 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
89
86
|
const base = getBaseBranch(type, args.fromMain, config.main, config.dev);
|
|
90
87
|
|
|
91
88
|
// Ensure base exists (local or create from remote after fetch)
|
|
92
|
-
let
|
|
89
|
+
let _baseExists = false;
|
|
93
90
|
try {
|
|
94
91
|
await revParse(repoRoot, base, [], { dryRun: false, verbose: opts.verbose });
|
|
95
|
-
|
|
92
|
+
_baseExists = true;
|
|
96
93
|
} catch {
|
|
97
94
|
// Fetch and try remote ref
|
|
98
95
|
await fetch(repoRoot, config.remote, opts);
|
|
@@ -102,10 +99,10 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
102
99
|
if (!opts.dryRun) {
|
|
103
100
|
await runGit(["branch", base, remoteRef], { cwd: repoRoot, ...opts, dryRun: false });
|
|
104
101
|
}
|
|
105
|
-
|
|
102
|
+
_baseExists = true;
|
|
106
103
|
} catch {
|
|
107
104
|
throw new BranchNotFoundError(
|
|
108
|
-
`Base branch '${base}' not found locally or on ${config.remote}. Create it or push it first
|
|
105
|
+
`Base branch '${base}' not found locally or on ${config.remote}. Create it or push it first.`,
|
|
109
106
|
);
|
|
110
107
|
}
|
|
111
108
|
}
|
|
@@ -116,7 +113,7 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
116
113
|
const branches = await branchList(repoRoot, { ...opts, dryRun: false });
|
|
117
114
|
if (branches.includes(fullBranchName)) {
|
|
118
115
|
throw new BranchNotFoundError(
|
|
119
|
-
`Branch '${fullBranchName}' already exists. Use a different name or switch to it
|
|
116
|
+
`Branch '${fullBranchName}' already exists. Use a different name or switch to it.`,
|
|
120
117
|
);
|
|
121
118
|
}
|
|
122
119
|
|
|
@@ -132,7 +129,7 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
132
129
|
const pushCode = await push(repoRoot, remote, [fullBranchName], false, opts);
|
|
133
130
|
if (pushCode !== 0) {
|
|
134
131
|
throw new Error(
|
|
135
|
-
`Push failed. Local branch '${fullBranchName}' was created. Retry with \`git push ${remote} ${fullBranchName}\` or \`gflows start ... --push
|
|
132
|
+
`Push failed. Local branch '${fullBranchName}' was created. Retry with \`git push ${remote} ${fullBranchName}\` or \`gflows start ... --push\`.`,
|
|
136
133
|
);
|
|
137
134
|
}
|
|
138
135
|
if (!args.quiet && !args.dryRun) {
|
package/src/commands/status.ts
CHANGED
|
@@ -6,28 +6,13 @@
|
|
|
6
6
|
* @module commands/status
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import
|
|
10
|
-
import type { ResolvedConfig } from "../types.js";
|
|
11
|
-
import {
|
|
12
|
-
getBranchTypeMeta,
|
|
13
|
-
resolveConfig,
|
|
14
|
-
} from "../config.js";
|
|
9
|
+
import { getBranchTypeMeta, resolveConfig } from "../config.js";
|
|
15
10
|
import { NotRepoError } from "../errors.js";
|
|
16
|
-
import {
|
|
17
|
-
getAheadBehind,
|
|
18
|
-
getCurrentBranch,
|
|
19
|
-
resolveRepoRoot,
|
|
20
|
-
} from "../git.js";
|
|
11
|
+
import { getAheadBehind, getCurrentBranch, resolveRepoRoot } from "../git.js";
|
|
21
12
|
import { hint } from "../out.js";
|
|
13
|
+
import type { BranchType, ParsedArgs, ResolvedConfig } from "../types.js";
|
|
22
14
|
|
|
23
|
-
const BRANCH_TYPES: BranchType[] = [
|
|
24
|
-
"feature",
|
|
25
|
-
"bugfix",
|
|
26
|
-
"chore",
|
|
27
|
-
"release",
|
|
28
|
-
"hotfix",
|
|
29
|
-
"spike",
|
|
30
|
-
];
|
|
15
|
+
const BRANCH_TYPES: BranchType[] = ["feature", "bugfix", "chore", "release", "hotfix", "spike"];
|
|
31
16
|
|
|
32
17
|
/**
|
|
33
18
|
* Classifies a branch name into a workflow type, "main", "dev", or null (unknown).
|
|
@@ -35,7 +20,7 @@ const BRANCH_TYPES: BranchType[] = [
|
|
|
35
20
|
*/
|
|
36
21
|
function classifyBranch(
|
|
37
22
|
branchName: string,
|
|
38
|
-
config: ResolvedConfig
|
|
23
|
+
config: ResolvedConfig,
|
|
39
24
|
): BranchType | "main" | "dev" | null {
|
|
40
25
|
if (branchName === config.main) return "main";
|
|
41
26
|
if (branchName === config.dev) return "dev";
|
|
@@ -54,7 +39,7 @@ function classifyBranch(
|
|
|
54
39
|
*/
|
|
55
40
|
function formatMergeTarget(
|
|
56
41
|
mergeTarget: "main" | "dev" | "main-then-dev",
|
|
57
|
-
config: ResolvedConfig
|
|
42
|
+
config: ResolvedConfig,
|
|
58
43
|
): string {
|
|
59
44
|
if (mergeTarget === "main-then-dev") {
|
|
60
45
|
return `${config.main}, then ${config.dev}`;
|
|
@@ -78,7 +63,7 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
78
63
|
const config = resolveConfig(
|
|
79
64
|
root,
|
|
80
65
|
{ main: args.main, dev: args.dev, remote: args.remote },
|
|
81
|
-
{ verbose: !!verbose }
|
|
66
|
+
{ verbose: !!verbose },
|
|
82
67
|
);
|
|
83
68
|
const current = await getCurrentBranch(root, {
|
|
84
69
|
dryRun: !!dryRun,
|
|
@@ -129,12 +114,10 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
129
114
|
console.log(`Merge target(s): ${mergeTargetDisplay}`);
|
|
130
115
|
}
|
|
131
116
|
|
|
132
|
-
const { ahead, behind } = await getAheadBehind(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
{ dryRun: !!dryRun, verbose: !!verbose }
|
|
137
|
-
);
|
|
117
|
+
const { ahead, behind } = await getAheadBehind(root, baseBranch, current, {
|
|
118
|
+
dryRun: !!dryRun,
|
|
119
|
+
verbose: !!verbose,
|
|
120
|
+
});
|
|
138
121
|
|
|
139
122
|
if (!quiet) {
|
|
140
123
|
console.log(`Ahead/behind: ${ahead} ahead, ${behind} behind`);
|
package/src/commands/switch.ts
CHANGED
|
@@ -3,38 +3,24 @@
|
|
|
3
3
|
* @module commands/switch
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { BranchType } from "../types.js";
|
|
7
|
-
import type { ParsedArgs } from "../types.js";
|
|
8
6
|
import { resolveConfig } from "../config.js";
|
|
9
7
|
import { EXIT_OK, EXIT_USER } from "../constants.js";
|
|
10
8
|
import { NotRepoError } from "../errors.js";
|
|
11
|
-
import {
|
|
12
|
-
branchList,
|
|
13
|
-
checkout,
|
|
14
|
-
resolveRepoRoot,
|
|
15
|
-
} from "../git.js";
|
|
9
|
+
import { branchList, checkout, resolveRepoRoot } from "../git.js";
|
|
16
10
|
import { hint, success } from "../out.js";
|
|
11
|
+
import type { BranchType, ParsedArgs } from "../types.js";
|
|
17
12
|
|
|
18
|
-
const BRANCH_TYPES: BranchType[] = [
|
|
19
|
-
"feature",
|
|
20
|
-
"bugfix",
|
|
21
|
-
"chore",
|
|
22
|
-
"release",
|
|
23
|
-
"hotfix",
|
|
24
|
-
"spike",
|
|
25
|
-
];
|
|
13
|
+
const BRANCH_TYPES: BranchType[] = ["feature", "bugfix", "chore", "release", "hotfix", "spike"];
|
|
26
14
|
|
|
27
15
|
/**
|
|
28
16
|
* Returns local branch names that match any workflow prefix (feature/, bugfix/, etc.).
|
|
29
17
|
*/
|
|
30
18
|
function getWorkflowBranches(
|
|
31
19
|
allBranches: string[],
|
|
32
|
-
prefixes: Record<BranchType, string
|
|
20
|
+
prefixes: Record<BranchType, string>,
|
|
33
21
|
): string[] {
|
|
34
22
|
const prefixed = BRANCH_TYPES.map((t) => prefixes[t]).filter(Boolean);
|
|
35
|
-
return allBranches.filter((b) =>
|
|
36
|
-
prefixed.some((p) => p && b.startsWith(p))
|
|
37
|
-
);
|
|
23
|
+
return allBranches.filter((b) => prefixed.some((p) => p && b.startsWith(p)));
|
|
38
24
|
}
|
|
39
25
|
|
|
40
26
|
/**
|
|
@@ -74,17 +60,20 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
74
60
|
const isTTY = typeof process.stdin.isTTY === "boolean" && process.stdin.isTTY;
|
|
75
61
|
if (!isTTY) {
|
|
76
62
|
console.error(
|
|
77
|
-
"gflows switch: no branch name given and stdin is not a TTY. Pass a branch name (e.g. gflows switch feature/my-branch) or run from an interactive terminal."
|
|
63
|
+
"gflows switch: no branch name given and stdin is not a TTY. Pass a branch name (e.g. gflows switch feature/my-branch) or run from an interactive terminal.",
|
|
78
64
|
);
|
|
79
65
|
process.exit(EXIT_USER);
|
|
80
66
|
}
|
|
81
67
|
|
|
82
68
|
const allLocal = await branchList(root, { dryRun, verbose: args.verbose });
|
|
83
69
|
const workflowBranches = getWorkflowBranches(allLocal, config.prefixes);
|
|
70
|
+
// Include main and dev so we always show whatever branches exist
|
|
71
|
+
const mainAndDev = [config.main, config.dev].filter((b) => allLocal.includes(b));
|
|
72
|
+
const choices = [...mainAndDev, ...workflowBranches];
|
|
84
73
|
|
|
85
|
-
if (
|
|
74
|
+
if (choices.length === 0) {
|
|
86
75
|
if (!quiet) {
|
|
87
|
-
console.error("No
|
|
76
|
+
console.error("No branches found.");
|
|
88
77
|
}
|
|
89
78
|
process.exit(EXIT_OK);
|
|
90
79
|
}
|
|
@@ -92,7 +81,7 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
92
81
|
const { select } = await import("@inquirer/prompts");
|
|
93
82
|
const chosen = await select({
|
|
94
83
|
message: "Switch to branch",
|
|
95
|
-
choices:
|
|
84
|
+
choices: choices.map((b) => ({ name: b, value: b })),
|
|
96
85
|
});
|
|
97
86
|
|
|
98
87
|
if (typeof chosen !== "string") {
|