cherrypick-interactive 1.4.2 → 1.5.0
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/cli.js +798 -723
- package/package.json +3 -8
- package/cli.backup.js +0 -193
package/cli.js
CHANGED
|
@@ -1,512 +1,523 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { promises as fsPromises, readFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import inquirer from "inquirer";
|
|
8
|
+
import semver from "semver";
|
|
9
|
+
import simpleGit from "simple-git";
|
|
10
|
+
import updateNotifier from "update-notifier";
|
|
11
|
+
import yargs from "yargs";
|
|
12
|
+
import { hideBin } from "yargs/helpers";
|
|
12
13
|
|
|
13
14
|
const git = simpleGit();
|
|
14
15
|
|
|
15
16
|
const __filename = fileURLToPath(import.meta.url);
|
|
16
17
|
const __dirname = dirname(__filename);
|
|
17
|
-
|
|
18
|
+
|
|
19
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf8"));
|
|
18
20
|
|
|
19
21
|
const notifier = updateNotifier({
|
|
20
|
-
|
|
21
|
-
updateCheckInterval: 0, // 12h
|
|
22
|
+
pkg,
|
|
22
23
|
});
|
|
23
24
|
|
|
24
|
-
// Only print if
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
// Only print if a *real* newer version exists
|
|
26
|
+
const upd = notifier.update;
|
|
27
|
+
if (
|
|
28
|
+
upd &&
|
|
29
|
+
semver.valid(upd.latest) &&
|
|
30
|
+
semver.valid(pkg.version) &&
|
|
31
|
+
semver.gt(upd.latest, pkg.version)
|
|
32
|
+
) {
|
|
33
|
+
const name = pkg.name || "cherrypick-interactive";
|
|
34
|
+
console.log("");
|
|
35
|
+
console.log(chalk.yellow("⚠️ A new version is available"));
|
|
36
|
+
console.log(chalk.gray(` ${name}: ${chalk.red(pkg.version)} → ${chalk.green(upd.latest)}`));
|
|
37
|
+
console.log(chalk.cyan(` Update with: ${chalk.bold(`npm i -g ${name}`)}\n`));
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
const argv = yargs(hideBin(process.argv))
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
41
|
+
.scriptName("cherrypick-interactive")
|
|
42
|
+
.usage("$0 [options]")
|
|
43
|
+
.option("dev", {
|
|
44
|
+
type: "string",
|
|
45
|
+
default: "origin/dev",
|
|
46
|
+
describe: "Source branch (contains commits you want).",
|
|
47
|
+
})
|
|
48
|
+
.option("main", {
|
|
49
|
+
type: "string",
|
|
50
|
+
default: "origin/main",
|
|
51
|
+
describe: "Comparison branch (commits present here will be filtered out).",
|
|
52
|
+
})
|
|
53
|
+
.option("since", {
|
|
54
|
+
type: "string",
|
|
55
|
+
default: "1 week ago",
|
|
56
|
+
describe: 'Time window passed to git --since (e.g. "2 weeks ago", "1 month ago").',
|
|
57
|
+
})
|
|
58
|
+
.option("no-fetch", {
|
|
59
|
+
type: "boolean",
|
|
60
|
+
default: false,
|
|
61
|
+
describe: "Skip 'git fetch --prune'.",
|
|
62
|
+
})
|
|
63
|
+
.option("all-yes", {
|
|
64
|
+
type: "boolean",
|
|
65
|
+
default: false,
|
|
66
|
+
describe: "Non-interactive: cherry-pick ALL missing commits (oldest → newest).",
|
|
67
|
+
})
|
|
68
|
+
.option("dry-run", {
|
|
69
|
+
type: "boolean",
|
|
70
|
+
default: false,
|
|
71
|
+
describe: "Print what would be cherry-picked and exit.",
|
|
72
|
+
})
|
|
73
|
+
.option("semantic-versioning", {
|
|
74
|
+
type: "boolean",
|
|
75
|
+
default: true,
|
|
76
|
+
describe: "Compute next semantic version from selected (or missing) commits.",
|
|
77
|
+
})
|
|
78
|
+
.option("current-version", {
|
|
79
|
+
type: "string",
|
|
80
|
+
describe: "Current version (X.Y.Z). Required when --semantic-versioning is set.",
|
|
81
|
+
})
|
|
82
|
+
.option("create-release", {
|
|
83
|
+
type: "boolean",
|
|
84
|
+
default: true,
|
|
85
|
+
describe:
|
|
86
|
+
"Create a release branch from --main named release/<computed-version> before cherry-picking.",
|
|
87
|
+
})
|
|
88
|
+
.option("push-release", {
|
|
89
|
+
type: "boolean",
|
|
90
|
+
default: true,
|
|
91
|
+
describe: "After creating the release branch, push and set upstream (origin).",
|
|
92
|
+
})
|
|
93
|
+
.option("draft-pr", {
|
|
94
|
+
type: "boolean",
|
|
95
|
+
default: false,
|
|
96
|
+
describe: "Create the release PR as a draft.",
|
|
97
|
+
})
|
|
98
|
+
.option("version-file", {
|
|
99
|
+
type: "string",
|
|
100
|
+
default: "./package.json",
|
|
101
|
+
describe:
|
|
102
|
+
"Path to package.json (read current version; optional replacement for --current-version)",
|
|
103
|
+
})
|
|
104
|
+
.option("version-commit-message", {
|
|
105
|
+
type: "string",
|
|
106
|
+
default: "chore(release): bump version to {{version}}",
|
|
107
|
+
describe: "Commit message template for version bump. Use {{version}} placeholder.",
|
|
108
|
+
})
|
|
109
|
+
.option("semver-ignore", {
|
|
110
|
+
type: "string",
|
|
111
|
+
describe:
|
|
112
|
+
"Comma-separated regex patterns. If a commit message matches any, it will be treated as a chore for semantic versioning.",
|
|
113
|
+
})
|
|
114
|
+
.wrap(200)
|
|
115
|
+
.help()
|
|
116
|
+
.alias("h", "help")
|
|
117
|
+
.alias("v", "version").argv;
|
|
111
118
|
|
|
112
119
|
const log = (...a) => console.log(...a);
|
|
113
120
|
const err = (...a) => console.error(...a);
|
|
114
121
|
|
|
115
122
|
async function gitRaw(args) {
|
|
116
|
-
|
|
117
|
-
|
|
123
|
+
const out = await git.raw(args);
|
|
124
|
+
return out.trim();
|
|
118
125
|
}
|
|
119
126
|
|
|
120
127
|
async function getSubjects(branch) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
128
|
+
const out = await gitRaw(["log", "--no-merges", "--pretty=%s", branch]);
|
|
129
|
+
if (!out) {
|
|
130
|
+
return new Set();
|
|
131
|
+
}
|
|
132
|
+
return new Set(out.split("\n").filter(Boolean));
|
|
126
133
|
}
|
|
127
134
|
|
|
128
135
|
async function getDevCommits(branch, since) {
|
|
129
|
-
|
|
136
|
+
const out = await gitRaw(["log", "--no-merges", `--since=${since}`, "--pretty=%H %s", branch]);
|
|
130
137
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
if (!out) {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
return out.split("\n").map((line) => {
|
|
142
|
+
const firstSpace = line.indexOf(" ");
|
|
143
|
+
const hash = line.slice(0, firstSpace);
|
|
144
|
+
const subject = line.slice(firstSpace + 1);
|
|
145
|
+
return { hash, subject };
|
|
146
|
+
});
|
|
140
147
|
}
|
|
141
148
|
|
|
142
149
|
function filterMissing(devCommits, mainSubjects) {
|
|
143
|
-
|
|
150
|
+
return devCommits.filter(({ subject }) => !mainSubjects.has(subject));
|
|
144
151
|
}
|
|
145
152
|
|
|
146
153
|
async function selectCommitsInteractive(missing) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
154
|
+
const choices = [
|
|
155
|
+
new inquirer.Separator(chalk.gray("── Newest commits ──")),
|
|
156
|
+
...missing.map(({ hash, subject }, idx) => {
|
|
157
|
+
// display-only trim to avoid accidental leading spaces
|
|
158
|
+
const displaySubject = subject.replace(/^[\s\u00A0]+/, "");
|
|
159
|
+
return {
|
|
160
|
+
name: `${chalk.dim(`(${hash.slice(0, 7)})`)} ${displaySubject}`,
|
|
161
|
+
value: hash,
|
|
162
|
+
short: displaySubject,
|
|
163
|
+
idx, // we keep index for oldest→newest ordering later
|
|
164
|
+
};
|
|
165
|
+
}),
|
|
166
|
+
new inquirer.Separator(chalk.gray("── Oldest commits ──")),
|
|
167
|
+
];
|
|
168
|
+
const termHeight = process.stdout.rows || 24; // fallback for non-TTY environments
|
|
169
|
+
|
|
170
|
+
const { selected } = await inquirer.prompt([
|
|
171
|
+
{
|
|
172
|
+
type: "checkbox",
|
|
173
|
+
name: "selected",
|
|
174
|
+
message: `Select commits to cherry-pick (${missing.length} missing):`,
|
|
175
|
+
choices,
|
|
176
|
+
pageSize: Math.max(10, Math.min(termHeight - 5, missing.length)),
|
|
177
|
+
},
|
|
178
|
+
]);
|
|
179
|
+
|
|
180
|
+
return selected;
|
|
174
181
|
}
|
|
175
182
|
|
|
176
183
|
async function handleCherryPickConflict(hash) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
184
|
+
if (!(await isCherryPickInProgress())) {
|
|
185
|
+
return "skipped";
|
|
186
|
+
}
|
|
180
187
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
188
|
+
while (true) {
|
|
189
|
+
err(chalk.red(`\n✖ Cherry-pick has conflicts on ${hash} (${hash.slice(0, 7)}).`));
|
|
190
|
+
await showConflictsList(); // prints conflicted files (if any)
|
|
184
191
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
192
|
+
const { action } = await inquirer.prompt([
|
|
193
|
+
{
|
|
194
|
+
type: "list",
|
|
195
|
+
name: "action",
|
|
196
|
+
message: "Choose how to proceed:",
|
|
197
|
+
choices: [
|
|
198
|
+
{ name: "Skip this commit", value: "skip" },
|
|
199
|
+
{ name: "Resolve conflicts now", value: "resolve" },
|
|
200
|
+
{ name: "Revoke and cancel (abort entire sequence)", value: "abort" },
|
|
201
|
+
],
|
|
202
|
+
},
|
|
203
|
+
]);
|
|
197
204
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
205
|
+
if (action === "skip") {
|
|
206
|
+
await gitRaw(["cherry-pick", "--skip"]);
|
|
207
|
+
log(chalk.yellow(`↷ Skipped commit ${chalk.dim(`(${hash.slice(0, 7)})`)}`));
|
|
208
|
+
return "skipped";
|
|
209
|
+
}
|
|
203
210
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
211
|
+
if (action === "abort") {
|
|
212
|
+
await gitRaw(["cherry-pick", "--abort"]);
|
|
213
|
+
throw new Error("Cherry-pick aborted by user.");
|
|
214
|
+
}
|
|
208
215
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
216
|
+
const res = await conflictsResolutionWizard(hash);
|
|
217
|
+
if (res === "continued" || res === "skipped") {
|
|
218
|
+
return res;
|
|
213
219
|
}
|
|
220
|
+
}
|
|
214
221
|
}
|
|
215
222
|
|
|
216
223
|
async function getConflictedFiles() {
|
|
217
|
-
|
|
218
|
-
|
|
224
|
+
const out = await gitRaw(["diff", "--name-only", "--diff-filter=U"]);
|
|
225
|
+
return out ? out.split("\n").filter(Boolean) : [];
|
|
219
226
|
}
|
|
220
227
|
|
|
221
228
|
async function assertNoUnmerged() {
|
|
222
|
-
|
|
223
|
-
|
|
229
|
+
const files = await getConflictedFiles();
|
|
230
|
+
return files.length === 0;
|
|
224
231
|
}
|
|
225
232
|
|
|
226
233
|
async function isCherryPickInProgress() {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
234
|
+
try {
|
|
235
|
+
const head = await gitRaw(["rev-parse", "-q", "--verify", "CHERRY_PICK_HEAD"]);
|
|
236
|
+
return !!head;
|
|
237
|
+
} catch {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
233
240
|
}
|
|
234
241
|
|
|
235
242
|
async function hasStagedChanges() {
|
|
236
|
-
|
|
237
|
-
|
|
243
|
+
const out = await gitRaw(["diff", "--cached", "--name-only"]);
|
|
244
|
+
return !!out;
|
|
238
245
|
}
|
|
239
246
|
|
|
240
247
|
async function isEmptyCherryPick() {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
248
|
+
if (!(await isCherryPickInProgress())) return false;
|
|
249
|
+
const noUnmerged = await assertNoUnmerged();
|
|
250
|
+
if (!noUnmerged) return false;
|
|
251
|
+
const anyStaged = await hasStagedChanges();
|
|
252
|
+
return !anyStaged;
|
|
246
253
|
}
|
|
247
254
|
|
|
248
255
|
async function runBin(bin, args) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
256
|
+
return new Promise((resolve, reject) => {
|
|
257
|
+
const p = spawn(bin, args, { stdio: "inherit" });
|
|
258
|
+
p.on("error", reject);
|
|
259
|
+
p.on("close", (code) => (code === 0 ? resolve() : reject(new Error(`${bin} exited ${code}`))));
|
|
260
|
+
});
|
|
254
261
|
}
|
|
255
262
|
|
|
256
263
|
async function showConflictsList() {
|
|
257
|
-
|
|
264
|
+
const files = await getConflictedFiles();
|
|
258
265
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
266
|
+
if (!files.length) {
|
|
267
|
+
log(chalk.green("No conflicted files reported by git."));
|
|
268
|
+
return [];
|
|
269
|
+
}
|
|
270
|
+
err(chalk.yellow("Conflicted files:"));
|
|
271
|
+
for (const f of files) {
|
|
272
|
+
err(` - ${f}`);
|
|
273
|
+
}
|
|
274
|
+
return files;
|
|
268
275
|
}
|
|
269
276
|
|
|
270
277
|
async function resolveSingleFileWizard(file) {
|
|
271
|
-
|
|
278
|
+
const { action } = await inquirer.prompt([
|
|
279
|
+
{
|
|
280
|
+
type: "list",
|
|
281
|
+
name: "action",
|
|
282
|
+
message: `How to resolve "${file}"?`,
|
|
283
|
+
choices: [
|
|
284
|
+
{ name: "Use ours (current branch)", value: "ours" },
|
|
285
|
+
{ name: "Use theirs (picked commit)", value: "theirs" },
|
|
286
|
+
{ name: "Open in editor", value: "edit" },
|
|
287
|
+
{ name: "Show diff", value: "diff" },
|
|
288
|
+
{ name: "Mark resolved (stage file)", value: "stage" },
|
|
289
|
+
{ name: "Back", value: "back" },
|
|
290
|
+
],
|
|
291
|
+
},
|
|
292
|
+
]);
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
if (action === "ours") {
|
|
296
|
+
await gitRaw(["checkout", "--ours", file]);
|
|
297
|
+
await git.add([file]);
|
|
298
|
+
log(chalk.green(`✓ Applied "ours" and staged: ${file}`));
|
|
299
|
+
} else if (action === "theirs") {
|
|
300
|
+
await gitRaw(["checkout", "--theirs", file]);
|
|
301
|
+
await git.add([file]);
|
|
302
|
+
log(chalk.green(`✓ Applied "theirs" and staged: ${file}`));
|
|
303
|
+
} else if (action === "edit") {
|
|
304
|
+
const editor = process.env.EDITOR || "vi";
|
|
305
|
+
log(chalk.cyan(`Opening ${file} in ${editor}...`));
|
|
306
|
+
await runBin(editor, [file]);
|
|
307
|
+
// user edits and saves, so now they can stage
|
|
308
|
+
const { stageNow } = await inquirer.prompt([
|
|
272
309
|
{
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
{ name: 'Use ours (current branch)', value: 'ours' },
|
|
278
|
-
{ name: 'Use theirs (picked commit)', value: 'theirs' },
|
|
279
|
-
{ name: 'Open in editor', value: 'edit' },
|
|
280
|
-
{ name: 'Show diff', value: 'diff' },
|
|
281
|
-
{ name: 'Mark resolved (stage file)', value: 'stage' },
|
|
282
|
-
{ name: 'Back', value: 'back' },
|
|
283
|
-
],
|
|
310
|
+
type: "confirm",
|
|
311
|
+
name: "stageNow",
|
|
312
|
+
message: "File edited. Stage it now?",
|
|
313
|
+
default: true,
|
|
284
314
|
},
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
} else if (action === 'edit') {
|
|
297
|
-
const editor = process.env.EDITOR || 'vi';
|
|
298
|
-
log(chalk.cyan(`Opening ${file} in ${editor}...`));
|
|
299
|
-
await runBin(editor, [file]);
|
|
300
|
-
// user edits and saves, so now they can stage
|
|
301
|
-
const { stageNow } = await inquirer.prompt([
|
|
302
|
-
{
|
|
303
|
-
type: 'confirm',
|
|
304
|
-
name: 'stageNow',
|
|
305
|
-
message: 'File edited. Stage it now?',
|
|
306
|
-
default: true,
|
|
307
|
-
},
|
|
308
|
-
]);
|
|
309
|
-
if (stageNow) {
|
|
310
|
-
await git.add([file]);
|
|
311
|
-
log(chalk.green(`✓ Staged: ${file}`));
|
|
312
|
-
}
|
|
313
|
-
} else if (action === 'diff') {
|
|
314
|
-
const d = await gitRaw(['diff', file]);
|
|
315
|
-
err(chalk.gray(`\n--- diff: ${file} ---\n${d}\n--- end diff ---\n`));
|
|
316
|
-
} else if (action === 'stage') {
|
|
317
|
-
await git.add([file]);
|
|
318
|
-
log(chalk.green(`✓ Staged: ${file}`));
|
|
319
|
-
}
|
|
320
|
-
} catch (e) {
|
|
321
|
-
err(chalk.red(`Action failed on ${file}: ${e.message || e}`));
|
|
315
|
+
]);
|
|
316
|
+
if (stageNow) {
|
|
317
|
+
await git.add([file]);
|
|
318
|
+
log(chalk.green(`✓ Staged: ${file}`));
|
|
319
|
+
}
|
|
320
|
+
} else if (action === "diff") {
|
|
321
|
+
const d = await gitRaw(["diff", file]);
|
|
322
|
+
err(chalk.gray(`\n--- diff: ${file} ---\n${d}\n--- end diff ---\n`));
|
|
323
|
+
} else if (action === "stage") {
|
|
324
|
+
await git.add([file]);
|
|
325
|
+
log(chalk.green(`✓ Staged: ${file}`));
|
|
322
326
|
}
|
|
327
|
+
} catch (e) {
|
|
328
|
+
err(chalk.red(`Action failed on ${file}: ${e.message || e}`));
|
|
329
|
+
}
|
|
323
330
|
|
|
324
|
-
|
|
331
|
+
return action;
|
|
325
332
|
}
|
|
326
333
|
|
|
327
334
|
async function conflictsResolutionWizard(hash) {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
]);
|
|
348
|
-
|
|
349
|
-
if (emptyAction === 'skip') {
|
|
350
|
-
await gitRaw(['cherry-pick', '--skip']);
|
|
351
|
-
log(chalk.yellow(`↷ Skipped empty pick ${chalk.dim(`(${hash.slice(0, 7)})`)}`));
|
|
352
|
-
return 'skipped';
|
|
353
|
-
}
|
|
354
|
-
if (emptyAction === 'empty-commit') {
|
|
355
|
-
const subject = await gitRaw(['show', '--format=%s', '-s', hash]);
|
|
356
|
-
await gitRaw(['commit', '--allow-empty', '-m', subject]);
|
|
357
|
-
log(`${chalk.green('✓')} (empty) cherry-picked ${chalk.dim(`(${hash.slice(0, 7)})`)} ${subject}`);
|
|
358
|
-
return 'continued';
|
|
359
|
-
}
|
|
360
|
-
if (emptyAction === 'back') {
|
|
361
|
-
// ← FIX #1: really go back to the conflict menu (do NOT try --continue)
|
|
362
|
-
continue;
|
|
363
|
-
}
|
|
364
|
-
// (re-loop)
|
|
365
|
-
} else {
|
|
366
|
-
try {
|
|
367
|
-
await gitRaw(['cherry-pick', '--continue']);
|
|
368
|
-
const subject = await gitRaw(['show', '--format=%s', '-s', hash]);
|
|
369
|
-
log(`${chalk.green('✓')} cherry-picked ${chalk.dim(`(${hash.slice(0, 7)})`)} ${subject}`);
|
|
370
|
-
return 'continued';
|
|
371
|
-
} catch (e) {
|
|
372
|
-
err(chalk.red('`git cherry-pick --continue` failed:'));
|
|
373
|
-
err(String(e.message || e));
|
|
374
|
-
// fall back to loop
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
const { choice } = await inquirer.prompt([
|
|
380
|
-
{
|
|
381
|
-
type: 'list',
|
|
382
|
-
name: 'choice',
|
|
383
|
-
message: 'Select a file to resolve or a global action:',
|
|
384
|
-
pageSize: Math.min(20, Math.max(8, files.length + 5)),
|
|
385
|
-
choices: [
|
|
386
|
-
...files.map((f) => ({ name: f, value: { type: 'file', file: f } })),
|
|
387
|
-
new inquirer.Separator(chalk.gray('─ Actions ─')),
|
|
388
|
-
{ name: 'Use ours for ALL', value: { type: 'all', action: 'ours-all' } },
|
|
389
|
-
{ name: 'Use theirs for ALL', value: { type: 'all', action: 'theirs-all' } },
|
|
390
|
-
{ name: 'Stage ALL', value: { type: 'all', action: 'stage-all' } },
|
|
391
|
-
{ name: 'Launch mergetool (all)', value: { type: 'all', action: 'mergetool-all' } },
|
|
392
|
-
{
|
|
393
|
-
name: 'Try to continue (run --continue)',
|
|
394
|
-
value: { type: 'global', action: 'continue' },
|
|
395
|
-
},
|
|
396
|
-
{ name: 'Back to main conflict menu', value: { type: 'global', action: 'back' } },
|
|
397
|
-
],
|
|
398
|
-
},
|
|
335
|
+
// Loop until no conflicts remain and continue succeeds
|
|
336
|
+
while (true) {
|
|
337
|
+
const files = await showConflictsList();
|
|
338
|
+
|
|
339
|
+
if (files.length === 0) {
|
|
340
|
+
// If there are no conflicted files, either continue or detect empty pick
|
|
341
|
+
if (await isEmptyCherryPick()) {
|
|
342
|
+
err(chalk.yellow("The previous cherry-pick is now empty."));
|
|
343
|
+
const { emptyAction } = await inquirer.prompt([
|
|
344
|
+
{
|
|
345
|
+
type: "list",
|
|
346
|
+
name: "emptyAction",
|
|
347
|
+
message: "No staged changes for this pick. Choose next step:",
|
|
348
|
+
choices: [
|
|
349
|
+
{ name: "Skip this commit (recommended)", value: "skip" },
|
|
350
|
+
{ name: "Create an empty commit", value: "empty-commit" },
|
|
351
|
+
{ name: "Back to conflict menu", value: "back" },
|
|
352
|
+
],
|
|
353
|
+
},
|
|
399
354
|
]);
|
|
400
355
|
|
|
401
|
-
if (
|
|
402
|
-
|
|
356
|
+
if (emptyAction === "skip") {
|
|
357
|
+
await gitRaw(["cherry-pick", "--skip"]);
|
|
358
|
+
log(chalk.yellow(`↷ Skipped empty pick ${chalk.dim(`(${hash.slice(0, 7)})`)}`));
|
|
359
|
+
return "skipped";
|
|
360
|
+
}
|
|
361
|
+
if (emptyAction === "empty-commit") {
|
|
362
|
+
const subject = await gitRaw(["show", "--format=%s", "-s", hash]);
|
|
363
|
+
await gitRaw(["commit", "--allow-empty", "-m", subject]);
|
|
364
|
+
log(
|
|
365
|
+
`${chalk.green("✓")} (empty) cherry-picked ${chalk.dim(`(${hash.slice(0, 7)})`)} ${subject}`,
|
|
366
|
+
);
|
|
367
|
+
return "continued";
|
|
368
|
+
}
|
|
369
|
+
if (emptyAction === "back") {
|
|
370
|
+
// ← FIX #1: really go back to the conflict menu (do NOT try --continue)
|
|
371
|
+
continue;
|
|
403
372
|
}
|
|
373
|
+
// (re-loop)
|
|
374
|
+
} else {
|
|
375
|
+
try {
|
|
376
|
+
await gitRaw(["cherry-pick", "--continue"]);
|
|
377
|
+
const subject = await gitRaw(["show", "--format=%s", "-s", hash]);
|
|
378
|
+
log(`${chalk.green("✓")} cherry-picked ${chalk.dim(`(${hash.slice(0, 7)})`)} ${subject}`);
|
|
379
|
+
return "continued";
|
|
380
|
+
} catch (e) {
|
|
381
|
+
err(chalk.red("`git cherry-pick --continue` failed:"));
|
|
382
|
+
err(String(e.message || e));
|
|
383
|
+
// fall back to loop
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
404
387
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
388
|
+
const { choice } = await inquirer.prompt([
|
|
389
|
+
{
|
|
390
|
+
type: "list",
|
|
391
|
+
name: "choice",
|
|
392
|
+
message: "Select a file to resolve or a global action:",
|
|
393
|
+
pageSize: Math.min(20, Math.max(8, files.length + 5)),
|
|
394
|
+
choices: [
|
|
395
|
+
...files.map((f) => ({ name: f, value: { type: "file", file: f } })),
|
|
396
|
+
new inquirer.Separator(chalk.gray("─ Actions ─")),
|
|
397
|
+
{ name: "Use ours for ALL", value: { type: "all", action: "ours-all" } },
|
|
398
|
+
{ name: "Use theirs for ALL", value: { type: "all", action: "theirs-all" } },
|
|
399
|
+
{ name: "Stage ALL", value: { type: "all", action: "stage-all" } },
|
|
400
|
+
{ name: "Launch mergetool (all)", value: { type: "all", action: "mergetool-all" } },
|
|
401
|
+
{
|
|
402
|
+
name: "Try to continue (run --continue)",
|
|
403
|
+
value: { type: "global", action: "continue" },
|
|
404
|
+
},
|
|
405
|
+
{ name: "Back to main conflict menu", value: { type: "global", action: "back" } },
|
|
406
|
+
],
|
|
407
|
+
},
|
|
408
|
+
]);
|
|
409
|
+
|
|
410
|
+
if (!choice) {
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (choice.type === "file") {
|
|
415
|
+
await resolveSingleFileWizard(choice.file);
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (choice.type === "all") {
|
|
420
|
+
for (const f of files) {
|
|
421
|
+
if (choice.action === "ours-all") {
|
|
422
|
+
await gitRaw(["checkout", "--ours", f]);
|
|
423
|
+
await git.add([f]);
|
|
424
|
+
} else if (choice.action === "theirs-all") {
|
|
425
|
+
await gitRaw(["checkout", "--theirs", f]);
|
|
426
|
+
await git.add([f]);
|
|
427
|
+
} else if (choice.action === "stage-all") {
|
|
428
|
+
await git.add([f]);
|
|
429
|
+
} else if (choice.action === "mergetool-all") {
|
|
430
|
+
await runBin("git", ["mergetool"]);
|
|
431
|
+
break; // mergetool all opens sequentially; re-loop to re-check state
|
|
408
432
|
}
|
|
433
|
+
}
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
409
436
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
}
|
|
424
|
-
|
|
437
|
+
if (choice.type === "global" && choice.action === "continue") {
|
|
438
|
+
if (await assertNoUnmerged()) {
|
|
439
|
+
// If nothing is staged, treat as empty pick and prompt
|
|
440
|
+
if (!(await hasStagedChanges())) {
|
|
441
|
+
err(chalk.yellow("No staged changes found for this cherry-pick."));
|
|
442
|
+
const { emptyAction } = await inquirer.prompt([
|
|
443
|
+
{
|
|
444
|
+
type: "list",
|
|
445
|
+
name: "emptyAction",
|
|
446
|
+
message: "This pick seems empty. Choose next step:",
|
|
447
|
+
choices: [
|
|
448
|
+
{ name: "Skip this commit", value: "skip" },
|
|
449
|
+
{ name: "Create empty commit", value: "empty-commit" },
|
|
450
|
+
{ name: "Back", value: "back" },
|
|
451
|
+
],
|
|
452
|
+
},
|
|
453
|
+
]);
|
|
454
|
+
|
|
455
|
+
if (emptyAction === "skip") {
|
|
456
|
+
await gitRaw(["cherry-pick", "--skip"]);
|
|
457
|
+
log(chalk.yellow(`↷ Skipped empty pick ${chalk.dim(`(${hash.slice(0, 7)})`)}`));
|
|
458
|
+
return "skipped";
|
|
459
|
+
}
|
|
460
|
+
if (emptyAction === "empty-commit") {
|
|
461
|
+
const subject = await gitRaw(["show", "--format=%s", "-s", hash]);
|
|
462
|
+
await gitRaw(["commit", "--allow-empty", "-m", subject]);
|
|
463
|
+
log(
|
|
464
|
+
`${chalk.green("✓")} (empty) cherry-picked ${chalk.dim(`(${hash.slice(0, 7)})`)} ${subject}`,
|
|
465
|
+
);
|
|
466
|
+
return "continued";
|
|
467
|
+
}
|
|
468
|
+
if (emptyAction === "back") {
|
|
469
|
+
// ← FIX #2: actually go back to the conflict menu; do NOT try --continue
|
|
425
470
|
continue;
|
|
471
|
+
}
|
|
426
472
|
}
|
|
427
473
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
type: 'list',
|
|
436
|
-
name: 'emptyAction',
|
|
437
|
-
message: 'This pick seems empty. Choose next step:',
|
|
438
|
-
choices: [
|
|
439
|
-
{ name: 'Skip this commit', value: 'skip' },
|
|
440
|
-
{ name: 'Create empty commit', value: 'empty-commit' },
|
|
441
|
-
{ name: 'Back', value: 'back' },
|
|
442
|
-
],
|
|
443
|
-
},
|
|
444
|
-
]);
|
|
445
|
-
|
|
446
|
-
if (emptyAction === 'skip') {
|
|
447
|
-
await gitRaw(['cherry-pick', '--skip']);
|
|
448
|
-
log(chalk.yellow(`↷ Skipped empty pick ${chalk.dim(`(${hash.slice(0, 7)})`)}`));
|
|
449
|
-
return 'skipped';
|
|
450
|
-
}
|
|
451
|
-
if (emptyAction === 'empty-commit') {
|
|
452
|
-
const subject = await gitRaw(['show', '--format=%s', '-s', hash]);
|
|
453
|
-
await gitRaw(['commit', '--allow-empty', '-m', subject]);
|
|
454
|
-
log(`${chalk.green('✓')} (empty) cherry-picked ${chalk.dim(`(${hash.slice(0, 7)})`)} ${subject}`);
|
|
455
|
-
return 'continued';
|
|
456
|
-
}
|
|
457
|
-
if (emptyAction === 'back') {
|
|
458
|
-
// ← FIX #2: actually go back to the conflict menu; do NOT try --continue
|
|
459
|
-
continue;
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
try {
|
|
464
|
-
await gitRaw(['cherry-pick', '--continue']);
|
|
465
|
-
const subject = await gitRaw(['show', '--format=%s', '-s', hash]);
|
|
466
|
-
log(`${chalk.green('✓')} cherry-picked ${chalk.dim(`(${hash.slice(0, 7)})`)} ${subject}`);
|
|
467
|
-
return 'continued';
|
|
468
|
-
} catch (e) {
|
|
469
|
-
err(chalk.red('`--continue` failed. Resolve remaining issues and try again.'));
|
|
470
|
-
}
|
|
471
|
-
} else {
|
|
472
|
-
err(chalk.yellow('There are still unmerged files.'));
|
|
473
|
-
}
|
|
474
|
+
try {
|
|
475
|
+
await gitRaw(["cherry-pick", "--continue"]);
|
|
476
|
+
const subject = await gitRaw(["show", "--format=%s", "-s", hash]);
|
|
477
|
+
log(`${chalk.green("✓")} cherry-picked ${chalk.dim(`(${hash.slice(0, 7)})`)} ${subject}`);
|
|
478
|
+
return "continued";
|
|
479
|
+
} catch (e) {
|
|
480
|
+
err(chalk.red("`--continue` failed. Resolve remaining issues and try again."));
|
|
474
481
|
}
|
|
482
|
+
} else {
|
|
483
|
+
err(chalk.yellow("There are still unmerged files."));
|
|
484
|
+
}
|
|
485
|
+
}
|
|
475
486
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
}
|
|
487
|
+
if (choice.type === "global" && choice.action === "back") {
|
|
488
|
+
return "back";
|
|
479
489
|
}
|
|
490
|
+
}
|
|
480
491
|
}
|
|
481
492
|
|
|
482
493
|
async function cherryPickSequential(hashes) {
|
|
483
|
-
|
|
494
|
+
const result = { applied: 0, skipped: 0 };
|
|
484
495
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
}
|
|
502
|
-
} catch (abortErr) {
|
|
503
|
-
err(chalk.red(`✖ Cherry-pick aborted on ${hash}`));
|
|
504
|
-
throw abortErr;
|
|
505
|
-
}
|
|
496
|
+
for (const hash of hashes) {
|
|
497
|
+
try {
|
|
498
|
+
await gitRaw(["cherry-pick", hash]);
|
|
499
|
+
const subject = await gitRaw(["show", "--format=%s", "-s", hash]);
|
|
500
|
+
log(`${chalk.green("✓")} cherry-picked ${chalk.dim(`(${hash.slice(0, 7)})`)} ${subject}`);
|
|
501
|
+
result.applied += 1;
|
|
502
|
+
} catch (e) {
|
|
503
|
+
try {
|
|
504
|
+
const action = await handleCherryPickConflict(hash);
|
|
505
|
+
if (action === "skipped") {
|
|
506
|
+
result.skipped += 1;
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
if (action === "continued") {
|
|
510
|
+
// --continue başarıyla commit oluşturdu
|
|
511
|
+
result.applied += 1;
|
|
506
512
|
}
|
|
513
|
+
} catch (abortErr) {
|
|
514
|
+
err(chalk.red(`✖ Cherry-pick aborted on ${hash}`));
|
|
515
|
+
throw abortErr;
|
|
516
|
+
}
|
|
507
517
|
}
|
|
518
|
+
}
|
|
508
519
|
|
|
509
|
-
|
|
520
|
+
return result;
|
|
510
521
|
}
|
|
511
522
|
|
|
512
523
|
/**
|
|
@@ -514,271 +525,282 @@ async function cherryPickSequential(hashes) {
|
|
|
514
525
|
* @returns {Promise<void>}
|
|
515
526
|
*/
|
|
516
527
|
function parseVersion(v) {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
528
|
+
const m = String(v || "")
|
|
529
|
+
.trim()
|
|
530
|
+
.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
531
|
+
if (!m) {
|
|
532
|
+
throw new Error(`Invalid --current-version "${v}". Expected X.Y.Z`);
|
|
533
|
+
}
|
|
534
|
+
return { major: +m[1], minor: +m[2], patch: +m[3] };
|
|
524
535
|
}
|
|
525
536
|
|
|
526
537
|
function incrementVersion(version, bump) {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
+
const cur = parseVersion(version);
|
|
539
|
+
if (bump === "major") {
|
|
540
|
+
return `${cur.major + 1}.0.0`;
|
|
541
|
+
}
|
|
542
|
+
if (bump === "minor") {
|
|
543
|
+
return `${cur.major}.${cur.minor + 1}.0`;
|
|
544
|
+
}
|
|
545
|
+
if (bump === "patch") {
|
|
546
|
+
return `${cur.major}.${cur.minor}.${cur.patch + 1}`;
|
|
547
|
+
}
|
|
548
|
+
return `${cur.major}.${cur.minor}.${cur.patch}`;
|
|
538
549
|
}
|
|
539
550
|
|
|
540
551
|
function normalizeMessage(msg) {
|
|
541
|
-
|
|
542
|
-
|
|
552
|
+
// normalize whitespace; keep case-insensitive matching
|
|
553
|
+
return (msg || "").replace(/\r\n/g, "\n");
|
|
543
554
|
}
|
|
544
555
|
|
|
545
556
|
// Returns "major" | "minor" | "patch" | null for a single commit message
|
|
546
557
|
function classifySingleCommit(messageBody) {
|
|
547
|
-
|
|
558
|
+
const body = normalizeMessage(messageBody);
|
|
548
559
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
560
|
+
// Major
|
|
561
|
+
if (/\bBREAKING[- _]CHANGE(?:\([^)]+\))?\s*:?/i.test(body)) {
|
|
562
|
+
return "major";
|
|
563
|
+
}
|
|
553
564
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
565
|
+
// Minor
|
|
566
|
+
if (/(^|\n)\s*(\*?\s*)?feat(?:\([^)]+\))?\s*:?/i.test(body)) {
|
|
567
|
+
return "minor";
|
|
568
|
+
}
|
|
558
569
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
570
|
+
// Patch
|
|
571
|
+
if (/(^|\n)\s*(\*?\s*)?(fix|perf)(?:\([^)]+\))?\s*:?/i.test(body)) {
|
|
572
|
+
return "patch";
|
|
573
|
+
}
|
|
563
574
|
|
|
564
|
-
|
|
575
|
+
return null;
|
|
565
576
|
}
|
|
566
577
|
|
|
567
578
|
// Given many commits, collapse to a single bump level
|
|
568
579
|
function collapseBumps(levels) {
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
580
|
+
if (levels.includes("major")) {
|
|
581
|
+
return "major";
|
|
582
|
+
}
|
|
583
|
+
if (levels.includes("minor")) {
|
|
584
|
+
return "minor";
|
|
585
|
+
}
|
|
586
|
+
if (levels.includes("patch")) {
|
|
587
|
+
return "patch";
|
|
588
|
+
}
|
|
589
|
+
return null;
|
|
579
590
|
}
|
|
580
591
|
|
|
581
592
|
// Fetch full commit messages (%B) for SHAs and compute bump
|
|
582
593
|
async function computeSemanticBumpForCommits(hashes, gitRawFn, semverignore) {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
const levels = [];
|
|
594
|
+
if (!hashes.length) {
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
588
597
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
const subject = msg.split(/\r?\n/)[0].trim();
|
|
592
|
-
let level = classifySingleCommit(msg);
|
|
598
|
+
const levels = [];
|
|
599
|
+
const semverIgnorePatterns = parseSemverIgnore(semverignore);
|
|
593
600
|
|
|
594
|
-
|
|
595
|
-
|
|
601
|
+
for (const h of hashes) {
|
|
602
|
+
const msg = await gitRawFn(["show", "--format=%B", "-s", h]);
|
|
603
|
+
const subject = msg.split(/\r?\n/)[0].trim();
|
|
604
|
+
let level = classifySingleCommit(msg);
|
|
596
605
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
level = null;
|
|
602
|
-
}
|
|
603
|
-
}
|
|
606
|
+
// 🔹 Apply --semverignore (treat matched commits as chores). Use full message (subject + body).
|
|
607
|
+
if (semverIgnorePatterns.length > 0 && matchesAnyPattern(msg, semverIgnorePatterns)) {
|
|
608
|
+
level = null;
|
|
609
|
+
}
|
|
604
610
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
}
|
|
611
|
+
if (level) {
|
|
612
|
+
levels.push(level);
|
|
613
|
+
if (level === "major") break; // early exit if major is found
|
|
609
614
|
}
|
|
615
|
+
}
|
|
610
616
|
|
|
611
|
-
|
|
617
|
+
return collapseBumps(levels);
|
|
612
618
|
}
|
|
613
619
|
|
|
614
620
|
async function main() {
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
const currentBranch = (await gitRaw(['rev-parse', '--abbrev-ref', 'HEAD'])) || 'HEAD';
|
|
622
|
-
|
|
623
|
-
log(chalk.gray(`Comparing subjects since ${argv.since}`));
|
|
624
|
-
log(chalk.gray(`Dev: ${argv.dev}`));
|
|
625
|
-
log(chalk.gray(`Main: ${argv.main}`));
|
|
626
|
-
|
|
627
|
-
const [devCommits, mainSubjects] = await Promise.all([getDevCommits(argv.dev, argv.since), getSubjects(argv.main)]);
|
|
628
|
-
|
|
629
|
-
const missing = filterMissing(devCommits, mainSubjects);
|
|
630
|
-
|
|
631
|
-
if (missing.length === 0) {
|
|
632
|
-
log(chalk.green('✅ No missing commits found in the selected window.'));
|
|
633
|
-
return;
|
|
634
|
-
}
|
|
621
|
+
try {
|
|
622
|
+
if (!argv["no-fetch"]) {
|
|
623
|
+
log(chalk.gray("Fetching remotes (git fetch --prune)..."));
|
|
624
|
+
await git.fetch(["--prune"]);
|
|
625
|
+
}
|
|
635
626
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
let selected;
|
|
639
|
-
if (argv['all-yes']) {
|
|
640
|
-
selected = missing.map((m) => m.hash);
|
|
641
|
-
} else {
|
|
642
|
-
selected = await selectCommitsInteractive(missing);
|
|
643
|
-
if (!selected.length) {
|
|
644
|
-
log(chalk.yellow('No commits selected. Exiting.'));
|
|
645
|
-
return;
|
|
646
|
-
}
|
|
647
|
-
}
|
|
627
|
+
const currentBranch = (await gitRaw(["rev-parse", "--abbrev-ref", "HEAD"])) || "HEAD";
|
|
648
628
|
|
|
649
|
-
|
|
629
|
+
log(chalk.gray(`Comparing subjects since ${argv.since}`));
|
|
630
|
+
log(chalk.gray(`Dev: ${argv.dev}`));
|
|
631
|
+
log(chalk.gray(`Main: ${argv.main}`));
|
|
650
632
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
log(`- ${chalk.dim(`(${h.slice(0, 7)})`)} ${subj}`);
|
|
656
|
-
}
|
|
657
|
-
return;
|
|
658
|
-
}
|
|
633
|
+
const [devCommits, mainSubjects] = await Promise.all([
|
|
634
|
+
getDevCommits(argv.dev, argv.since),
|
|
635
|
+
getSubjects(argv.main),
|
|
636
|
+
]);
|
|
659
637
|
|
|
660
|
-
|
|
661
|
-
const currentVersionFromPkg = await getPkgVersion(argv['version-file']);
|
|
662
|
-
argv['current-version'] = currentVersionFromPkg;
|
|
663
|
-
}
|
|
638
|
+
const missing = filterMissing(devCommits, mainSubjects);
|
|
664
639
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
}
|
|
640
|
+
if (missing.length === 0) {
|
|
641
|
+
log(chalk.green("✅ No missing commits found in the selected window."));
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
670
644
|
|
|
671
|
-
|
|
672
|
-
|
|
645
|
+
const semverIgnore = argv["semver-ignore"];
|
|
646
|
+
const indexByHash = new Map(missing.map((c, i) => [c.hash, i])); // 0=newest, larger=older
|
|
647
|
+
|
|
648
|
+
let selected;
|
|
649
|
+
if (argv["all-yes"]) {
|
|
650
|
+
selected = missing.map((m) => m.hash);
|
|
651
|
+
} else {
|
|
652
|
+
selected = await selectCommitsInteractive(missing);
|
|
653
|
+
if (!selected.length) {
|
|
654
|
+
log(chalk.yellow("No commits selected. Exiting."));
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
673
658
|
|
|
674
|
-
|
|
659
|
+
const bottomToTop = [...selected].sort((a, b) => indexByHash.get(b) - indexByHash.get(a));
|
|
675
660
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
if (argv['create-release']) {
|
|
686
|
-
if (!argv['semantic-versioning'] || !argv['current-version']) {
|
|
687
|
-
throw new Error(' --create-release requires --semantic-versioning and --current-version X.Y.Z');
|
|
688
|
-
}
|
|
689
|
-
if (!computedNextVersion) {
|
|
690
|
-
throw new Error('Unable to determine release version. Check semantic-versioning inputs.');
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
const releaseBranch = `release/${computedNextVersion}`;
|
|
694
|
-
await ensureBranchDoesNotExistLocally(releaseBranch);
|
|
695
|
-
const startPoint = argv.main; // e.g., 'origin/main' or a local ref
|
|
696
|
-
|
|
697
|
-
const changelogBody = await buildChangelogBody({
|
|
698
|
-
version: computedNextVersion,
|
|
699
|
-
hashes: bottomToTop,
|
|
700
|
-
gitRawFn: gitRaw,
|
|
701
|
-
semverIgnore: argv.semverignore, // raw flag value
|
|
702
|
-
});
|
|
703
|
-
|
|
704
|
-
await fsPromises.writeFile('RELEASE_CHANGELOG.md', changelogBody, 'utf8');
|
|
705
|
-
await gitRaw(['reset', 'RELEASE_CHANGELOG.md']);
|
|
706
|
-
log(chalk.gray(`✅ Generated changelog for ${releaseBranch} → RELEASE_CHANGELOG.md`));
|
|
707
|
-
|
|
708
|
-
log(chalk.cyan(`\nCreating ${chalk.bold(releaseBranch)} from ${chalk.bold(startPoint)}...`));
|
|
709
|
-
|
|
710
|
-
await git.checkoutBranch(releaseBranch, startPoint);
|
|
711
|
-
|
|
712
|
-
log(chalk.green(`✓ Ready on ${chalk.bold(releaseBranch)}. Cherry-picking will apply here.`));
|
|
713
|
-
} else {
|
|
714
|
-
// otherwise we stay on the current branch
|
|
715
|
-
log(chalk.bold(`Base branch: ${currentBranch}`));
|
|
716
|
-
}
|
|
661
|
+
if (argv.dry_run || argv["dry-run"]) {
|
|
662
|
+
log(chalk.cyan("\n--dry-run: would cherry-pick (oldest → newest):"));
|
|
663
|
+
for (const h of bottomToTop) {
|
|
664
|
+
const subj = await gitRaw(["show", "--format=%s", "-s", h]);
|
|
665
|
+
log(`- ${chalk.dim(`(${h.slice(0, 7)})`)} ${subj}`);
|
|
666
|
+
}
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
717
669
|
|
|
718
|
-
|
|
670
|
+
if (argv["version-file"] && !argv["current-version"]) {
|
|
671
|
+
const currentVersionFromPkg = await getPkgVersion(argv["version-file"]);
|
|
672
|
+
argv["current-version"] = currentVersionFromPkg;
|
|
673
|
+
}
|
|
719
674
|
|
|
720
|
-
|
|
675
|
+
let computedNextVersion = argv["current-version"];
|
|
676
|
+
if (argv["semantic-versioning"]) {
|
|
677
|
+
if (!argv["current-version"]) {
|
|
678
|
+
throw new Error(
|
|
679
|
+
" --semantic-versioning requires --current-version X.Y.Z (or pass --version-file)",
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Bump is based on the commits you are about to apply (selected).
|
|
684
|
+
const bump = await computeSemanticBumpForCommits(bottomToTop, gitRaw, semverIgnore);
|
|
685
|
+
|
|
686
|
+
computedNextVersion = bump
|
|
687
|
+
? incrementVersion(argv["current-version"], bump)
|
|
688
|
+
: argv["current-version"];
|
|
689
|
+
|
|
690
|
+
log("");
|
|
691
|
+
log(chalk.magenta("Semantic Versioning"));
|
|
692
|
+
log(
|
|
693
|
+
` Current: ${chalk.bold(argv["current-version"])} ` +
|
|
694
|
+
`Detected bump: ${chalk.bold(bump || "none")} ` +
|
|
695
|
+
`Next: ${chalk.bold(computedNextVersion)}`,
|
|
696
|
+
);
|
|
697
|
+
}
|
|
721
698
|
|
|
722
|
-
|
|
699
|
+
if (argv["create-release"]) {
|
|
700
|
+
if (!argv["semantic-versioning"] || !argv["current-version"]) {
|
|
701
|
+
throw new Error(
|
|
702
|
+
" --create-release requires --semantic-versioning and --current-version X.Y.Z",
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
if (!computedNextVersion) {
|
|
706
|
+
throw new Error("Unable to determine release version. Check semantic-versioning inputs.");
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const releaseBranch = `release/${computedNextVersion}`;
|
|
710
|
+
const startPoint = argv.main; // e.g., 'origin/main' or a local ref
|
|
711
|
+
await ensureReleaseBranchFresh(releaseBranch, startPoint);
|
|
712
|
+
|
|
713
|
+
const changelogBody = await buildChangelogBody({
|
|
714
|
+
version: computedNextVersion,
|
|
715
|
+
hashes: bottomToTop,
|
|
716
|
+
gitRawFn: gitRaw,
|
|
717
|
+
semverIgnore, // raw flag value
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
await fsPromises.writeFile("RELEASE_CHANGELOG.md", changelogBody, "utf8");
|
|
721
|
+
await gitRaw(["reset", "RELEASE_CHANGELOG.md"]);
|
|
722
|
+
log(chalk.gray(`✅ Generated changelog for ${releaseBranch} → RELEASE_CHANGELOG.md`));
|
|
723
|
+
|
|
724
|
+
log(chalk.cyan(`\nCreating ${chalk.bold(releaseBranch)} from ${chalk.bold(startPoint)}...`));
|
|
725
|
+
|
|
726
|
+
await git.checkoutBranch(releaseBranch, startPoint);
|
|
727
|
+
|
|
728
|
+
log(chalk.green(`✓ Ready on ${chalk.bold(releaseBranch)}. Cherry-picking will apply here.`));
|
|
729
|
+
} else {
|
|
730
|
+
// otherwise we stay on the current branch
|
|
731
|
+
log(chalk.bold(`Base branch: ${currentBranch}`));
|
|
732
|
+
}
|
|
723
733
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
734
|
+
log(
|
|
735
|
+
chalk.cyan(
|
|
736
|
+
`\nCherry-picking ${bottomToTop.length} commit(s) onto ${currentBranch} (oldest → newest)...\n`,
|
|
737
|
+
),
|
|
738
|
+
);
|
|
739
|
+
|
|
740
|
+
const stats = await cherryPickSequential(bottomToTop);
|
|
741
|
+
|
|
742
|
+
log(chalk.gray(`\nSummary → applied: ${stats.applied}, skipped: ${stats.skipped}`));
|
|
743
|
+
|
|
744
|
+
if (stats.applied === 0) {
|
|
745
|
+
err(
|
|
746
|
+
chalk.yellow("\nNo commits were cherry-picked (all were skipped or unresolved). Aborting."),
|
|
747
|
+
);
|
|
748
|
+
// Abort any leftover state just in case
|
|
749
|
+
try {
|
|
750
|
+
await gitRaw(["cherry-pick", "--abort"]);
|
|
751
|
+
} catch {}
|
|
752
|
+
throw new Error("Nothing cherry-picked");
|
|
753
|
+
}
|
|
732
754
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
755
|
+
if (argv["push-release"]) {
|
|
756
|
+
const baseBranchForGh = stripOrigin(argv.main); // 'origin/main' -> 'main'
|
|
757
|
+
const prTitle = `Release ${computedNextVersion}`;
|
|
758
|
+
const releaseBranch = `release/${computedNextVersion}`;
|
|
759
|
+
|
|
760
|
+
const onBranch = await gitRaw(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
761
|
+
if (!onBranch.startsWith(releaseBranch)) {
|
|
762
|
+
throw new Error(`Version update should happen on a release branch. Current: ${onBranch}`);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
log(chalk.cyan(`\nUpdating ${argv["version-file"]} version → ${computedNextVersion} ...`));
|
|
766
|
+
await setPkgVersion(argv["version-file"], computedNextVersion);
|
|
767
|
+
await git.add([argv["version-file"]]);
|
|
768
|
+
const msg = argv["version-commit-message"].replace("{{version}}", computedNextVersion);
|
|
769
|
+
await git.raw(["commit", "--no-verify", "-m", msg]);
|
|
770
|
+
|
|
771
|
+
log(chalk.green(`✓ package.json updated and committed: ${msg}`));
|
|
772
|
+
|
|
773
|
+
await git.push(["-u", "origin", releaseBranch, "--no-verify"]);
|
|
774
|
+
|
|
775
|
+
const ghArgs = [
|
|
776
|
+
"pr",
|
|
777
|
+
"create",
|
|
778
|
+
"--base",
|
|
779
|
+
baseBranchForGh,
|
|
780
|
+
"--head",
|
|
781
|
+
releaseBranch,
|
|
782
|
+
"--title",
|
|
783
|
+
prTitle,
|
|
784
|
+
"--body-file",
|
|
785
|
+
"RELEASE_CHANGELOG.md",
|
|
786
|
+
];
|
|
787
|
+
if (argv["draft-pr"]) {
|
|
788
|
+
ghArgs.push("--draft");
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
await runGh(ghArgs);
|
|
792
|
+
log(chalk.gray(`Pushed ${onBranch} with version bump.`));
|
|
793
|
+
}
|
|
772
794
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
795
|
+
const finalBranch = argv["create-release"]
|
|
796
|
+
? await gitRaw(["rev-parse", "--abbrev-ref", "HEAD"]) // should be release/*
|
|
797
|
+
: currentBranch;
|
|
776
798
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
799
|
+
log(chalk.green(`\n✅ Done on ${finalBranch}`));
|
|
800
|
+
} catch (e) {
|
|
801
|
+
err(chalk.red(`\n❌ Error: ${e.message || e}`));
|
|
802
|
+
process.exit(1);
|
|
803
|
+
}
|
|
782
804
|
}
|
|
783
805
|
|
|
784
806
|
main();
|
|
@@ -787,129 +809,182 @@ main();
|
|
|
787
809
|
* Utils
|
|
788
810
|
*/
|
|
789
811
|
|
|
790
|
-
async function
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
812
|
+
async function ensureReleaseBranchFresh(branchName, startPoint) {
|
|
813
|
+
const branches = await git.branchLocal();
|
|
814
|
+
const localExists = branches.all.includes(branchName);
|
|
815
|
+
const remoteRef = await gitRaw(["ls-remote", "--heads", "origin", branchName]);
|
|
816
|
+
const remoteExists = Boolean(remoteRef);
|
|
817
|
+
|
|
818
|
+
if (!localExists && !remoteExists) {
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const { action } = await inquirer.prompt([
|
|
823
|
+
{
|
|
824
|
+
type: "list",
|
|
825
|
+
name: "action",
|
|
826
|
+
message: `Release branch "${branchName}" already exists${localExists ? " locally" : ""}${remoteExists ? " on origin" : ""}. How do you want to proceed? (override, abort)`,
|
|
827
|
+
choices: [
|
|
828
|
+
{ name: "Override (delete existing branch and recreate)", value: "override" },
|
|
829
|
+
{ name: "Abort", value: "abort" },
|
|
830
|
+
],
|
|
831
|
+
},
|
|
832
|
+
]);
|
|
833
|
+
|
|
834
|
+
if (action === "abort") {
|
|
835
|
+
throw new Error(`Aborted: release branch "${branchName}" already exists.`);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Ensure we are not on the branch before deleting local copy
|
|
839
|
+
if (localExists) {
|
|
840
|
+
if (branches.current === branchName) {
|
|
841
|
+
const target = startPoint || "HEAD";
|
|
842
|
+
log(chalk.gray(`Switching to ${target} before deleting ${branchName}...`));
|
|
843
|
+
await gitRaw(["checkout", target]);
|
|
844
|
+
}
|
|
845
|
+
await gitRaw(["branch", "-D", branchName]);
|
|
846
|
+
log(chalk.yellow(`↷ Deleted existing local branch ${branchName}`));
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
if (remoteExists) {
|
|
850
|
+
try {
|
|
851
|
+
await gitRaw(["push", "origin", "--delete", branchName]);
|
|
852
|
+
log(chalk.yellow(`↷ Deleted existing remote branch origin/${branchName}`));
|
|
853
|
+
} catch (e) {
|
|
854
|
+
err(chalk.red(`Failed to delete remote branch origin/${branchName}: ${e.message || e}`));
|
|
855
|
+
throw e;
|
|
796
856
|
}
|
|
857
|
+
}
|
|
797
858
|
}
|
|
798
859
|
|
|
799
860
|
async function buildChangelogBody({ version, hashes, gitRawFn, semverIgnore }) {
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
861
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
862
|
+
const header = version ? `## Release ${version} — ${today}` : `## Release — ${today}`;
|
|
863
|
+
const semverIgnorePatterns = parseSemverIgnore(semverIgnore);
|
|
803
864
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
865
|
+
const breakings = [];
|
|
866
|
+
const features = [];
|
|
867
|
+
const fixes = [];
|
|
868
|
+
const others = [];
|
|
808
869
|
|
|
809
|
-
|
|
810
|
-
|
|
870
|
+
for (const h of hashes) {
|
|
871
|
+
const msg = await gitRawFn(["show", "--format=%B", "-s", h]);
|
|
811
872
|
|
|
812
|
-
|
|
813
|
-
|
|
873
|
+
const subject = msg.split(/\r?\n/)[0].trim(); // first line of commit message
|
|
874
|
+
const shaDisplay = shortSha(h);
|
|
814
875
|
|
|
815
|
-
|
|
816
|
-
|
|
876
|
+
// normal classification first
|
|
877
|
+
let level = classifySingleCommit(msg);
|
|
817
878
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
switch (level) {
|
|
825
|
-
case 'major':
|
|
826
|
-
breakings.push(`${shaDisplay} ${subject}`);
|
|
827
|
-
break;
|
|
828
|
-
case 'minor':
|
|
829
|
-
features.push(`${shaDisplay} ${subject}`);
|
|
830
|
-
break;
|
|
831
|
-
case 'patch':
|
|
832
|
-
fixes.push(`${shaDisplay} ${subject}`);
|
|
833
|
-
break;
|
|
834
|
-
default:
|
|
835
|
-
others.push(`${shaDisplay} ${subject}`);
|
|
836
|
-
break;
|
|
837
|
-
}
|
|
879
|
+
// ⬇ Apply semver-ignore logic
|
|
880
|
+
const matched = matchesAnyPattern(msg, semverIgnorePatterns); // evaluate against full message
|
|
881
|
+
if (matched) {
|
|
882
|
+
level = null; // drop it into "Other"
|
|
838
883
|
}
|
|
839
884
|
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
885
|
+
switch (level) {
|
|
886
|
+
case "major":
|
|
887
|
+
breakings.push(`${shaDisplay} ${subject}`);
|
|
888
|
+
break;
|
|
889
|
+
case "minor":
|
|
890
|
+
features.push(`${shaDisplay} ${subject}`);
|
|
891
|
+
break;
|
|
892
|
+
case "patch":
|
|
893
|
+
fixes.push(`${shaDisplay} ${subject}`);
|
|
894
|
+
break;
|
|
895
|
+
default:
|
|
896
|
+
others.push(`${shaDisplay} ${subject}`);
|
|
897
|
+
break;
|
|
846
898
|
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
const sections = [];
|
|
902
|
+
if (breakings.length) {
|
|
903
|
+
sections.push(`### ✨ Breaking Changes\n${breakings.join("\n")}`);
|
|
904
|
+
}
|
|
905
|
+
if (features.length) {
|
|
906
|
+
sections.push(`### ✨ Features\n${features.join("\n")}`);
|
|
907
|
+
}
|
|
908
|
+
if (fixes.length) {
|
|
909
|
+
sections.push(`### 🐛 Fixes\n${fixes.join("\n")}`);
|
|
910
|
+
}
|
|
911
|
+
if (others.length) {
|
|
912
|
+
sections.push(`### 🧹 Others\n${others.join("\n")}`);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
return `${header}\n\n${sections.join("\n\n")}\n`;
|
|
855
916
|
}
|
|
856
917
|
function shortSha(sha) {
|
|
857
|
-
|
|
918
|
+
return String(sha).slice(0, 7);
|
|
858
919
|
}
|
|
859
920
|
|
|
860
921
|
function stripOrigin(ref) {
|
|
861
|
-
|
|
922
|
+
return ref.startsWith("origin/") ? ref.slice("origin/".length) : ref;
|
|
862
923
|
}
|
|
863
924
|
|
|
864
925
|
async function runGh(args) {
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
926
|
+
return new Promise((resolve, reject) => {
|
|
927
|
+
const p = spawn("gh", args, { stdio: "inherit" });
|
|
928
|
+
p.on("error", reject);
|
|
929
|
+
p.on("close", (code) => (code === 0 ? resolve() : reject(new Error(`gh exited ${code}`))));
|
|
930
|
+
});
|
|
870
931
|
}
|
|
871
932
|
async function readJson(filePath) {
|
|
872
|
-
|
|
873
|
-
|
|
933
|
+
const raw = await fsPromises.readFile(filePath, "utf8");
|
|
934
|
+
return JSON.parse(raw);
|
|
874
935
|
}
|
|
875
936
|
|
|
876
937
|
async function writeJson(filePath, data) {
|
|
877
|
-
|
|
878
|
-
|
|
938
|
+
const text = `${JSON.stringify(data, null, 2)}\n`;
|
|
939
|
+
await fsPromises.writeFile(filePath, text, "utf8");
|
|
879
940
|
}
|
|
880
941
|
|
|
881
942
|
/** Read package.json version; throw if missing */
|
|
882
943
|
async function getPkgVersion(pkgPath) {
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
944
|
+
const pkg = await readJson(pkgPath);
|
|
945
|
+
const v = pkg?.version;
|
|
946
|
+
if (!v) {
|
|
947
|
+
throw new Error(`No "version" field found in ${pkgPath}`);
|
|
948
|
+
}
|
|
949
|
+
return v;
|
|
889
950
|
}
|
|
890
951
|
|
|
891
952
|
/** Update package.json version in-place */
|
|
892
953
|
async function setPkgVersion(pkgPath, nextVersion) {
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
954
|
+
const pkg = await readJson(pkgPath);
|
|
955
|
+
pkg.version = nextVersion;
|
|
956
|
+
await writeJson(pkgPath, pkg);
|
|
896
957
|
}
|
|
897
958
|
|
|
898
959
|
function parseSemverIgnore(argvValue) {
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
960
|
+
if (!argvValue) return [];
|
|
961
|
+
return argvValue
|
|
962
|
+
.split(",")
|
|
963
|
+
.map((p) => p.trim())
|
|
964
|
+
.filter(Boolean)
|
|
965
|
+
.map((pattern) => {
|
|
966
|
+
try {
|
|
967
|
+
return new RegExp(pattern, "i"); // case-insensitive on full message
|
|
968
|
+
} catch (e) {
|
|
969
|
+
err(chalk.red(`Invalid --semver-ignore pattern "${pattern}": ${e.message || e}`));
|
|
970
|
+
return null;
|
|
971
|
+
}
|
|
972
|
+
})
|
|
973
|
+
.filter(Boolean);
|
|
904
974
|
}
|
|
905
975
|
|
|
906
976
|
function matchesAnyPattern(text, patterns) {
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
977
|
+
if (!patterns || patterns.length === 0) return false;
|
|
978
|
+
|
|
979
|
+
const matched = patterns.find((rx) => rx.test(text));
|
|
980
|
+
if (matched) {
|
|
981
|
+
log(
|
|
982
|
+
chalk.cyan(
|
|
983
|
+
`↷ Semver ignored (pattern: /${matched.source}/i): ${chalk.dim(`(${text.split("\n")[0]})`)}`,
|
|
984
|
+
),
|
|
985
|
+
);
|
|
986
|
+
return true;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
return false;
|
|
915
990
|
}
|