@errhythm/gitmux 1.6.2
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 +219 -0
- package/bin/gitmux.js +10 -0
- package/package.json +43 -0
- package/src/commands/fetch.js +76 -0
- package/src/commands/mr.js +484 -0
- package/src/commands/portal.js +1711 -0
- package/src/commands/settings.js +490 -0
- package/src/commands/status.js +86 -0
- package/src/commands/switch.js +296 -0
- package/src/config/index.js +22 -0
- package/src/constants.js +15 -0
- package/src/git/branches.js +68 -0
- package/src/git/core.js +67 -0
- package/src/git/templates.js +51 -0
- package/src/gitlab/api.js +46 -0
- package/src/gitlab/helpers.js +73 -0
- package/src/main.js +418 -0
- package/src/ui/colors.js +54 -0
- package/src/ui/print.js +177 -0
- package/src/ui/theme.js +26 -0
- package/src/utils/exec.js +12 -0
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
import { basename } from "path";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import boxen from "boxen";
|
|
5
|
+
import enquirer from "enquirer";
|
|
6
|
+
import { input, confirm, select, search } from "@inquirer/prompts";
|
|
7
|
+
import { Listr } from "listr2";
|
|
8
|
+
|
|
9
|
+
import { getCurrentBranch } from "../git/core.js";
|
|
10
|
+
import { getBranchChoices } from "../git/branches.js";
|
|
11
|
+
import { getRemoteUrl, isGitLabRemote, getDefaultBranch } from "../gitlab/helpers.js";
|
|
12
|
+
import { execFileAsync, extractMsg } from "../utils/exec.js";
|
|
13
|
+
import { loadConfig, saveConfig } from "../config/index.js";
|
|
14
|
+
import { p, THEME } from "../ui/theme.js";
|
|
15
|
+
import { colorBranch } from "../ui/colors.js";
|
|
16
|
+
|
|
17
|
+
export async function cmdMr(repos, opts = {}) {
|
|
18
|
+
const config = loadConfig();
|
|
19
|
+
const mrConfig = config.mr ?? {};
|
|
20
|
+
|
|
21
|
+
// Destructure CLI opts (all optional — absence falls through to interactive prompts)
|
|
22
|
+
const {
|
|
23
|
+
target: cliTarget,
|
|
24
|
+
mrRepos: cliMrRepos,
|
|
25
|
+
title: cliTitle,
|
|
26
|
+
description: cliDescription,
|
|
27
|
+
labels: cliLabels,
|
|
28
|
+
draft: cliDraft,
|
|
29
|
+
noPush: cliNoPush,
|
|
30
|
+
yes: autoConfirm,
|
|
31
|
+
} = opts;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
execSync("glab version", { encoding: "utf8", stdio: "pipe" });
|
|
35
|
+
} catch {
|
|
36
|
+
console.log(
|
|
37
|
+
boxen(
|
|
38
|
+
chalk.bold(p.red("glab not found")) + "\n\n" +
|
|
39
|
+
p.muted("The GitLab CLI is required for this feature.\n") +
|
|
40
|
+
p.muted("Install: ") + p.cyan("https://gitlab.com/gitlab-org/cli#installation"),
|
|
41
|
+
{
|
|
42
|
+
padding: { top: 1, bottom: 1, left: 3, right: 3 },
|
|
43
|
+
borderStyle: "round",
|
|
44
|
+
borderColor: "#f87171",
|
|
45
|
+
title: p.red(" missing dependency "),
|
|
46
|
+
titleAlignment: "center",
|
|
47
|
+
},
|
|
48
|
+
),
|
|
49
|
+
);
|
|
50
|
+
return 1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const repoInfo = await Promise.all(repos.map(async (repo) => {
|
|
54
|
+
const name = basename(repo);
|
|
55
|
+
const branch = await getCurrentBranch(repo);
|
|
56
|
+
const remote = getRemoteUrl(repo);
|
|
57
|
+
const isGitlab = isGitLabRemote(remote);
|
|
58
|
+
const defBranch = getDefaultBranch(repo);
|
|
59
|
+
const remoteShort = remote.replace(/^https?:\/\//, "").replace(/\.git$/, "").slice(0, 55);
|
|
60
|
+
return { repo, name, branch, remote, remoteShort, isGitlab, defBranch };
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
const repoChoice = (repo) => ({
|
|
64
|
+
value: repo,
|
|
65
|
+
name: chalk.bold(p.white(repo.name)),
|
|
66
|
+
description:
|
|
67
|
+
colorBranch(repo.branch) + " " +
|
|
68
|
+
(repo.isGitlab ? p.muted(repo.remoteShort) : p.dim(repo.remoteShort || "no remote")),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const printSelectedRepos = (selected) => {
|
|
72
|
+
if (selected.length === 1) {
|
|
73
|
+
console.log(
|
|
74
|
+
" " + chalk.bold(p.white(selected[0].name)) +
|
|
75
|
+
" " + colorBranch(selected[0].branch) + "\n",
|
|
76
|
+
);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const maxN = Math.max(...selected.map((r) => r.name.length));
|
|
80
|
+
console.log(" " + p.muted(`${selected.length} repos selected:`));
|
|
81
|
+
console.log(" " + p.dim("─".repeat(maxN + 20)));
|
|
82
|
+
for (const r of selected) {
|
|
83
|
+
console.log(
|
|
84
|
+
" " + p.muted("◉") + " " +
|
|
85
|
+
chalk.bold(p.white(r.name.padEnd(maxN))) +
|
|
86
|
+
" " + colorBranch(r.branch),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
console.log();
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const promptRepoSearch = (message, excludedNames = new Set()) =>
|
|
93
|
+
search({
|
|
94
|
+
message,
|
|
95
|
+
source: (input) => {
|
|
96
|
+
const term = (input ?? "").toLowerCase().trim();
|
|
97
|
+
return [
|
|
98
|
+
{ value: "__back__", name: p.yellow("← Go back"), description: p.muted("return to previous step") },
|
|
99
|
+
...repoInfo
|
|
100
|
+
.filter((r) => !excludedNames.has(r.name))
|
|
101
|
+
.filter((r) => !term || r.name.toLowerCase().includes(term))
|
|
102
|
+
.map(repoChoice),
|
|
103
|
+
];
|
|
104
|
+
},
|
|
105
|
+
theme: THEME,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const promptMultiRepoSearch = async () => {
|
|
109
|
+
const choices = repoInfo.map((repo) => ({
|
|
110
|
+
name: repo.name,
|
|
111
|
+
value: repo.name,
|
|
112
|
+
repo,
|
|
113
|
+
message: chalk.bold(p.white(repo.name)),
|
|
114
|
+
hint:
|
|
115
|
+
colorBranch(repo.branch) + " " +
|
|
116
|
+
(repo.isGitlab ? p.muted(repo.remoteShort) : p.dim(repo.remoteShort || "no remote")),
|
|
117
|
+
}));
|
|
118
|
+
|
|
119
|
+
const selectedNames = await enquirer.autocomplete({
|
|
120
|
+
name: "repos",
|
|
121
|
+
message: "Select repositories:",
|
|
122
|
+
multiple: true,
|
|
123
|
+
initial: 0,
|
|
124
|
+
limit: 12,
|
|
125
|
+
choices,
|
|
126
|
+
symbols: { indicator: { on: "◉", off: "◯" } },
|
|
127
|
+
footer() {
|
|
128
|
+
return p.muted("space to toggle, type to filter, enter to confirm, esc to go back");
|
|
129
|
+
},
|
|
130
|
+
suggest(input = "", allChoices = []) {
|
|
131
|
+
const term = input.toLowerCase().trim();
|
|
132
|
+
const selectedChoices = allChoices.filter((c) => c.enabled);
|
|
133
|
+
const unselectedChoices = allChoices.filter((c) => !c.enabled);
|
|
134
|
+
const filteredUnselected = term
|
|
135
|
+
? unselectedChoices.filter((c) =>
|
|
136
|
+
c.repo.name.toLowerCase().includes(term) ||
|
|
137
|
+
c.repo.branch.toLowerCase().includes(term) ||
|
|
138
|
+
c.repo.remoteShort.toLowerCase().includes(term),
|
|
139
|
+
)
|
|
140
|
+
: unselectedChoices;
|
|
141
|
+
return [...selectedChoices, ...filteredUnselected];
|
|
142
|
+
},
|
|
143
|
+
}).catch(() => "__back__");
|
|
144
|
+
|
|
145
|
+
if (selectedNames === "__back__") return "__back__";
|
|
146
|
+
|
|
147
|
+
return selectedNames
|
|
148
|
+
.map((name) => repoInfo.find((r) => r.name === name))
|
|
149
|
+
.filter(Boolean);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const promptTargetBranch = async (selectedRepos) => {
|
|
153
|
+
const branchChoices = getBranchChoices(selectedRepos);
|
|
154
|
+
const defaultTarget = mrConfig.targetBranch || selectedRepos[0].defBranch || "main";
|
|
155
|
+
|
|
156
|
+
if (branchChoices.length > 0) {
|
|
157
|
+
return search({
|
|
158
|
+
message: p.white("Target branch:"),
|
|
159
|
+
source: (input) => {
|
|
160
|
+
const term = (input ?? "").toLowerCase().trim();
|
|
161
|
+
return [
|
|
162
|
+
{ value: "__back__", name: p.yellow("← Go back"), description: p.muted("return to repository selection") },
|
|
163
|
+
...branchChoices
|
|
164
|
+
.filter((b) => !term || b.name.toLowerCase().includes(term))
|
|
165
|
+
.map((b) => {
|
|
166
|
+
const repoLabel = selectedRepos.length === 1
|
|
167
|
+
? p.muted("latest local/origin ref")
|
|
168
|
+
: p.muted(`seen in ${b.repos.length}/${selectedRepos.length} repos`);
|
|
169
|
+
return {
|
|
170
|
+
value: b.name,
|
|
171
|
+
name: colorBranch(b.name),
|
|
172
|
+
description: b.name === defaultTarget
|
|
173
|
+
? p.teal("default target") + " " + repoLabel
|
|
174
|
+
: repoLabel,
|
|
175
|
+
};
|
|
176
|
+
}),
|
|
177
|
+
];
|
|
178
|
+
},
|
|
179
|
+
theme: THEME,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return input({
|
|
184
|
+
message: p.white("Target branch") + p.muted(" (merge into):"),
|
|
185
|
+
default: defaultTarget,
|
|
186
|
+
theme: THEME,
|
|
187
|
+
validate: (v) => v.trim() !== "" || "Target branch cannot be empty.",
|
|
188
|
+
});
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
let selected;
|
|
192
|
+
let targetBranch;
|
|
193
|
+
|
|
194
|
+
// ── Non-interactive path — both --target and (optionally) --repo supplied ────
|
|
195
|
+
if (cliTarget) {
|
|
196
|
+
targetBranch = cliTarget;
|
|
197
|
+
|
|
198
|
+
if (cliMrRepos) {
|
|
199
|
+
// Filter by requested repo names
|
|
200
|
+
selected = repoInfo.filter((r) => cliMrRepos.includes(r.name));
|
|
201
|
+
if (selected.length === 0) {
|
|
202
|
+
console.log(p.yellow(` No matching repos for --repo filter(s).\n`));
|
|
203
|
+
return 1;
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
// Use all repos in scope
|
|
207
|
+
selected = repoInfo;
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
// ── Interactive path ─────────────────────────────────────────────────────────
|
|
211
|
+
selectionFlow: while (true) {
|
|
212
|
+
const selMode = await select({
|
|
213
|
+
message: p.white("Scope:"),
|
|
214
|
+
choices: [
|
|
215
|
+
{ value: "__back__", name: p.yellow("← Go back"), description: p.muted("return to mode selection") },
|
|
216
|
+
{ value: "single", name: p.cyan("Single repo") + p.muted(" search and pick one") },
|
|
217
|
+
{ value: "multi", name: p.purple("Multiple repos") + p.muted(" manually build a selection") },
|
|
218
|
+
],
|
|
219
|
+
default: mrConfig.scope === "multi" ? "multi" : "single",
|
|
220
|
+
theme: THEME,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
if (selMode === "__back__") return "__back__";
|
|
224
|
+
|
|
225
|
+
console.log();
|
|
226
|
+
|
|
227
|
+
while (true) {
|
|
228
|
+
if (selMode === "single") {
|
|
229
|
+
const picked = await promptRepoSearch(p.white("Repository:"));
|
|
230
|
+
console.log();
|
|
231
|
+
if (picked === "__back__") continue selectionFlow;
|
|
232
|
+
selected = [picked];
|
|
233
|
+
} else {
|
|
234
|
+
selected = await promptMultiRepoSearch();
|
|
235
|
+
console.log();
|
|
236
|
+
if (selected === "__back__") continue selectionFlow;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!selected || selected.length === 0) {
|
|
240
|
+
console.log(p.muted(" Nothing selected. Exiting.\n"));
|
|
241
|
+
return 0;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
printSelectedRepos(selected);
|
|
245
|
+
|
|
246
|
+
const target = await promptTargetBranch(selected);
|
|
247
|
+
console.log();
|
|
248
|
+
if (target === "__back__") continue;
|
|
249
|
+
|
|
250
|
+
targetBranch = target;
|
|
251
|
+
break selectionFlow;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
} // end non-interactive / interactive split
|
|
255
|
+
|
|
256
|
+
const title = cliTitle !== null && cliTitle !== undefined
|
|
257
|
+
? cliTitle
|
|
258
|
+
: await input({
|
|
259
|
+
message: p.white("Title") + p.muted(" (blank → use last commit message):"),
|
|
260
|
+
theme: THEME,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const description = cliDescription !== null && cliDescription !== undefined
|
|
264
|
+
? cliDescription
|
|
265
|
+
: await input({
|
|
266
|
+
message: p.white("Description") + p.muted(" (blank → use commit body):"),
|
|
267
|
+
theme: THEME,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const labels = cliLabels !== null && cliLabels !== undefined
|
|
271
|
+
? cliLabels
|
|
272
|
+
: await input({
|
|
273
|
+
message: p.white("Labels") + p.muted(" (comma separated, optional):"),
|
|
274
|
+
default: mrConfig.labels ?? "",
|
|
275
|
+
theme: { ...THEME, style: { ...THEME.style, answer: (s) => p.purple(s) } },
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const isDraft = cliDraft
|
|
279
|
+
? true
|
|
280
|
+
: await confirm({
|
|
281
|
+
message: p.white("Mark as") + p.muted(" Draft?"),
|
|
282
|
+
default: mrConfig.isDraft ?? false,
|
|
283
|
+
theme: THEME,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// --no-push takes priority; cliNoPush=true means skip push
|
|
287
|
+
const pushFirst = cliNoPush
|
|
288
|
+
? false
|
|
289
|
+
: await confirm({
|
|
290
|
+
message: p.white("Push branch") + p.muted(" to remote before creating MR?"),
|
|
291
|
+
default: mrConfig.pushFirst ?? true,
|
|
292
|
+
theme: THEME,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
console.log();
|
|
296
|
+
|
|
297
|
+
const repoLabel = selected.length === 1
|
|
298
|
+
? chalk.bold(p.white(selected[0].name))
|
|
299
|
+
: p.white(`${selected.length} repos`) + " " +
|
|
300
|
+
p.muted(
|
|
301
|
+
selected.map((r) => r.name).join(", ").slice(0, 60) +
|
|
302
|
+
(selected.map((r) => r.name).join(", ").length > 60 ? "…" : ""),
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
const sourceLabel = selected.length === 1
|
|
306
|
+
? colorBranch(selected[0].branch) + p.muted(" → ") + colorBranch(targetBranch)
|
|
307
|
+
: p.muted("each branch") + p.muted(" → ") + colorBranch(targetBranch);
|
|
308
|
+
|
|
309
|
+
const previewLines = [
|
|
310
|
+
p.muted(" repo ") + repoLabel,
|
|
311
|
+
p.muted(" source ") + sourceLabel,
|
|
312
|
+
p.muted(" title ") + (title.trim() ? chalk.bold(p.white(title)) : p.dim("← last commit message")),
|
|
313
|
+
p.muted(" description ") + (description.trim()
|
|
314
|
+
? p.white(description.slice(0, 60) + (description.length > 60 ? "…" : ""))
|
|
315
|
+
: p.dim(title.trim() ? "—" : "← commit body")),
|
|
316
|
+
labels ? p.muted(" labels ") + p.purple(labels) : null,
|
|
317
|
+
isDraft ? p.muted(" flags ") + p.yellow("draft") : null,
|
|
318
|
+
pushFirst ? p.muted(" push first ") + p.teal("yes") : null,
|
|
319
|
+
].filter(Boolean).join("\n");
|
|
320
|
+
|
|
321
|
+
console.log(
|
|
322
|
+
boxen(previewLines, {
|
|
323
|
+
padding: { top: 1, bottom: 1, left: 2, right: 2 },
|
|
324
|
+
borderStyle: "round",
|
|
325
|
+
borderColor: "#334155",
|
|
326
|
+
title: p.muted(" merge request preview "),
|
|
327
|
+
titleAlignment: "right",
|
|
328
|
+
}),
|
|
329
|
+
);
|
|
330
|
+
console.log();
|
|
331
|
+
|
|
332
|
+
const confirmed = autoConfirm
|
|
333
|
+
? true
|
|
334
|
+
: await confirm({
|
|
335
|
+
message: p.white(
|
|
336
|
+
selected.length === 1 ? "Create merge request?" : `Create ${selected.length} merge requests?`,
|
|
337
|
+
),
|
|
338
|
+
default: true,
|
|
339
|
+
theme: THEME,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
if (!confirmed) {
|
|
343
|
+
console.log("\n" + p.muted(" Aborted.\n"));
|
|
344
|
+
return 0;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
saveConfig({
|
|
348
|
+
...config,
|
|
349
|
+
mr: {
|
|
350
|
+
...mrConfig,
|
|
351
|
+
scope: selected.length > 1 ? "multi" : "single",
|
|
352
|
+
targetBranch,
|
|
353
|
+
labels,
|
|
354
|
+
isDraft,
|
|
355
|
+
pushFirst,
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
console.log();
|
|
360
|
+
|
|
361
|
+
const buildGlabArgs = () => {
|
|
362
|
+
const a = ["mr", "create", "--target-branch", targetBranch, "--yes"];
|
|
363
|
+
if (title.trim()) {
|
|
364
|
+
a.push("--title", title.trim());
|
|
365
|
+
} else {
|
|
366
|
+
a.push("--fill");
|
|
367
|
+
}
|
|
368
|
+
if (description.trim()) a.push("--description", description.trim());
|
|
369
|
+
if (isDraft) a.push("--draft");
|
|
370
|
+
if (labels) a.push("--label", labels);
|
|
371
|
+
if (pushFirst) a.push("--push");
|
|
372
|
+
return a;
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const mrResults = [];
|
|
376
|
+
const numWidth = String(selected.length).length;
|
|
377
|
+
|
|
378
|
+
const tasks = new Listr(
|
|
379
|
+
selected.map((r, i) => {
|
|
380
|
+
const idx = p.muted(`[${String(i + 1).padStart(numWidth)}/${selected.length}]`);
|
|
381
|
+
return {
|
|
382
|
+
title:
|
|
383
|
+
idx + " " + chalk.bold(p.white(r.name)) + " " +
|
|
384
|
+
colorBranch(r.branch) + p.muted(" → ") + colorBranch(targetBranch) +
|
|
385
|
+
p.muted(" opening…"),
|
|
386
|
+
task: async (_, task) => {
|
|
387
|
+
const t0 = Date.now();
|
|
388
|
+
try {
|
|
389
|
+
const { stdout, stderr } = await execFileAsync("glab", buildGlabArgs(), { cwd: r.repo });
|
|
390
|
+
const combined = (stdout + "\n" + stderr).trim();
|
|
391
|
+
const url = combined.split("\n").find((l) => /https?:\/\//.test(l))?.trim() ?? "";
|
|
392
|
+
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
|
|
393
|
+
|
|
394
|
+
mrResults.push({ name: r.name, ok: true, url });
|
|
395
|
+
task.title =
|
|
396
|
+
idx + " " + chalk.bold(p.white(r.name)) +
|
|
397
|
+
" " + p.green("✔") + " " +
|
|
398
|
+
colorBranch(r.branch) + p.muted(" → ") + colorBranch(targetBranch) +
|
|
399
|
+
(url ? " " + p.cyan(url) : "") +
|
|
400
|
+
p.muted(" " + elapsed + "s");
|
|
401
|
+
} catch (e) {
|
|
402
|
+
const raw = (e.stdout ?? "") + (e.stderr ?? "");
|
|
403
|
+
const url = raw.split("\n").find((l) => /https?:\/\//.test(l))?.trim() ?? "";
|
|
404
|
+
const msg = extractMsg(e);
|
|
405
|
+
|
|
406
|
+
if (url) {
|
|
407
|
+
mrResults.push({ name: r.name, ok: true, url, existing: true });
|
|
408
|
+
task.title =
|
|
409
|
+
idx + " " + chalk.bold(p.white(r.name)) +
|
|
410
|
+
" " + p.teal("◉") +
|
|
411
|
+
" " + p.muted("MR already exists ") + p.cyan(url);
|
|
412
|
+
} else {
|
|
413
|
+
mrResults.push({ name: r.name, ok: false, msg });
|
|
414
|
+
task.title =
|
|
415
|
+
idx + " " + chalk.bold(p.white(r.name)) +
|
|
416
|
+
" " + p.red("✘") +
|
|
417
|
+
" " + p.muted(msg.slice(0, 70));
|
|
418
|
+
throw new Error(msg);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
};
|
|
423
|
+
}),
|
|
424
|
+
{ concurrent: false, exitOnError: false },
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
await tasks.run().catch(() => {});
|
|
428
|
+
|
|
429
|
+
console.log();
|
|
430
|
+
|
|
431
|
+
const opened = mrResults.filter((r) => r.ok && !r.existing);
|
|
432
|
+
const existing = mrResults.filter((r) => r.ok && r.existing);
|
|
433
|
+
const failed = mrResults.filter((r) => !r.ok);
|
|
434
|
+
const sep = p.slate(" · ");
|
|
435
|
+
|
|
436
|
+
const summaryParts = [];
|
|
437
|
+
if (opened.length) summaryParts.push(chalk.bold(p.green(`✔ ${opened.length} opened`)));
|
|
438
|
+
if (existing.length) summaryParts.push(p.teal(`◉ ${existing.length} already existed`));
|
|
439
|
+
if (failed.length) summaryParts.push(p.red(`✘ ${failed.length} failed`));
|
|
440
|
+
|
|
441
|
+
console.log(
|
|
442
|
+
boxen(
|
|
443
|
+
summaryParts.join(sep) +
|
|
444
|
+
"\n" + p.muted(`${selected.length} repo${selected.length !== 1 ? "s" : ""} processed`),
|
|
445
|
+
{
|
|
446
|
+
padding: { top: 0, bottom: 0, left: 2, right: 2 },
|
|
447
|
+
borderStyle: "round",
|
|
448
|
+
borderColor: failed.length > 0 ? "#f87171" : "#4ade80",
|
|
449
|
+
},
|
|
450
|
+
),
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
const successful = mrResults.filter((r) => r.ok && r.url);
|
|
454
|
+
if (successful.length > 0) {
|
|
455
|
+
const maxN = Math.max(...successful.map((r) => r.name.length));
|
|
456
|
+
console.log();
|
|
457
|
+
console.log(" " + chalk.bold(p.white("Merge Requests")));
|
|
458
|
+
console.log(" " + p.dim("─".repeat(50)));
|
|
459
|
+
for (const r of successful) {
|
|
460
|
+
const badge = r.existing ? p.teal("◉") : p.green("✔");
|
|
461
|
+
console.log(
|
|
462
|
+
" " + badge + " " +
|
|
463
|
+
chalk.bold(p.white(r.name.padEnd(maxN))) +
|
|
464
|
+
" " + p.cyan(r.url),
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (failed.length > 0) {
|
|
470
|
+
console.log();
|
|
471
|
+
console.log(" " + chalk.bold(p.white("Errors")));
|
|
472
|
+
console.log(" " + p.dim("─".repeat(50)));
|
|
473
|
+
for (const r of failed) {
|
|
474
|
+
console.log(
|
|
475
|
+
" " + p.red("✘") + " " +
|
|
476
|
+
chalk.bold(p.white(r.name)) +
|
|
477
|
+
" " + p.muted(r.msg.slice(0, 90)),
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
console.log();
|
|
483
|
+
return failed.length > 0 ? 1 : 0;
|
|
484
|
+
}
|