@nemo-cli/git 0.0.1-beta.4

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/dist/index.js ADDED
@@ -0,0 +1,1088 @@
1
+ import { addFiles, colors, createCheckbox, createCommand, createConfirm, createHelpExample, createInput, createNote, createOptions, createSearch, createSelect, createSpinner, exit, getCurrentBranch, getGitStatus, intro, isEmpty, isString, loadConfig, log, outro, readPackage, x, xASync } from "@nemo-cli/shared";
2
+ import { BigText, ErrorMessage, Message } from "@nemo-cli/ui";
3
+ import { spawn } from "node:child_process";
4
+ import { unlinkSync, writeFileSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+
8
+ //#region src/constants/index.ts
9
+ const HELP_MESSAGE$1 = {
10
+ main: createHelpExample("ng --version", "ng --help", "ng <command> [option]"),
11
+ branch: createHelpExample("ng branch --version", "ng branch --help", "ng branch <command> [option]"),
12
+ branchDelete: createHelpExample("ng branch delete --version", "ng branch delete --help", "ng branch delete <command> [option]"),
13
+ branchClean: createHelpExample("ng branch clean --version", "ng branch clean --help")
14
+ };
15
+
16
+ //#endregion
17
+ //#region src/utils.ts
18
+ const remotePrefix = /^origin\//;
19
+ const getRemoteBranches = async () => {
20
+ return { branches: (await x("git", [
21
+ "branch",
22
+ "-r",
23
+ "--sort=-committerdate"
24
+ ])).stdout.split("\n").filter((line) => line.trim() && !line.includes("->")).map((line) => line.trim().replace(remotePrefix, "")) };
25
+ };
26
+ const currentBranchPrefix = /^\* /;
27
+ const formatBranch$1 = (branch) => branch?.trim().replace(currentBranchPrefix, "");
28
+ const getLocalBranches = async () => {
29
+ const list = (await x("git", ["branch", "--sort=-committerdate"])).stdout.split("\n");
30
+ const currentBranch = list.find((line) => line.includes("*"));
31
+ return {
32
+ branches: list.filter((line) => line.trim() && !line.includes("->")).map((line) => line.trim().replace(currentBranchPrefix, "")),
33
+ currentBranch: formatBranch$1(currentBranch)
34
+ };
35
+ };
36
+ const getRemoteOptions = async () => {
37
+ const { branches } = await getRemoteBranches();
38
+ const currentBranch = await getCurrentBranch();
39
+ return {
40
+ options: branches.map((branch) => ({
41
+ label: branch,
42
+ value: branch,
43
+ hint: branch === currentBranch ? "current branch" : void 0
44
+ })),
45
+ currentBranch
46
+ };
47
+ };
48
+ const getLocalOptions = async () => {
49
+ const { branches, currentBranch } = await getLocalBranches();
50
+ return {
51
+ options: branches.map((branch) => ({
52
+ label: branch,
53
+ value: branch,
54
+ hint: branch === currentBranch ? "current branch" : void 0
55
+ })),
56
+ currentBranch
57
+ };
58
+ };
59
+ /**
60
+ * 处理合并提交信息
61
+ */
62
+ const handleMergeCommit = async () => {
63
+ try {
64
+ const [error, result] = await xASync("git", ["status", "--porcelain"]);
65
+ if (error) return;
66
+ if (!(result.stdout.trim().length > 0)) return;
67
+ log.show("\n📝 Merge commit detected. You can customize the commit message.", { type: "info" });
68
+ if (!await createConfirm({ message: "Do you want to customize the merge commit message?" })) {
69
+ await xASync("git", ["commit", "--no-edit"]);
70
+ log.show("Using default merge commit message.", { type: "info" });
71
+ return;
72
+ }
73
+ const [processError, processResult] = await xASync("git", [
74
+ "log",
75
+ "--format=%B",
76
+ "-n",
77
+ "1",
78
+ "HEAD"
79
+ ]);
80
+ if (processError) return;
81
+ const defaultMessage = processResult.stdout;
82
+ const tempFile = join(tmpdir(), `merge-commit-${Date.now()}.txt`);
83
+ writeFileSync(tempFile, defaultMessage);
84
+ const editor = process.env.EDITOR || process.env.VISUAL || "vim";
85
+ log.show(`Opening ${editor} to edit commit message...`, { type: "info" });
86
+ log.show("Save and close the editor to continue, or close without saving to cancel.", { type: "info" });
87
+ const editProcess = spawn(editor, [tempFile], {
88
+ stdio: "inherit",
89
+ shell: true
90
+ });
91
+ if (await new Promise((resolve) => {
92
+ editProcess.on("close", (code) => {
93
+ resolve(code || 0);
94
+ });
95
+ }) !== 0) {
96
+ log.show("Editor was closed without saving. Using default commit message.", { type: "warn" });
97
+ await xASync("git", ["commit", "--no-edit"]);
98
+ unlinkSync(tempFile);
99
+ return;
100
+ }
101
+ const { readFileSync } = await import("node:fs");
102
+ const editedMessage = readFileSync(tempFile, "utf-8");
103
+ unlinkSync(tempFile);
104
+ if (!editedMessage.trim()) {
105
+ log.show("Commit message is empty. Using default commit message.", { type: "warn" });
106
+ await xASync("git", ["commit", "--no-edit"]);
107
+ return;
108
+ }
109
+ const commitProcess = spawn("git", [
110
+ "commit",
111
+ "-F",
112
+ "-"
113
+ ], {
114
+ stdio: [
115
+ "pipe",
116
+ "pipe",
117
+ "pipe"
118
+ ],
119
+ shell: true
120
+ });
121
+ commitProcess.stdin?.write(editedMessage);
122
+ commitProcess.stdin?.end();
123
+ if (await new Promise((resolve) => {
124
+ commitProcess.on("close", (code) => {
125
+ resolve(code || 0);
126
+ });
127
+ }) === 0) log.show("Merge commit completed with custom message.", { type: "success" });
128
+ else throw new Error("Failed to create merge commit with custom message");
129
+ } catch (error) {
130
+ log.show("Error handling merge commit:", { type: "error" });
131
+ log.error(error);
132
+ try {
133
+ await xASync("git", ["commit", "--no-edit"]);
134
+ log.show("Fallback: Using default merge commit message.", { type: "info" });
135
+ } catch (fallbackError) {
136
+ log.show("Failed to create merge commit. Please handle manually.", { type: "error" });
137
+ throw fallbackError;
138
+ }
139
+ }
140
+ };
141
+ const handleGitPull = async (branch, options = {}) => {
142
+ const { rebase = false } = options;
143
+ const modeText = rebase ? "rebase" : "merge";
144
+ log.show(`Pulling from remote (${modeText} mode)...`, { type: "step" });
145
+ try {
146
+ const [error, result] = await xASync("git", rebase ? [
147
+ "pull",
148
+ "--rebase",
149
+ "origin",
150
+ branch
151
+ ] : [
152
+ "pull",
153
+ "origin",
154
+ branch
155
+ ], { nodeOptions: { stdio: "inherit" } });
156
+ if (error) {
157
+ log.show(`Failed to pull from remote. Command exited with code ${error.message}.`, { type: "error" });
158
+ return;
159
+ }
160
+ if (!rebase && (result.stdout.includes("Merge branch") || result.stdout.includes("Merge made by"))) await handleMergeCommit();
161
+ log.show(`Successfully pulled from remote: ${colors.bgGreen(branch)} (${modeText})`, { type: "success" });
162
+ } catch (error) {
163
+ log.error(error);
164
+ return;
165
+ }
166
+ };
167
+ const createStashName = () => `NEMO-CLI-STASH:${Date.now()}`;
168
+ const handleGitStash = async (name = createStashName()) => {
169
+ const [error, result] = await xASync("git", [
170
+ "stash",
171
+ "save",
172
+ name
173
+ ]);
174
+ if (error) {
175
+ log.show(`Failed to stash changes. ${name}`, { type: "error" });
176
+ return null;
177
+ }
178
+ if (result?.stdout.includes(name)) {
179
+ log.show(`Successfully stashed changes. ${name}`, { type: "success" });
180
+ return name;
181
+ }
182
+ log.show("No file changes.");
183
+ return null;
184
+ };
185
+ const handleGitStashCheck = async () => {
186
+ const [error, result] = await xASync("git", ["stash", "list"]);
187
+ if (error) return [];
188
+ return result.stdout.split("\n").filter((line) => line.trim());
189
+ };
190
+ const handleGitPop = async (branch) => {
191
+ const stashName = (await handleGitStashCheck()).find((stash) => stash.includes(branch));
192
+ if (!stashName) {
193
+ log.show(`No stash found for this branch: ${colors.bgRed(branch)}.`, { type: "warn" });
194
+ return;
195
+ }
196
+ const name = stashName.split(":")[0];
197
+ if (!name) {
198
+ log.error(name, "is not valid");
199
+ return;
200
+ }
201
+ const [error, result] = await xASync("git", [
202
+ "stash",
203
+ "pop",
204
+ name
205
+ ]);
206
+ if (!error) createNote({
207
+ message: result.stdout,
208
+ title: "Successfully popped changes."
209
+ });
210
+ };
211
+ const isBranchMergedToMain = async (branches) => {
212
+ const spinner = createSpinner("Fetching latest changes from remote");
213
+ const [fetchError] = await xASync("git", [
214
+ "fetch",
215
+ "origin",
216
+ "--prune"
217
+ ]);
218
+ if (fetchError) spinner.stop("Failed to fetch latest changes from remote. Proceeding with local information.");
219
+ else spinner.stop("Fetching latest changes from remote Done");
220
+ const remoteMainBranch = await getRemoteMainBranch();
221
+ if (!remoteMainBranch) return [];
222
+ return Promise.all(branches.map(async (branch) => {
223
+ const [error, result] = await xASync("git", ["log", `${remoteMainBranch}..${branch}`], { quiet: true });
224
+ if (error) return {
225
+ branch,
226
+ isMerged: false
227
+ };
228
+ return {
229
+ branch,
230
+ isMerged: !result?.stdout.trim()
231
+ };
232
+ }));
233
+ };
234
+ const checkGitRepository = async () => {
235
+ try {
236
+ const [error, result] = await xASync("git", ["rev-parse", "--is-inside-work-tree"], { quiet: true });
237
+ if (error) return false;
238
+ return result.stdout.trim() === "true";
239
+ } catch (err) {
240
+ return false;
241
+ }
242
+ };
243
+ const getRemoteMainBranch = async () => {
244
+ const [error, result] = await xASync("git", ["symbolic-ref", "refs/remotes/origin/HEAD"]);
245
+ if (error) return null;
246
+ return result.stdout.trim().split("/").splice(2).join("/");
247
+ };
248
+ const getBranchCommitTime = async (branch) => {
249
+ const [_, result] = await xASync("git", [
250
+ "show",
251
+ "--format=%at",
252
+ `${branch}`
253
+ ]);
254
+ return result?.stdout.split("\n")[0] ?? Date.now();
255
+ };
256
+
257
+ //#endregion
258
+ //#region src/commands/branch.ts
259
+ const formatTime = (time) => (/* @__PURE__ */ new Date(time * 1e3)).toLocaleString();
260
+ const formatBranch = (branch) => branch.startsWith("origin/") ? branch.slice(7) : branch;
261
+ const handleDelete = async (branch, { isRemote }) => {
262
+ if (!branch.isMerged) {
263
+ if (!await createConfirm({ message: `Branch ${branch.branch} is not merged to main. Are you sure you want to delete it?` })) return;
264
+ }
265
+ const spinner = createSpinner(`Deleting branch ${branch.branch}...`);
266
+ const process = x("git", isRemote ? [
267
+ "push",
268
+ "origin",
269
+ "--delete",
270
+ formatBranch(branch.branch)
271
+ ] : [
272
+ "branch",
273
+ "-D",
274
+ branch.branch
275
+ ]);
276
+ for await (const line of process) spinner.message(line);
277
+ if (process.exitCode) spinner.stop(`Failed to delete branch ${branch}. Command exited with code ${process.exitCode}.`);
278
+ else spinner.stop(`Successfully deleted branch ${branch.branch}`);
279
+ };
280
+ const excludeBranch = [
281
+ "main",
282
+ "master",
283
+ "develop"
284
+ ];
285
+ const oneDay = 3600 * 24;
286
+ function branchCommand(command) {
287
+ const subCommand = command.command("branch").description("Git branch management").addHelpText("after", HELP_MESSAGE$1.branch);
288
+ subCommand.command("clean").description("Git branch clean merged to main").addHelpText("after", HELP_MESSAGE$1.branchClean).action(async () => {
289
+ const timeRange = await createSelect({
290
+ message: "Select the time range",
291
+ options: [
292
+ {
293
+ label: "all",
294
+ value: 0
295
+ },
296
+ {
297
+ label: "1 month",
298
+ value: oneDay * 30
299
+ },
300
+ {
301
+ label: "1 year",
302
+ value: oneDay * 365
303
+ },
304
+ {
305
+ label: "3 months",
306
+ value: oneDay * 90
307
+ }
308
+ ]
309
+ });
310
+ const { branches } = await getLocalBranches();
311
+ const mergedBranches = (await isBranchMergedToMain(branches.filter((branch) => !excludeBranch.includes(branch)))).filter((branch) => branch.isMerged).map((branch) => branch.branch);
312
+ const lastCommitBranches = await Promise.all(mergedBranches.map(async (branch) => {
313
+ const time = await getBranchCommitTime(branch);
314
+ return {
315
+ branch,
316
+ lastCommitTime: Number(time)
317
+ };
318
+ }));
319
+ const now = Date.now() / 1e3;
320
+ const deleteBranches = lastCommitBranches.filter((branch) => now - branch.lastCommitTime >= timeRange);
321
+ if (deleteBranches.length === 0) {
322
+ Message({ text: "No branches to delete. Please check your git repository." });
323
+ return;
324
+ }
325
+ createNote({
326
+ message: `Found ${deleteBranches.length} branches, will delete\n${deleteBranches.map((branch) => colors.red(branch.branch)).join("\n")}`,
327
+ title: "Delete Branches"
328
+ });
329
+ if (!await createConfirm({ message: "Are you sure you want to delete these branches?" })) return;
330
+ await Promise.all(deleteBranches.map((branch) => handleDelete({
331
+ branch: branch.branch,
332
+ isMerged: true
333
+ }, { isRemote: false })));
334
+ Message({ text: "Successfully deleted branches" });
335
+ });
336
+ subCommand.command("delete").description("Git branch delete").addHelpText("after", HELP_MESSAGE$1.branchDelete).option("-r, --remote", "remote branch").action(async (options) => {
337
+ const { branches } = options.remote ? await getRemoteBranches() : await getLocalBranches();
338
+ if (!branches || branches.length === 0) {
339
+ ErrorMessage({ text: "No branches found. Please check your git repository." });
340
+ return;
341
+ }
342
+ const mergeInfoList = await isBranchMergedToMain((options.remote ? branches.map((branch) => `origin/${branch}`) : branches).filter((branch) => !excludeBranch.includes(branch)));
343
+ if (mergeInfoList.length === 0) {
344
+ ErrorMessage({ text: "No branches to delete. Please check your git repository." });
345
+ return;
346
+ }
347
+ const deleteBranches = await createCheckbox({
348
+ message: "Select the branch to delete",
349
+ options: await Promise.all(mergeInfoList.map(async (branch) => {
350
+ const lastCommitTime = await getBranchCommitTime(branch.branch);
351
+ return {
352
+ label: `${branch.branch} ${branch.isMerged ? colors.green("(merged)") : colors.yellow("(not merged)")}`,
353
+ value: {
354
+ lastCommitTime,
355
+ branch: branch.branch,
356
+ isMerged: branch.isMerged
357
+ },
358
+ hint: `last commit: ${formatTime(Number(lastCommitTime))}`
359
+ };
360
+ }))
361
+ });
362
+ if (!deleteBranches.length) {
363
+ log.error("No branch selected. Aborting delete operation.");
364
+ return;
365
+ }
366
+ await Promise.all(deleteBranches.map((branch) => handleDelete({
367
+ branch: branch.branch,
368
+ isMerged: true
369
+ }, { isRemote: options.remote ?? false })));
370
+ Message({ text: "Successfully deleted branches" });
371
+ });
372
+ }
373
+
374
+ //#endregion
375
+ //#region src/commands/checkout.ts
376
+ const handleCheckout = async (branch, { isNew = false, isRemote = false } = {}) => {
377
+ const args = ["checkout"];
378
+ if (isNew || isRemote) args.push("-b");
379
+ args.push(branch);
380
+ if (isRemote) args.push(`origin/${branch}`);
381
+ const stashName = await handleGitStash(branch);
382
+ const process = x("git", args);
383
+ for await (const line of process) log.show(line);
384
+ const { exitCode, stderr } = await process;
385
+ if (exitCode) {
386
+ log.show(`Failed to checkout branch ${branch}. Command exited with code ${exitCode}.`, { type: "error" });
387
+ log.show(stderr, { type: "error" });
388
+ } else log.show(`Successfully checked out branch ${branch}.`, { type: "success" });
389
+ stashName && handleGitPop(stashName);
390
+ };
391
+ function checkoutCommand(command) {
392
+ command.command("checkout").alias("co").option("-l, --local", "Checkout a local branch", true).option("-r, --remote", "Checkout a remote branch").option("-b, --branch [branch]", "Create and checkout a new branch").description("Checkout a branch").action(async (params) => {
393
+ let isLocal = params.local && !params.remote;
394
+ const branch = params.branch;
395
+ if (branch) {
396
+ if (isString(branch)) {
397
+ handleCheckout(branch, { isNew: true });
398
+ return;
399
+ }
400
+ handleCheckout(`${await createSelect({
401
+ message: "Enter the new branch name:",
402
+ options: createOptions([
403
+ "feature/PRIME-",
404
+ "feature/",
405
+ "bugfix/"
406
+ ])
407
+ })}${await createInput({
408
+ message: "Enter the new branch name:",
409
+ validate: (value) => {
410
+ if (!value?.trim()) return "Branch name is required";
411
+ if (value.length > 15) return "Branch name must be less than 15 characters";
412
+ }
413
+ })}`, { isNew: true });
414
+ return;
415
+ }
416
+ if (isEmpty(params)) isLocal = await createSelect({
417
+ message: "Select the branch type",
418
+ options: [{
419
+ label: "Remote",
420
+ value: false
421
+ }, {
422
+ label: "Local",
423
+ value: true
424
+ }],
425
+ initialValue: true
426
+ });
427
+ if (isLocal) {
428
+ const { options } = await getLocalOptions();
429
+ handleCheckout(await createSelect({
430
+ message: `Select the ${colors.bgGreen(" local ")} branch to checkout`,
431
+ options
432
+ }));
433
+ } else {
434
+ const { options } = await getRemoteOptions();
435
+ const selectedBranch = await createSelect({
436
+ message: `Select the ${colors.bgYellow(" remote ")} branch to checkout`,
437
+ options
438
+ });
439
+ if (await createConfirm({ message: `Do you want to checkout ${colors.bgRed(selectedBranch)}?` })) handleCheckout(selectedBranch, { isRemote: true });
440
+ }
441
+ });
442
+ }
443
+
444
+ //#endregion
445
+ //#region src/commands/commit-options.ts
446
+ const commitScopeOptions = [
447
+ {
448
+ value: "app",
449
+ label: "app"
450
+ },
451
+ {
452
+ value: "shared",
453
+ label: "shared"
454
+ },
455
+ {
456
+ value: "server",
457
+ label: "server"
458
+ },
459
+ {
460
+ value: "tools",
461
+ label: "tools"
462
+ },
463
+ {
464
+ value: "",
465
+ label: "none"
466
+ }
467
+ ];
468
+ const commitTypeOptions = [
469
+ {
470
+ value: "feat",
471
+ label: "feat",
472
+ hint: "A new feature",
473
+ emoji: "🌟",
474
+ trailer: "Changelog: feature"
475
+ },
476
+ {
477
+ value: "fix",
478
+ label: "fix",
479
+ hint: "A bug fix",
480
+ emoji: "🐛",
481
+ trailer: "Changelog: fix"
482
+ },
483
+ {
484
+ value: "docs",
485
+ label: "docs",
486
+ hint: "Documentation only changes",
487
+ emoji: "📚",
488
+ trailer: "Changelog: documentation"
489
+ },
490
+ {
491
+ value: "refactor",
492
+ label: "refactor",
493
+ hint: "A code change that neither fixes a bug nor adds a feature",
494
+ emoji: "🔨",
495
+ trailer: "Changelog: refactor"
496
+ },
497
+ {
498
+ value: "perf",
499
+ label: "perf",
500
+ hint: "A code change that improves performance",
501
+ emoji: "🚀",
502
+ trailer: "Changelog: performance"
503
+ },
504
+ {
505
+ value: "test",
506
+ label: "test",
507
+ hint: "Adding missing tests or correcting existing tests",
508
+ emoji: "🚨",
509
+ trailer: "Changelog: test"
510
+ },
511
+ {
512
+ value: "build",
513
+ label: "build",
514
+ hint: "Changes that affect the build system or external dependencies",
515
+ emoji: "🚧",
516
+ trailer: "Changelog: build"
517
+ },
518
+ {
519
+ value: "ci",
520
+ label: "ci",
521
+ hint: "Changes to our CI configuration files and scripts",
522
+ emoji: "🤖",
523
+ trailer: "Changelog: ci"
524
+ },
525
+ {
526
+ value: "chore",
527
+ label: "chore",
528
+ hint: "Other changes that do not modify src or test files",
529
+ emoji: "🧹",
530
+ trailer: "Changelog: chore"
531
+ },
532
+ {
533
+ value: "revert",
534
+ label: "revert",
535
+ hint: "Revert a previous commit",
536
+ emoji: "🔙",
537
+ trailer: "Changelog: revert"
538
+ },
539
+ {
540
+ value: "",
541
+ label: "none"
542
+ }
543
+ ];
544
+ const commitlintConfig = {
545
+ extends: ["@commitlint/config-conventional"],
546
+ rules: {
547
+ "subject-empty": [2, "never"],
548
+ "type-empty": [2, "never"],
549
+ "type-enum": [
550
+ 2,
551
+ "always",
552
+ [
553
+ "feat",
554
+ "fix",
555
+ "docs",
556
+ "style",
557
+ "refactor",
558
+ "perf",
559
+ "test",
560
+ "build",
561
+ "ci",
562
+ "chore",
563
+ "revert",
564
+ "wip",
565
+ "release"
566
+ ]
567
+ ],
568
+ "scope-empty": [1, "always"],
569
+ "scope-enum": [
570
+ 1,
571
+ "always",
572
+ [
573
+ "app",
574
+ "shared",
575
+ "server",
576
+ "tools",
577
+ ""
578
+ ]
579
+ ]
580
+ }
581
+ };
582
+ const mergeCommitTypeEnumOptions = (options) => {
583
+ return options.map((option) => {
584
+ const commitTypeOption = commitTypeOptions.find((commitTypeOption$1) => commitTypeOption$1.value === option);
585
+ return {
586
+ value: commitTypeOption?.value ?? option,
587
+ label: commitTypeOption?.label ?? option,
588
+ hint: commitTypeOption?.hint,
589
+ emoji: commitTypeOption?.emoji,
590
+ trailer: commitTypeOption?.trailer
591
+ };
592
+ });
593
+ };
594
+ const mergeCommitScopeEnumOptions = (options) => {
595
+ return (options.includes("none") ? options : options.concat("none")).map((option) => {
596
+ const commitScopeOption = commitScopeOptions.find((commitScopeOption$1) => commitScopeOption$1.label === option);
597
+ return {
598
+ value: commitScopeOption?.value ?? option,
599
+ label: commitScopeOption?.label ?? option
600
+ };
601
+ });
602
+ };
603
+
604
+ //#endregion
605
+ //#region src/commands/push.ts
606
+ const handlePush = async (branch) => {
607
+ const spinner = createSpinner(`Pushing branch ${branch} to remote...`);
608
+ try {
609
+ const process = x("git", [
610
+ "push",
611
+ "origin",
612
+ branch
613
+ ]);
614
+ for await (const line of process) spinner.message(line);
615
+ const code = process.exitCode;
616
+ if (code) throw new Error(`Failed code ${code}`);
617
+ spinner.stop(colors.green(`Successfully pushed branch ${colors.bgGreen(branch)} to remote.`));
618
+ } catch (error) {
619
+ spinner.stop();
620
+ BigText({ text: `Failed to push branch ${branch}.` });
621
+ log.error(error);
622
+ }
623
+ };
624
+ function pushCommand(command) {
625
+ command.command("push").alias("ps").description("Push current branch to remote").action(async () => {
626
+ await pushInteractive();
627
+ });
628
+ }
629
+ const pushInteractive = async () => {
630
+ const currentBranch = await getCurrentBranch();
631
+ if (!currentBranch) {
632
+ log.error("No branch selected. Aborting push operation.");
633
+ return;
634
+ }
635
+ if (await createConfirm({ message: `Do you want to push ${colors.bgGreen(currentBranch)} to remote?` })) {
636
+ await handlePush(currentBranch);
637
+ return;
638
+ }
639
+ const { options } = await getRemoteOptions();
640
+ await handlePush(await createSelect({
641
+ message: "Select the branch to push",
642
+ options,
643
+ initialValue: "main"
644
+ }));
645
+ };
646
+
647
+ //#endregion
648
+ //#region src/commands/commit.ts
649
+ const lintHandle = async () => {
650
+ const [error, result] = await xASync("lint-staged");
651
+ if (error?.message === "spawn lint-staged ENOENT") return true;
652
+ if (error) return false;
653
+ return true;
654
+ };
655
+ const handleCommit = async (message) => {
656
+ const spinner = createSpinner("Committing...");
657
+ const [error, _result] = await xASync("git", [
658
+ "commit",
659
+ "-m",
660
+ message
661
+ ]);
662
+ if (error) spinner.stop("Failed to commit");
663
+ else spinner.stop("Committed");
664
+ };
665
+ const handleLint = async () => {
666
+ const spinner = createSpinner("run lint-staged linting...");
667
+ if (!await lintHandle()) !await createConfirm({
668
+ message: "Lint failed. Do you want to continue?",
669
+ initialValue: false
670
+ }) && exit(0);
671
+ spinner.stop("lint-staged done");
672
+ };
673
+ const commitCommand = (command) => {
674
+ command.command("commit").description("Commit a message").action(async () => {
675
+ console.clear();
676
+ const title = colors.bgRed(` ${await getCurrentBranch()} `);
677
+ intro(`${colors.bgCyan(" Current Branch: ")} ${title}`);
678
+ const { staged, unstaged } = await getGitStatus();
679
+ if (staged.length === 0 && unstaged.length === 0) {
680
+ ErrorMessage({ text: "No changes detected. Nothing to commit." });
681
+ exit(0);
682
+ }
683
+ log.show(`Changes to be committed:\n${staged.map((text) => colors.green(text)).join("\n")}`, { type: "success" });
684
+ const selectedStaged = staged;
685
+ if (unstaged.length > 0) {
686
+ const selectedFiles = await createCheckbox({
687
+ message: "Select files to stage for commit (optional):",
688
+ options: createOptions(unstaged),
689
+ required: false
690
+ });
691
+ selectedStaged.push(...selectedFiles);
692
+ if (selectedFiles.length > 0) {
693
+ await addFiles(selectedFiles);
694
+ createNote({
695
+ message: `Added ${selectedFiles.length} unstaged file(s) to staging area.\n${selectedFiles.map((file) => colors.green(file)).join("\n")}`,
696
+ title: "Add Files"
697
+ });
698
+ }
699
+ }
700
+ if (selectedStaged.length === 0) {
701
+ ErrorMessage({ text: "No staged files. Nothing to commit." });
702
+ exit(0);
703
+ }
704
+ await handleLint();
705
+ const options = await loadConfig({ sources: [{
706
+ files: "commitlint.config",
707
+ extensions: [
708
+ "js",
709
+ "ts",
710
+ "cjs",
711
+ "mjs",
712
+ "json",
713
+ ""
714
+ ]
715
+ }] });
716
+ const commitType = await createSelect({
717
+ message: "Select type:",
718
+ options: mergeCommitTypeEnumOptions((options.config ?? commitlintConfig).rules["type-enum"][2])
719
+ });
720
+ const commitScope = await createSelect({
721
+ message: "Select scope:",
722
+ options: mergeCommitScopeEnumOptions((options.config ?? commitlintConfig).rules["scope-enum"][2])
723
+ });
724
+ const commitTitle = await createInput({
725
+ message: "Write a brief title describing the commit:",
726
+ validate(value) {
727
+ if (!value?.trim()) return "Title is required";
728
+ if (value.length > 80) return "Title must be less than 80 characters";
729
+ }
730
+ });
731
+ const commitBody = await createInput({ message: "Write a detailed description of the changes (optional):" });
732
+ const ticket = await getTicket();
733
+ const scopeMessage = commitScope ? `(${commitScope})` : "";
734
+ const message = `${commitType}${scopeMessage}: ${ticket} ${commitTitle}\n${commitBody}`;
735
+ createNote({
736
+ message: `${colors.blue(commitType)}${colors.green(scopeMessage)}: ${colors.redBright(ticket)} ${commitTitle}\n${commitBody}`,
737
+ title: "Commit Message"
738
+ });
739
+ if (!await createConfirm({ message: "Are you sure you want to commit?" })) return;
740
+ await handleCommit(message);
741
+ await createConfirm({ message: "Do you want to push to remote?" }) && await pushInteractive();
742
+ outro(colors.bgGreen(" Git Commit Success "));
743
+ });
744
+ };
745
+ const REGEX_SLASH_TAG = /* @__PURE__ */ new RegExp(/\/(\w+-\d+)/);
746
+ const REGEX_START_TAG = /* @__PURE__ */ new RegExp(/^(\w+-\d+)/);
747
+ const REGEX_START_UND = /* @__PURE__ */ new RegExp(/^([A-Z]+-[[a-zA-Z\]\d]+)_/);
748
+ const REGEX_SLASH_UND = /* @__PURE__ */ new RegExp(/\/([A-Z]+-[[a-zA-Z\]\d]+)_/);
749
+ const REGEX_SLASH_NUM = /* @__PURE__ */ new RegExp(/\/(\d+)/);
750
+ const REGEX_START_NUM = /* @__PURE__ */ new RegExp(/^(\d+)/);
751
+ const getTicket = async () => {
752
+ const branch = await getCurrentBranch();
753
+ const chain = [
754
+ REGEX_START_UND,
755
+ REGEX_SLASH_UND,
756
+ REGEX_SLASH_TAG,
757
+ REGEX_SLASH_NUM,
758
+ REGEX_START_TAG,
759
+ REGEX_START_NUM
760
+ ];
761
+ for (const regex of chain) {
762
+ const match = branch.match(regex);
763
+ if (match) return match[1];
764
+ }
765
+ return branch;
766
+ };
767
+
768
+ //#endregion
769
+ //#region src/commands/diff.ts
770
+ const handleDiff = async (branch, { isLocal }) => {
771
+ console.log("🚀 : handleDiff : branch:", branch, isLocal);
772
+ const currentBranch = await getCurrentBranch();
773
+ if (!currentBranch) {
774
+ log.error("Could not determine current branch");
775
+ return;
776
+ }
777
+ const diffArgs = branch === currentBranch ? ["diff"] : ["diff", `${branch}...${currentBranch}`];
778
+ log.show(`Showing diff between ${branch === currentBranch ? "working directory and HEAD" : `${branch} and ${currentBranch}`}`);
779
+ const process = x("git", diffArgs);
780
+ let hasOutput = false;
781
+ for await (const line of process) {
782
+ hasOutput = true;
783
+ log.show(line);
784
+ }
785
+ const { exitCode, stderr } = await process;
786
+ if (exitCode) {
787
+ log.error(`Failed to diff. Command exited with code ${exitCode}.`);
788
+ if (stderr) log.error(stderr);
789
+ } else if (!hasOutput) log.show("No differences found.", { type: "info" });
790
+ };
791
+ function diffCommand(command) {
792
+ command.command("diff").alias("di").description("Show differences between branches or working directory").option("-l, --local", "Diff local branch", true).option("-r, --remote", "Diff remote branch").action(async (options) => {
793
+ const { branches } = options.remote ? await getRemoteBranches() : await getLocalBranches();
794
+ if (!branches || branches.length === 0) {
795
+ log.error("No branches found. Please check your git repository.");
796
+ return;
797
+ }
798
+ const selectedBranch = await createSelect({
799
+ message: "Select the branch to diff",
800
+ options: createOptions(branches)
801
+ });
802
+ if (!selectedBranch) {
803
+ log.error("No branch selected. Aborting diff operation.");
804
+ return;
805
+ }
806
+ await handleDiff(selectedBranch, { isLocal: !options.remote });
807
+ });
808
+ }
809
+
810
+ //#endregion
811
+ //#region src/commands/list.ts
812
+ function listCommand(command) {
813
+ command.command("list").alias("ls").description("List git branches").option("-l, --local", "List local branches").option("-r, --remote", "List remote branches").option("-a, --all", "List all branches", true).action(async (options) => {
814
+ if (options.all) {
815
+ const { branches: localBranches, currentBranch } = await getLocalBranches();
816
+ const { branches: remoteBranches } = await getRemoteBranches();
817
+ if (!localBranches.length && !remoteBranches.length) {
818
+ log.error("No branches found. Please check your git repository.");
819
+ return;
820
+ }
821
+ log.show(`Local ${localBranches.length} branches`, {
822
+ symbol: "🔖",
823
+ colors: colors.bgGreen
824
+ });
825
+ for (const branch of localBranches) if (branch === currentBranch) log.show(`${branch} (current)`, { type: "info" });
826
+ else log.show(branch, { type: "step" });
827
+ log.show(`Remote ${remoteBranches.length} branches`, {
828
+ symbol: "🔖",
829
+ colors: colors.bgYellow
830
+ });
831
+ for (const branch of remoteBranches) if (branch === currentBranch) log.show(`${branch} (current)`, { type: "info" });
832
+ else log.show(branch, { type: "step" });
833
+ } else if (options.local) {
834
+ const { branches } = await getLocalBranches();
835
+ if (!branches || branches.length === 0) {
836
+ log.error("No local branches found. Please check your git repository.");
837
+ return;
838
+ }
839
+ log.info(`Found ${branches.length} local branches:`);
840
+ for (const branch of branches) log.info(branch);
841
+ } else {
842
+ const { branches } = await getRemoteBranches();
843
+ if (!branches || branches.length === 0) {
844
+ log.error("No remote branches found. Please check your git repository.");
845
+ return;
846
+ }
847
+ log.info(`Found ${branches.length} remote branches:`);
848
+ for (const branch of branches) log.info(branch);
849
+ }
850
+ });
851
+ }
852
+
853
+ //#endregion
854
+ //#region src/commands/merge.ts
855
+ const handleMerge = async (branch) => {
856
+ const spinner = createSpinner(`Merging branch ${branch}...`);
857
+ const args = ["merge", branch];
858
+ const stashName = await handleGitStash();
859
+ const [error] = await xASync("git", args, { nodeOptions: { stdio: "inherit" } });
860
+ if (error) return;
861
+ spinner.stop(`Successfully merged branch ${branch}.`);
862
+ stashName && handleGitPop(stashName);
863
+ };
864
+ function mergeCommand(command) {
865
+ command.command("merge").alias("mg").argument("[branch]", "The branch to merge").option("-l, --local", "Merge a local branch").option("-r, --remote", "Merge a remote branch").option("-b, --branch <branch>", "Create and merge a new branch").description("Merge a branch").action(async (branch, params) => {
866
+ let isLocal = params.local;
867
+ if (branch) {
868
+ handleMerge(branch);
869
+ return;
870
+ }
871
+ if (isEmpty(params)) isLocal = await createSelect({
872
+ message: "Select the branch type",
873
+ options: [{
874
+ label: "Remote",
875
+ value: false
876
+ }, {
877
+ label: "Local",
878
+ value: true
879
+ }],
880
+ initialValue: false
881
+ });
882
+ if (isLocal) {
883
+ const { options } = await getLocalOptions();
884
+ handleMerge(await createSearch({
885
+ message: "Select the branch to merge",
886
+ options
887
+ }));
888
+ } else {
889
+ const { options } = await getRemoteOptions();
890
+ const selectedBranch = await createSearch({
891
+ message: "Select the branch to merge",
892
+ options
893
+ });
894
+ if (await createConfirm({ message: `Do you want to merge ${colors.bgRed(selectedBranch)}?` })) handleMerge(selectedBranch);
895
+ }
896
+ });
897
+ }
898
+
899
+ //#endregion
900
+ //#region src/commands/pull.ts
901
+ function pullCommand(command) {
902
+ command.command("pull").alias("pl").description("Pull git branch").option("-r, --rebase", "Use rebase mode instead of merge").option("-m, --merge", "Use merge mode (default)").action(async (options) => {
903
+ const { options: branchOptions, currentBranch } = await getRemoteOptions();
904
+ if (!branchOptions.length) {
905
+ log.error("No branches found. Please check your git repository.");
906
+ return;
907
+ }
908
+ const selectedBranch = await createSelect({
909
+ message: "Select the branch to pull",
910
+ options: branchOptions,
911
+ initialValue: currentBranch
912
+ });
913
+ if (!selectedBranch) {
914
+ log.error("No branch selected. Aborting pull operation.");
915
+ return;
916
+ }
917
+ let useRebase = options.rebase === true;
918
+ if (!options.rebase && !options.merge) useRebase = await createSelect({
919
+ message: "Select pull mode",
920
+ options: [{
921
+ label: "Merge (default)",
922
+ value: "merge",
923
+ hint: "git pull origin <branch>"
924
+ }, {
925
+ label: "Rebase",
926
+ value: "rebase",
927
+ hint: "git pull --rebase origin <branch>"
928
+ }],
929
+ initialValue: "merge"
930
+ }) === "rebase";
931
+ const stashName = await handleGitStash();
932
+ await handleGitPull(selectedBranch, { rebase: useRebase });
933
+ stashName && handleGitPop(stashName);
934
+ });
935
+ }
936
+
937
+ //#endregion
938
+ //#region src/constants/stash.ts
939
+ const HELP_MESSAGE = {
940
+ main: createHelpExample("ng stash", "ng stash save \"work in progress\"", "ng stash ls", "ng stash pop", "ng stash drop"),
941
+ save: createHelpExample("ng stash save \"work in progress\""),
942
+ list: createHelpExample("ng stash ls"),
943
+ pop: createHelpExample("ng stash pop"),
944
+ drop: createHelpExample("ng stash drop")
945
+ };
946
+
947
+ //#endregion
948
+ //#region src/commands/stash.ts
949
+ var StashCommand = /* @__PURE__ */ function(StashCommand) {
950
+ StashCommand["POP"] = "pop";
951
+ StashCommand["LIST"] = "list";
952
+ StashCommand["SAVE"] = "save";
953
+ StashCommand["DROP"] = "drop";
954
+ return StashCommand;
955
+ }(StashCommand || {});
956
+ const handleCheck = (callback) => async () => {
957
+ const stashes = await handleGitStashCheck();
958
+ if (stashes.length === 0) {
959
+ log.show("No stash found.", { type: "error" });
960
+ return;
961
+ }
962
+ return callback(stashes);
963
+ };
964
+ /**
965
+ * 获取 stash 中的文件列表
966
+ * @param stashRef - stash 引用,如 "stash@{0}"
967
+ */
968
+ const getStashFiles = async (stashRef) => {
969
+ const [error, result] = await xASync("git", [
970
+ "stash",
971
+ "show",
972
+ stashRef,
973
+ "--name-only"
974
+ ], { quiet: true });
975
+ if (error) return [];
976
+ return result.stdout.split("\n").filter((line) => line.trim());
977
+ };
978
+ /**
979
+ * 从 stash 条目中提取 stash 引用
980
+ * @param stashEntry - 完整的 stash 条目,如 "stash@{0}: On main: message"
981
+ */
982
+ const extractStashRef = (stashEntry) => {
983
+ const match = stashEntry.match(/^(stash@\{\d+\})/);
984
+ if (match?.[1]) return match[1];
985
+ return stashEntry.split(":")[0] ?? stashEntry;
986
+ };
987
+ const handlePop = handleCheck(async (stashes) => {
988
+ const selectedStashes = await createCheckbox({
989
+ message: "Select the stash to pop",
990
+ options: stashes.map((stash) => ({
991
+ label: stash,
992
+ value: stash
993
+ }))
994
+ });
995
+ for await (const stash of selectedStashes) {
996
+ const [error] = await xASync("git", [
997
+ "stash",
998
+ "pop",
999
+ extractStashRef(stash)
1000
+ ]);
1001
+ if (error) log.show("Failed to pop stash.", { type: "error" });
1002
+ else log.show("Successfully popped changes.", { type: "success" });
1003
+ }
1004
+ });
1005
+ const handleList = handleCheck(async (stashes) => {
1006
+ log.show(`\n${colors.bold(`📦 Found ${stashes.length} stash(es)`)}\n`);
1007
+ for await (const stash of stashes) {
1008
+ const files = await getStashFiles(extractStashRef(stash));
1009
+ log.show(colors.cyan(`━━━ ${stash} ━━━`));
1010
+ if (files.length > 0) {
1011
+ log.show(colors.dim(` ${files.length} file(s) changed:`));
1012
+ for (const file of files) log.show(colors.yellow(` • ${file}`));
1013
+ } else log.show(colors.dim(" (no files)"));
1014
+ log.show("");
1015
+ }
1016
+ });
1017
+ const handleDrop = handleCheck(async (stashes) => {
1018
+ const selectedStashes = await createCheckbox({
1019
+ message: "Select the stash to clear",
1020
+ options: createOptions(stashes)
1021
+ });
1022
+ for await (const stash of selectedStashes) {
1023
+ const stashRef = extractStashRef(stash);
1024
+ if (!stashRef) {
1025
+ log.show("Invalid stash name.", { type: "error" });
1026
+ exit(0);
1027
+ }
1028
+ const [error] = await xASync("git", [
1029
+ "stash",
1030
+ StashCommand.DROP,
1031
+ stashRef
1032
+ ]);
1033
+ if (error) log.show("Failed to drop stash.", { type: "error" });
1034
+ else log.show(`Successfully dropped stash: ${stashRef}`, { type: "success" });
1035
+ }
1036
+ });
1037
+ const handleClear = handleCheck(async () => {
1038
+ const [error] = await xASync("git", ["stash", "clear"]);
1039
+ if (error) log.show("Failed to clear stashes.", { type: "error" });
1040
+ else log.show("Successfully cleared stashes.", { type: "success" });
1041
+ });
1042
+ const stashCommand = (command) => {
1043
+ const stashCmd = command.command("stash").alias("st").description("Git stash management").addHelpText("after", HELP_MESSAGE.main);
1044
+ stashCmd.command("save [message]").alias("s").description("Save current changes to stash").action(async (message) => {
1045
+ await handleGitStash(message);
1046
+ });
1047
+ stashCmd.command("list").alias("ls").alias("l").description("List all stashes").action(async () => {
1048
+ await handleList();
1049
+ });
1050
+ stashCmd.command("pop").alias("p").description("Pop the most recent stash").action(async () => {
1051
+ await handlePop();
1052
+ });
1053
+ stashCmd.command("drop").alias("d").description("Drop/clear stashes").action(async () => {
1054
+ await handleDrop();
1055
+ });
1056
+ stashCmd.command("clear").alias("c").description("clear stashes").action(async () => {
1057
+ await handleClear();
1058
+ });
1059
+ return stashCmd;
1060
+ };
1061
+
1062
+ //#endregion
1063
+ //#region src/index.ts
1064
+ const pkg = readPackage(import.meta, "..");
1065
+ const init = () => {
1066
+ const command = createCommand("ng").version(pkg.version).description(`${pkg.name} CLI helper for git`).addHelpText("after", HELP_MESSAGE$1.main);
1067
+ pullCommand(command);
1068
+ listCommand(command);
1069
+ pushCommand(command);
1070
+ checkoutCommand(command);
1071
+ branchCommand(command);
1072
+ diffCommand(command);
1073
+ mergeCommand(command);
1074
+ stashCommand(command);
1075
+ commitCommand(command);
1076
+ return command;
1077
+ };
1078
+ const run = async () => {
1079
+ if (!await checkGitRepository()) {
1080
+ ErrorMessage({ text: "Not a git repository" });
1081
+ exit(0);
1082
+ }
1083
+ init().parse(process.argv);
1084
+ };
1085
+
1086
+ //#endregion
1087
+ export { init, pkg, run };
1088
+ //# sourceMappingURL=index.js.map