@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/README.md +44 -0
- package/bin/index.mjs +5 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1088 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
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
|