@nemo-cli/git 0.1.2 → 0.1.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 +696 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1207 -74
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,10 +1,220 @@
|
|
|
1
|
-
import { addFiles, colors, createCheckbox, createCommand, createConfirm, createHelpExample, createInput, createNote, createOptions,
|
|
2
|
-
import { BigText, ErrorMessage, Message } from "@nemo-cli/ui";
|
|
1
|
+
import { addFiles, colors, createCheckbox, createCommand, createConfirm, createHelpExample, createInput, createNote, createOptions, createSelect, createSpinner, exit, getCurrentBranch, getGitStatus, intro, isEmpty, isString, loadConfig, log, outro, readPackage, safeAwait, x, xASync } from "@nemo-cli/shared";
|
|
2
|
+
import { BigText, ErrorMessage, Message, renderHistViewer, renderStashList, renderStatusViewer } from "@nemo-cli/ui";
|
|
3
|
+
import path, { dirname, join } from "node:path";
|
|
4
|
+
import readline from "node:readline";
|
|
3
5
|
import { spawn } from "node:child_process";
|
|
4
|
-
import { unlinkSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { tmpdir } from "node:os";
|
|
6
|
-
import {
|
|
6
|
+
import { readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { homedir, tmpdir } from "node:os";
|
|
8
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
7
9
|
|
|
10
|
+
//#region src/commands/blame.ts
|
|
11
|
+
const MAX_DIFF_LINES = 50;
|
|
12
|
+
/**
|
|
13
|
+
* Registers the 'ng blame' command for viewing file commit history with interactive navigation
|
|
14
|
+
* @param command - The commander Command instance
|
|
15
|
+
* @returns The modified command instance
|
|
16
|
+
*/
|
|
17
|
+
const blameCommand = (command) => {
|
|
18
|
+
command.command("blame [file]").description("View file commit history with interactive navigation").action(async (file) => {
|
|
19
|
+
if (!file) {
|
|
20
|
+
log.show("Please specify a file path.", { type: "error" });
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
await handleBlame(file);
|
|
24
|
+
});
|
|
25
|
+
return command;
|
|
26
|
+
};
|
|
27
|
+
const commitCache = /* @__PURE__ */ new Map();
|
|
28
|
+
/**
|
|
29
|
+
* Finalizes a commit object from partial data
|
|
30
|
+
* @param currentCommit - Partial commit data
|
|
31
|
+
* @param diffLines - Array of diff lines
|
|
32
|
+
* @returns Complete Commit object or null if invalid
|
|
33
|
+
*/
|
|
34
|
+
const finalizeCommit = (currentCommit, diffLines) => {
|
|
35
|
+
if (!currentCommit?.hash || !currentCommit.author || !currentCommit.message) return null;
|
|
36
|
+
return {
|
|
37
|
+
hash: currentCommit.hash,
|
|
38
|
+
author: currentCommit.author,
|
|
39
|
+
date: currentCommit.date ?? "",
|
|
40
|
+
message: currentCommit.message,
|
|
41
|
+
diff: diffLines.join("\n")
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
const handleBlame = async (filePath) => {
|
|
45
|
+
const resolvedPath = path.resolve(filePath);
|
|
46
|
+
const cwd = process.cwd();
|
|
47
|
+
if (!resolvedPath.startsWith(cwd + path.sep) && resolvedPath !== cwd) {
|
|
48
|
+
log.show("❌ Error: File path must be within current directory", { type: "error" });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const [statError] = await xASync("test", ["-f", resolvedPath], { quiet: true });
|
|
52
|
+
if (statError) {
|
|
53
|
+
log.show(`❌ Error: File not found: ${filePath}`, { type: "error" });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (!commitCache.has(resolvedPath)) {
|
|
57
|
+
const [error, result] = await xASync("git", [
|
|
58
|
+
"log",
|
|
59
|
+
"--follow",
|
|
60
|
+
"-p",
|
|
61
|
+
`--pretty=format:%H%x00%an%x00%ad%x00%s`,
|
|
62
|
+
"--",
|
|
63
|
+
resolvedPath
|
|
64
|
+
]);
|
|
65
|
+
if (error) {
|
|
66
|
+
handleGitError(error, "git log");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
commitCache.set(resolvedPath, parseCommitsFromLog(result.stdout));
|
|
70
|
+
}
|
|
71
|
+
const commits = commitCache.get(resolvedPath);
|
|
72
|
+
if (commits.length === 0) {
|
|
73
|
+
log.show(`No git history found for ${filePath}`, { type: "warn" });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
log.show(colors.bold(`Found ${commits.length} commits for ${filePath}`));
|
|
77
|
+
log.show(colors.dim("Use [n/p] to navigate, [j] to jump, [q] to quit"));
|
|
78
|
+
await enterInteractiveBlameMode(resolvedPath, commits);
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Parses git log output into structured Commit objects
|
|
82
|
+
* @param logOutput - Raw git log output with null separators
|
|
83
|
+
* @returns Array of parsed Commit objects
|
|
84
|
+
*/
|
|
85
|
+
const parseCommitsFromLog = (logOutput) => {
|
|
86
|
+
const commits = [];
|
|
87
|
+
const lines = logOutput.split("\n");
|
|
88
|
+
let currentCommit = null;
|
|
89
|
+
let diffLines = [];
|
|
90
|
+
for (const line of lines) if (line.includes("\0")) {
|
|
91
|
+
const finalized = finalizeCommit(currentCommit, diffLines);
|
|
92
|
+
if (finalized) commits.push(finalized);
|
|
93
|
+
const [hash, author, date, message] = line.split("\0");
|
|
94
|
+
if (hash && hash.length >= 8) {
|
|
95
|
+
currentCommit = {
|
|
96
|
+
hash: hash.substring(0, 8),
|
|
97
|
+
author: author?.trim() ?? "",
|
|
98
|
+
date: date?.trim() ?? "",
|
|
99
|
+
message: message?.trim() ?? ""
|
|
100
|
+
};
|
|
101
|
+
diffLines = [];
|
|
102
|
+
}
|
|
103
|
+
} else if (currentCommit) diffLines.push(line);
|
|
104
|
+
const finalized = finalizeCommit(currentCommit, diffLines);
|
|
105
|
+
if (finalized) commits.push(finalized);
|
|
106
|
+
return commits;
|
|
107
|
+
};
|
|
108
|
+
const showCommit = (commit, index, total) => {
|
|
109
|
+
log.clearScreen();
|
|
110
|
+
log.show(`📝 Commit ${index}/${total}`);
|
|
111
|
+
log.show(`${colors.cyan(commit.hash)} - ${colors.yellow(commit.author)} - ${colors.dim(commit.date)}`);
|
|
112
|
+
log.show(`${commit.message}`);
|
|
113
|
+
log.show(colors.bold("--- Diff ---"));
|
|
114
|
+
if (commit.diff.includes("Binary files differ")) {
|
|
115
|
+
log.show("📄 Binary file - diff not available");
|
|
116
|
+
log.show(commit.diff);
|
|
117
|
+
} else {
|
|
118
|
+
const diffLines = commit.diff.split("\n");
|
|
119
|
+
if (diffLines.length > MAX_DIFF_LINES) {
|
|
120
|
+
log.show(colors.dim(`(Showing first ${MAX_DIFF_LINES} lines of ${diffLines.length})`));
|
|
121
|
+
log.show(diffLines.slice(0, MAX_DIFF_LINES).join("\n"));
|
|
122
|
+
log.show(colors.dim("\n... (truncated)"));
|
|
123
|
+
} else log.show(commit.diff);
|
|
124
|
+
}
|
|
125
|
+
log.show(colors.bold("\n--- Actions ---"));
|
|
126
|
+
log.show("[n] Next commit [p] Previous commit [j] Jump [q] Quit");
|
|
127
|
+
};
|
|
128
|
+
const enterInteractiveBlameMode = async (filePath, commits) => {
|
|
129
|
+
if (commits.length === 0) {
|
|
130
|
+
log.show("No commits to display", { type: "warn" });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
let currentIndex = 0;
|
|
134
|
+
while (true) {
|
|
135
|
+
const currentCommit = commits[currentIndex];
|
|
136
|
+
if (!currentCommit) break;
|
|
137
|
+
await showCommit(currentCommit, currentIndex + 1, commits.length);
|
|
138
|
+
let input;
|
|
139
|
+
try {
|
|
140
|
+
input = await createCheckbox({
|
|
141
|
+
message: "Enter action:",
|
|
142
|
+
options: [
|
|
143
|
+
{
|
|
144
|
+
label: currentIndex < commits.length - 1 ? "Next commit" : "Next commit (already at latest)",
|
|
145
|
+
value: "n"
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
label: currentIndex > 0 ? "Previous commit" : "Previous commit (already at earliest)",
|
|
149
|
+
value: "p"
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
label: "Jump to commit",
|
|
153
|
+
value: "j"
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
label: "Quit",
|
|
157
|
+
value: "q"
|
|
158
|
+
}
|
|
159
|
+
]
|
|
160
|
+
});
|
|
161
|
+
} catch (error) {
|
|
162
|
+
log.show("Error reading input. Exiting...", { type: "error" });
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (!input || input.length === 0) {
|
|
166
|
+
log.show("No input received. Exiting...", { type: "warn" });
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
if (input[0] === "q") exit(0);
|
|
170
|
+
if (input[0] === "n" && currentIndex < commits.length - 1) currentIndex++;
|
|
171
|
+
else if (input[0] === "p" && currentIndex > 0) currentIndex--;
|
|
172
|
+
else if (input[0] === "j") {
|
|
173
|
+
log.show(`\nEnter commit number (1-${commits.length}):`);
|
|
174
|
+
const jumpInput = await readUserInput();
|
|
175
|
+
const jumpNum = Number.parseInt(jumpInput, 10);
|
|
176
|
+
if (!isNaN(jumpNum) && jumpNum >= 1 && jumpNum <= commits.length) currentIndex = jumpNum - 1;
|
|
177
|
+
else {
|
|
178
|
+
log.show(`Invalid number. Please enter 1-${commits.length}`, { type: "error" });
|
|
179
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
const readUserInput = () => {
|
|
185
|
+
return new Promise((resolve) => {
|
|
186
|
+
const rl = readline.createInterface({
|
|
187
|
+
input: process.stdin,
|
|
188
|
+
output: process.stdout
|
|
189
|
+
});
|
|
190
|
+
const cleanup = () => {
|
|
191
|
+
rl.close();
|
|
192
|
+
rl.removeAllListeners();
|
|
193
|
+
};
|
|
194
|
+
rl.on("SIGINT", () => {
|
|
195
|
+
cleanup();
|
|
196
|
+
exit(0);
|
|
197
|
+
});
|
|
198
|
+
rl.question("", (answer) => {
|
|
199
|
+
cleanup();
|
|
200
|
+
resolve(answer.trim());
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
};
|
|
204
|
+
const handleGitError = (error, context) => {
|
|
205
|
+
const errorMessage = error.message || String(error);
|
|
206
|
+
if (errorMessage.includes("not a git repository")) {
|
|
207
|
+
log.show("❌ Error: Not in a git repository", { type: "error" });
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (errorMessage.includes("does not exist") || errorMessage.includes("no such file")) {
|
|
211
|
+
log.show("❌ Error: File not found", { type: "error" });
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
log.show(`❌ Error: ${errorMessage}`, { type: "error" });
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
//#endregion
|
|
8
218
|
//#region src/constants/index.ts
|
|
9
219
|
const HELP_MESSAGE$1 = {
|
|
10
220
|
main: createHelpExample("ng --version", "ng --help", "ng <command> [option]"),
|
|
@@ -13,9 +223,142 @@ const HELP_MESSAGE$1 = {
|
|
|
13
223
|
branchClean: createHelpExample("ng branch clean --version", "ng branch clean --help")
|
|
14
224
|
};
|
|
15
225
|
|
|
226
|
+
//#endregion
|
|
227
|
+
//#region src/utils/stash-index.ts
|
|
228
|
+
/**
|
|
229
|
+
* Stash 索引文件路径
|
|
230
|
+
*/
|
|
231
|
+
const STASH_INDEX_FILENAME = "ng-stash-index.json";
|
|
232
|
+
/**
|
|
233
|
+
* 获取 Stash 索引文件的完整路径
|
|
234
|
+
* @returns Stash 索引文件路径,如果不在 Git 仓库中则返回 null
|
|
235
|
+
*/
|
|
236
|
+
async function getStashIndexPath() {
|
|
237
|
+
const gitRoot = await getGitRoot();
|
|
238
|
+
if (!gitRoot) return null;
|
|
239
|
+
return join(gitRoot, ".git", STASH_INDEX_FILENAME);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* 读取 Stash 索引文件
|
|
243
|
+
* @returns Stash 索引对象,如果文件不存在或读取失败则返回空对象
|
|
244
|
+
*/
|
|
245
|
+
async function readStashIndex() {
|
|
246
|
+
const indexPath = await getStashIndexPath();
|
|
247
|
+
if (!indexPath) {
|
|
248
|
+
log.show("Not in a Git repository. Cannot read stash index.", { type: "error" });
|
|
249
|
+
return {};
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
const content = await readFile(indexPath, "utf-8");
|
|
253
|
+
const index = JSON.parse(content);
|
|
254
|
+
if (typeof index !== "object" || index === null || Array.isArray(index)) throw new Error("Invalid stash index format: expected an object");
|
|
255
|
+
for (const [branchName, stashes] of Object.entries(index)) {
|
|
256
|
+
if (!Array.isArray(stashes)) throw new Error(`Invalid stash index format: branch "${branchName}" should have an array of stashes`);
|
|
257
|
+
for (const stash of stashes) if (typeof stash.stashRef !== "string" || typeof stash.timestamp !== "string" || typeof stash.createdAt !== "string" || typeof stash.message !== "string") throw new Error(`Invalid stash index format: stash metadata for branch "${branchName}" is missing required fields`);
|
|
258
|
+
}
|
|
259
|
+
return index;
|
|
260
|
+
} catch (error) {
|
|
261
|
+
const err = error;
|
|
262
|
+
if (err.code === "ENOENT") return {};
|
|
263
|
+
if (err instanceof SyntaxError) {
|
|
264
|
+
log.show(`Failed to parse stash index file: ${err.message}. The file may be corrupted.`, { type: "error" });
|
|
265
|
+
return {};
|
|
266
|
+
}
|
|
267
|
+
log.show(`Failed to read stash index file: ${err.message}`, { type: "error" });
|
|
268
|
+
return {};
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* 写入 Stash 索引文件
|
|
273
|
+
* @param index - Stash 索引对象
|
|
274
|
+
* @throws 如果写入失败会抛出错误
|
|
275
|
+
*/
|
|
276
|
+
async function writeStashIndex(index) {
|
|
277
|
+
const indexPath = await getStashIndexPath();
|
|
278
|
+
if (!indexPath) throw new Error("Not in a Git repository. Cannot write stash index.");
|
|
279
|
+
try {
|
|
280
|
+
await mkdir(dirname(indexPath), { recursive: true });
|
|
281
|
+
await writeFile(indexPath, JSON.stringify(index, null, 2), "utf-8");
|
|
282
|
+
} catch (error) {
|
|
283
|
+
const err = error;
|
|
284
|
+
log.show(`Failed to write stash index file: ${err.message}`, { type: "error" });
|
|
285
|
+
throw new Error(`Failed to write stash index: ${err.message}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* 获取分支的所有 Stash 元数据
|
|
290
|
+
* @param branchName - 分支名称
|
|
291
|
+
* @returns Stash 元数据数组,如果分支不存在则返回空数组
|
|
292
|
+
*/
|
|
293
|
+
async function getBranchStashes(branchName) {
|
|
294
|
+
return (await readStashIndex())[branchName] ?? [];
|
|
295
|
+
}
|
|
296
|
+
async function addStashMetadataWithDetails(branchName, metadata) {
|
|
297
|
+
const index = await readStashIndex();
|
|
298
|
+
if (!index[branchName]) index[branchName] = [];
|
|
299
|
+
index[branchName].push(metadata);
|
|
300
|
+
const indexPath = await getStashIndexPath();
|
|
301
|
+
if (!indexPath) throw new Error("Not in a Git repository");
|
|
302
|
+
const tmpPath = `${indexPath}.tmp.${Date.now()}`;
|
|
303
|
+
await mkdir(dirname(indexPath), { recursive: true });
|
|
304
|
+
await writeFile(tmpPath, JSON.stringify(index, null, 2), "utf-8");
|
|
305
|
+
try {
|
|
306
|
+
await rename(tmpPath, indexPath);
|
|
307
|
+
} catch (renameError) {
|
|
308
|
+
const err = renameError;
|
|
309
|
+
if (err.code === "EACCES" || err.code === "EBUSY" || err.code === "ENOENT") for (let i = 0; i < 3; i++) {
|
|
310
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
311
|
+
try {
|
|
312
|
+
await rename(tmpPath, indexPath);
|
|
313
|
+
return;
|
|
314
|
+
} catch {
|
|
315
|
+
if (i === 2) throw new Error(`Failed to write stash index after 3 retries: ${err.message}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
else throw err;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
async function updateStashStatus(branchName, internalId, status, error) {
|
|
322
|
+
const index = await readStashIndex();
|
|
323
|
+
if (!index[branchName]) return;
|
|
324
|
+
const stash = index[branchName].find((s) => s.internalId === internalId);
|
|
325
|
+
if (!stash) return;
|
|
326
|
+
stash.status = status;
|
|
327
|
+
if (error) stash.error = error;
|
|
328
|
+
await writeStashIndex(index);
|
|
329
|
+
}
|
|
330
|
+
async function cleanOldStashes(days = 30) {
|
|
331
|
+
const index = await readStashIndex();
|
|
332
|
+
const cutoffDate = Date.now() - days * 24 * 60 * 60 * 1e3;
|
|
333
|
+
let count = 0;
|
|
334
|
+
for (const [branchName, stashes] of Object.entries(index)) {
|
|
335
|
+
const before = stashes.length;
|
|
336
|
+
index[branchName] = stashes.filter((stash) => {
|
|
337
|
+
if (stash.status === "active") return true;
|
|
338
|
+
return new Date(stash.timestamp).getTime() >= cutoffDate;
|
|
339
|
+
});
|
|
340
|
+
count += before - index[branchName].length;
|
|
341
|
+
if (index[branchName].length === 0) delete index[branchName];
|
|
342
|
+
}
|
|
343
|
+
await writeStashIndex(index);
|
|
344
|
+
return count;
|
|
345
|
+
}
|
|
346
|
+
async function getAllStashes(filterStatus) {
|
|
347
|
+
const index = await readStashIndex();
|
|
348
|
+
const allStashes = [];
|
|
349
|
+
for (const stashes of Object.values(index)) allStashes.push(...stashes);
|
|
350
|
+
if (filterStatus) return allStashes.filter((s) => s.status === filterStatus);
|
|
351
|
+
return allStashes;
|
|
352
|
+
}
|
|
353
|
+
|
|
16
354
|
//#endregion
|
|
17
355
|
//#region src/utils.ts
|
|
18
356
|
const remotePrefix = /^origin\//;
|
|
357
|
+
const getRemotes = async () => {
|
|
358
|
+
const [error, result] = await xASync("git", ["remote"]);
|
|
359
|
+
if (error) throw new Error(`Failed to get remote repositories: ${error instanceof Error ? error.message : String(error)}`);
|
|
360
|
+
return { remotes: result.stdout.split("\n").filter((line) => line.trim()) };
|
|
361
|
+
};
|
|
19
362
|
const getRemoteBranches = async () => {
|
|
20
363
|
return { branches: (await x("git", [
|
|
21
364
|
"branch",
|
|
@@ -33,7 +376,7 @@ const getLocalBranches = async () => {
|
|
|
33
376
|
currentBranch: formatBranch$1(currentBranch)
|
|
34
377
|
};
|
|
35
378
|
};
|
|
36
|
-
const
|
|
379
|
+
const getRemoteBranchOptions = async () => {
|
|
37
380
|
const { branches } = await getRemoteBranches();
|
|
38
381
|
const currentBranch = await getCurrentBranch();
|
|
39
382
|
return {
|
|
@@ -45,7 +388,7 @@ const getRemoteOptions = async () => {
|
|
|
45
388
|
currentBranch
|
|
46
389
|
};
|
|
47
390
|
};
|
|
48
|
-
const
|
|
391
|
+
const getLocalBranchOptions = async () => {
|
|
49
392
|
const { branches, currentBranch } = await getLocalBranches();
|
|
50
393
|
return {
|
|
51
394
|
options: branches.map((branch) => ({
|
|
@@ -56,6 +399,18 @@ const getLocalOptions = async () => {
|
|
|
56
399
|
currentBranch
|
|
57
400
|
};
|
|
58
401
|
};
|
|
402
|
+
const getRemoteRepositoryOptions = async () => {
|
|
403
|
+
const { remotes } = await getRemotes();
|
|
404
|
+
const validRemotes = remotes.filter((remote) => remote.trim().length > 0);
|
|
405
|
+
const uniqueRemotes = Array.from(new Set(validRemotes));
|
|
406
|
+
return {
|
|
407
|
+
options: uniqueRemotes.map((remote) => ({
|
|
408
|
+
label: remote,
|
|
409
|
+
value: remote
|
|
410
|
+
})),
|
|
411
|
+
remotes: uniqueRemotes
|
|
412
|
+
};
|
|
413
|
+
};
|
|
59
414
|
/**
|
|
60
415
|
* 处理合并提交信息
|
|
61
416
|
*/
|
|
@@ -139,18 +494,18 @@ const handleMergeCommit = async () => {
|
|
|
139
494
|
}
|
|
140
495
|
};
|
|
141
496
|
const handleGitPull = async (branch, options = {}) => {
|
|
142
|
-
const { rebase = false } = options;
|
|
497
|
+
const { rebase = false, remote = "origin" } = options;
|
|
143
498
|
const modeText = rebase ? "rebase" : "merge";
|
|
144
499
|
log.show(`Pulling from remote (${modeText} mode)...`, { type: "step" });
|
|
145
500
|
try {
|
|
146
501
|
const [error, result] = await xASync("git", rebase ? [
|
|
147
502
|
"pull",
|
|
148
503
|
"--rebase",
|
|
149
|
-
|
|
504
|
+
remote,
|
|
150
505
|
branch
|
|
151
506
|
] : [
|
|
152
507
|
"pull",
|
|
153
|
-
|
|
508
|
+
remote,
|
|
154
509
|
branch
|
|
155
510
|
], { nodeOptions: { stdio: "inherit" } });
|
|
156
511
|
if (error) {
|
|
@@ -164,30 +519,100 @@ const handleGitPull = async (branch, options = {}) => {
|
|
|
164
519
|
return;
|
|
165
520
|
}
|
|
166
521
|
};
|
|
167
|
-
const
|
|
168
|
-
const
|
|
522
|
+
const handleGitStash = async (message, options) => {
|
|
523
|
+
const [unstagedError, unstagedResult] = await xASync("git", ["diff", "--name-only"]);
|
|
524
|
+
const unstagedFiles = unstagedError ? [] : unstagedResult.stdout.split("\n").filter(Boolean);
|
|
525
|
+
const [stagedError, stagedResult] = await xASync("git", [
|
|
526
|
+
"diff",
|
|
527
|
+
"--cached",
|
|
528
|
+
"--name-only"
|
|
529
|
+
]);
|
|
530
|
+
const stagedFiles = stagedError ? [] : stagedResult.stdout.split("\n").filter(Boolean);
|
|
531
|
+
const [untrackedError, untrackedResult] = await xASync("git", [
|
|
532
|
+
"ls-files",
|
|
533
|
+
"--others",
|
|
534
|
+
"--exclude-standard"
|
|
535
|
+
]);
|
|
536
|
+
const untrackedFiles = untrackedError ? [] : untrackedResult.stdout.split("\n").filter(Boolean);
|
|
537
|
+
const files = [...new Set([
|
|
538
|
+
...unstagedFiles,
|
|
539
|
+
...stagedFiles,
|
|
540
|
+
...untrackedFiles
|
|
541
|
+
])];
|
|
542
|
+
if (files.length === 0) {
|
|
543
|
+
log.show("No file changes to stash.");
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
const [, commitResult] = await xASync("git", ["rev-parse", "HEAD"]);
|
|
547
|
+
const commitHash = commitResult?.stdout.trim() || "";
|
|
548
|
+
const currentBranch = await getCurrentBranch();
|
|
549
|
+
const { branch = void 0, operation = "manual" } = options || {};
|
|
550
|
+
const now = /* @__PURE__ */ new Date();
|
|
551
|
+
let stashName;
|
|
552
|
+
if (message && message.trim()) stashName = message.trim();
|
|
553
|
+
else stashName = `${operation}:${currentBranch}@${now.toISOString().replace(/[:.]/g, "-").slice(0, 19)}`;
|
|
554
|
+
const internalId = `${Date.now()}_${operation}_${currentBranch.replace(/[/]/g, "_")}`;
|
|
169
555
|
const [error, result] = await xASync("git", [
|
|
170
556
|
"stash",
|
|
171
557
|
"save",
|
|
172
|
-
|
|
558
|
+
"-u",
|
|
559
|
+
stashName
|
|
173
560
|
]);
|
|
174
561
|
if (error) {
|
|
175
|
-
log.show(`Failed to stash changes. ${
|
|
562
|
+
log.show(`Failed to stash changes. ${stashName}`, { type: "error" });
|
|
176
563
|
return null;
|
|
177
564
|
}
|
|
178
|
-
if (result?.stdout.includes(
|
|
179
|
-
log.show(
|
|
180
|
-
return
|
|
565
|
+
if (!result?.stdout.includes(stashName)) {
|
|
566
|
+
log.show("No file changes to stash.");
|
|
567
|
+
return null;
|
|
181
568
|
}
|
|
182
|
-
|
|
183
|
-
|
|
569
|
+
const stashRefMatch = result.stdout.match(/stash@\{(\d+)\}/);
|
|
570
|
+
const metadata = {
|
|
571
|
+
stashRef: stashRefMatch ? stashRefMatch[0] : "stash@{0}",
|
|
572
|
+
timestamp: now.toISOString(),
|
|
573
|
+
createdAt: now.toISOString(),
|
|
574
|
+
message: stashName,
|
|
575
|
+
internalId,
|
|
576
|
+
operation,
|
|
577
|
+
currentBranch,
|
|
578
|
+
targetBranch: branch,
|
|
579
|
+
files,
|
|
580
|
+
status: "active",
|
|
581
|
+
commitHash
|
|
582
|
+
};
|
|
583
|
+
await addStashMetadataWithDetails(currentBranch, metadata);
|
|
584
|
+
log.show(`Successfully stashed changes. ${stashName}`, { type: "success" });
|
|
585
|
+
return {
|
|
586
|
+
metadata,
|
|
587
|
+
stashName
|
|
588
|
+
};
|
|
184
589
|
};
|
|
185
590
|
const handleGitStashCheck = async () => {
|
|
186
591
|
const [error, result] = await xASync("git", ["stash", "list"]);
|
|
187
592
|
if (error) return [];
|
|
188
593
|
return result.stdout.split("\n").filter((line) => line.trim());
|
|
189
594
|
};
|
|
190
|
-
const handleGitPop = async (
|
|
595
|
+
const handleGitPop = async (stashOrResult) => {
|
|
596
|
+
if (typeof stashOrResult !== "string") {
|
|
597
|
+
const { metadata } = stashOrResult;
|
|
598
|
+
const [error, result] = await xASync("git", [
|
|
599
|
+
"stash",
|
|
600
|
+
"pop",
|
|
601
|
+
metadata.stashRef
|
|
602
|
+
]);
|
|
603
|
+
if (error) {
|
|
604
|
+
log.show(`Failed to pop stash: ${error.message}`, { type: "error" });
|
|
605
|
+
if (metadata.internalId && metadata.currentBranch) await updateStashStatus(metadata.currentBranch, metadata.internalId, "popped", error.message);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
createNote({
|
|
609
|
+
message: result.stdout,
|
|
610
|
+
title: "Successfully popped changes."
|
|
611
|
+
});
|
|
612
|
+
if (metadata.internalId && metadata.currentBranch) await updateStashStatus(metadata.currentBranch, metadata.internalId, "popped");
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
const branch = stashOrResult;
|
|
191
616
|
const stashName = (await handleGitStashCheck()).find((stash) => stash.includes(branch));
|
|
192
617
|
if (!stashName) {
|
|
193
618
|
log.show(`No stash found for this branch: ${colors.bgRed(branch)}.`, { type: "warn" });
|
|
@@ -231,12 +656,17 @@ const isBranchMergedToMain = async (branches) => {
|
|
|
231
656
|
};
|
|
232
657
|
}));
|
|
233
658
|
};
|
|
659
|
+
const getGitRoot = async () => {
|
|
660
|
+
const [error, result] = await xASync("git", ["rev-parse", "--show-toplevel"]);
|
|
661
|
+
if (error) return "";
|
|
662
|
+
return result.stdout.trim();
|
|
663
|
+
};
|
|
234
664
|
const checkGitRepository = async () => {
|
|
235
665
|
try {
|
|
236
666
|
const [error, result] = await xASync("git", ["rev-parse", "--is-inside-work-tree"], { quiet: true });
|
|
237
667
|
if (error) return false;
|
|
238
668
|
return result.stdout.trim() === "true";
|
|
239
|
-
} catch (
|
|
669
|
+
} catch (_err) {
|
|
240
670
|
return false;
|
|
241
671
|
}
|
|
242
672
|
};
|
|
@@ -373,20 +803,46 @@ function branchCommand(command) {
|
|
|
373
803
|
|
|
374
804
|
//#endregion
|
|
375
805
|
//#region src/commands/checkout.ts
|
|
806
|
+
/**
|
|
807
|
+
* 查找并恢复当前分支的 stash
|
|
808
|
+
* 当用户 checkout 回之前的分支时,自动恢复该分支的 stash
|
|
809
|
+
*/
|
|
810
|
+
const restoreBranchStash = async (branchName) => {
|
|
811
|
+
try {
|
|
812
|
+
const activeStashes = (await getBranchStashes(branchName)).filter((s) => s.status === "active" && s.operation === "checkout").sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
813
|
+
if (activeStashes.length === 0) return;
|
|
814
|
+
const latestStash = activeStashes[0];
|
|
815
|
+
if (!latestStash) return;
|
|
816
|
+
log.show(`Found stashed changes from previous checkout to ${colors.bgYellow(latestStash.targetBranch || "unknown")}. Restoring...`, { type: "info" });
|
|
817
|
+
await handleGitPop({
|
|
818
|
+
metadata: latestStash,
|
|
819
|
+
stashName: latestStash.message
|
|
820
|
+
});
|
|
821
|
+
} catch (_error) {
|
|
822
|
+
log.show(`Failed to restore stash for branch ${branchName}`, { type: "warn" });
|
|
823
|
+
}
|
|
824
|
+
};
|
|
376
825
|
const handleCheckout = async (branch, { isNew = false, isRemote = false } = {}) => {
|
|
826
|
+
const currentBranchName = await getCurrentBranch();
|
|
377
827
|
const args = ["checkout"];
|
|
378
828
|
if (isNew || isRemote) args.push("-b");
|
|
379
829
|
args.push(branch);
|
|
380
830
|
if (isRemote) args.push(`origin/${branch}`);
|
|
381
|
-
const
|
|
831
|
+
const stashResult = branch !== currentBranchName ? await handleGitStash(void 0, {
|
|
832
|
+
branch,
|
|
833
|
+
operation: "checkout"
|
|
834
|
+
}) : null;
|
|
382
835
|
const process = x("git", args);
|
|
836
|
+
log.show(stashResult?.stashName, { type: "info" });
|
|
383
837
|
for await (const line of process) log.show(line);
|
|
384
838
|
const { exitCode, stderr } = await process;
|
|
385
839
|
if (exitCode) {
|
|
386
840
|
log.show(`Failed to checkout branch ${branch}. Command exited with code ${exitCode}.`, { type: "error" });
|
|
387
841
|
log.show(stderr, { type: "error" });
|
|
388
|
-
} else
|
|
389
|
-
|
|
842
|
+
} else {
|
|
843
|
+
log.show(`Successfully checked out branch ${branch}.`, { type: "success" });
|
|
844
|
+
await restoreBranchStash(branch);
|
|
845
|
+
}
|
|
390
846
|
};
|
|
391
847
|
function checkoutCommand(command) {
|
|
392
848
|
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) => {
|
|
@@ -425,13 +881,13 @@ function checkoutCommand(command) {
|
|
|
425
881
|
initialValue: true
|
|
426
882
|
});
|
|
427
883
|
if (isLocal) {
|
|
428
|
-
const { options } = await
|
|
884
|
+
const { options } = await getLocalBranchOptions();
|
|
429
885
|
handleCheckout(await createSelect({
|
|
430
886
|
message: `Select the ${colors.bgGreen(" local ")} branch to checkout`,
|
|
431
887
|
options
|
|
432
888
|
}));
|
|
433
889
|
} else {
|
|
434
|
-
const { options } = await
|
|
890
|
+
const { options } = await getRemoteBranchOptions();
|
|
435
891
|
const selectedBranch = await createSelect({
|
|
436
892
|
message: `Select the ${colors.bgYellow(" remote ")} branch to checkout`,
|
|
437
893
|
options
|
|
@@ -581,7 +1037,7 @@ const commitlintConfig = {
|
|
|
581
1037
|
};
|
|
582
1038
|
const mergeCommitTypeEnumOptions = (options) => {
|
|
583
1039
|
return options.map((option) => {
|
|
584
|
-
const commitTypeOption = commitTypeOptions.find((commitTypeOption
|
|
1040
|
+
const commitTypeOption = commitTypeOptions.find((commitTypeOption) => commitTypeOption.value === option);
|
|
585
1041
|
return {
|
|
586
1042
|
value: commitTypeOption?.value ?? option,
|
|
587
1043
|
label: commitTypeOption?.label ?? option,
|
|
@@ -593,7 +1049,7 @@ const mergeCommitTypeEnumOptions = (options) => {
|
|
|
593
1049
|
};
|
|
594
1050
|
const mergeCommitScopeEnumOptions = (options) => {
|
|
595
1051
|
return (options.includes("none") ? options : options.concat("none")).map((option) => {
|
|
596
|
-
const commitScopeOption = commitScopeOptions.find((commitScopeOption
|
|
1052
|
+
const commitScopeOption = commitScopeOptions.find((commitScopeOption) => commitScopeOption.label === option);
|
|
597
1053
|
return {
|
|
598
1054
|
value: commitScopeOption?.value ?? option,
|
|
599
1055
|
label: commitScopeOption?.label ?? option
|
|
@@ -603,21 +1059,22 @@ const mergeCommitScopeEnumOptions = (options) => {
|
|
|
603
1059
|
|
|
604
1060
|
//#endregion
|
|
605
1061
|
//#region src/commands/push.ts
|
|
606
|
-
const handlePush = async (branch) => {
|
|
607
|
-
const spinner = createSpinner(`Pushing branch ${branch} to remote...`);
|
|
1062
|
+
const handlePush = async (branch, remote = "origin") => {
|
|
1063
|
+
const spinner = createSpinner(`Pushing branch ${branch} to ${remote}...`);
|
|
608
1064
|
try {
|
|
609
|
-
const
|
|
1065
|
+
const [error, result] = await xASync("git", [
|
|
610
1066
|
"push",
|
|
611
|
-
|
|
1067
|
+
remote,
|
|
612
1068
|
branch
|
|
613
1069
|
]);
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
1070
|
+
if (error) {
|
|
1071
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1072
|
+
throw new Error(`Failed with error: ${errorMessage}`);
|
|
1073
|
+
}
|
|
1074
|
+
spinner.stop(colors.green(`Successfully pushed branch ${colors.bgGreen(branch)} to ${remote}.`));
|
|
618
1075
|
} catch (error) {
|
|
619
1076
|
spinner.stop();
|
|
620
|
-
BigText({ text: `Failed to push branch ${branch}.` });
|
|
1077
|
+
BigText({ text: `Failed to push branch ${branch} to ${remote}.` });
|
|
621
1078
|
log.error(error);
|
|
622
1079
|
}
|
|
623
1080
|
};
|
|
@@ -632,16 +1089,50 @@ const pushInteractive = async () => {
|
|
|
632
1089
|
log.error("No branch selected. Aborting push operation.");
|
|
633
1090
|
return;
|
|
634
1091
|
}
|
|
635
|
-
|
|
636
|
-
|
|
1092
|
+
let repositories;
|
|
1093
|
+
try {
|
|
1094
|
+
repositories = (await getRemoteRepositoryOptions()).remotes;
|
|
1095
|
+
} catch (error) {
|
|
1096
|
+
log.error(`Failed to get remote repositories: ${error instanceof Error ? error.message : String(error)}`);
|
|
1097
|
+
log.show("Hint: Make sure you're in a git repository and have configured remotes.", { type: "info" });
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
if (repositories.length === 0) {
|
|
1101
|
+
log.error("No remote repositories found. Aborting push operation.");
|
|
1102
|
+
log.show("Hint: Run `git remote add <name> <url>` to add a remote repository.", { type: "info" });
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
let selectedRepository = repositories[0];
|
|
1106
|
+
if (repositories.length > 1) selectedRepository = await createSelect({
|
|
1107
|
+
message: "Select remote repository",
|
|
1108
|
+
options: repositories.map((repo) => ({
|
|
1109
|
+
label: repo,
|
|
1110
|
+
value: repo
|
|
1111
|
+
})),
|
|
1112
|
+
initialValue: repositories[0]
|
|
1113
|
+
});
|
|
1114
|
+
if (await createConfirm({ message: `Do you want to push ${colors.bgGreen(currentBranch)} to ${selectedRepository}?` })) {
|
|
1115
|
+
await handlePush(currentBranch, selectedRepository);
|
|
637
1116
|
return;
|
|
638
1117
|
}
|
|
639
|
-
const { options } = await
|
|
640
|
-
|
|
1118
|
+
const { options } = await getRemoteBranchOptions();
|
|
1119
|
+
const selectedBranch = await createSelect({
|
|
641
1120
|
message: "Select the branch to push",
|
|
642
1121
|
options,
|
|
643
1122
|
initialValue: "main"
|
|
644
|
-
})
|
|
1123
|
+
});
|
|
1124
|
+
selectedRepository = repositories[0];
|
|
1125
|
+
if (repositories.length > 1) {
|
|
1126
|
+
if (!await createConfirm({ message: `Push ${colors.bgGreen(selectedBranch)} to ${selectedRepository}?` })) selectedRepository = await createSelect({
|
|
1127
|
+
message: "Select remote repository",
|
|
1128
|
+
options: repositories.map((repo) => ({
|
|
1129
|
+
label: repo,
|
|
1130
|
+
value: repo
|
|
1131
|
+
})),
|
|
1132
|
+
initialValue: selectedRepository
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
await handlePush(selectedBranch, selectedRepository);
|
|
645
1136
|
};
|
|
646
1137
|
|
|
647
1138
|
//#endregion
|
|
@@ -765,6 +1256,459 @@ const getTicket = async () => {
|
|
|
765
1256
|
return branch;
|
|
766
1257
|
};
|
|
767
1258
|
|
|
1259
|
+
//#endregion
|
|
1260
|
+
//#region src/commands/config.ts
|
|
1261
|
+
/** biome-ignore-all lint/suspicious/noExplicitAny: need any */
|
|
1262
|
+
const GITCONFIG_PATH = join(homedir(), ".gitconfig");
|
|
1263
|
+
const readGitConfig = () => {
|
|
1264
|
+
try {
|
|
1265
|
+
const content = readFileSync(GITCONFIG_PATH, "utf-8");
|
|
1266
|
+
const config = {};
|
|
1267
|
+
let currentSection = null;
|
|
1268
|
+
let subsection = null;
|
|
1269
|
+
for (const line of content.split("\n")) {
|
|
1270
|
+
const trimmed = line.trim();
|
|
1271
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1272
|
+
const sectionMatch = trimmed.match(/^\[([^\]]+)\]$/);
|
|
1273
|
+
if (sectionMatch) {
|
|
1274
|
+
currentSection = sectionMatch[1] || null;
|
|
1275
|
+
subsection = null;
|
|
1276
|
+
if (currentSection && !config[currentSection]) config[currentSection] = {};
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
const subsectionMatch = trimmed.match(/^\[([^\s]+) "([^"]+)"\]$/);
|
|
1280
|
+
if (subsectionMatch) {
|
|
1281
|
+
currentSection = subsectionMatch[1] || null;
|
|
1282
|
+
subsection = subsectionMatch[2] || null;
|
|
1283
|
+
if (currentSection && !config[currentSection]) config[currentSection] = {};
|
|
1284
|
+
if (subsection && currentSection && typeof config[currentSection] === "object") {
|
|
1285
|
+
if (!config[currentSection][subsection]) config[currentSection][subsection] = {};
|
|
1286
|
+
}
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1289
|
+
const keyValueMatch = trimmed.match(/^(\w+)\s*=\s*(.+)$/);
|
|
1290
|
+
if (keyValueMatch && currentSection) {
|
|
1291
|
+
const key = keyValueMatch[1];
|
|
1292
|
+
const value = keyValueMatch[2];
|
|
1293
|
+
let target = config[currentSection];
|
|
1294
|
+
if (subsection && typeof target === "object") {
|
|
1295
|
+
if (!target[subsection]) target[subsection] = {};
|
|
1296
|
+
target = target[subsection];
|
|
1297
|
+
}
|
|
1298
|
+
if (key && value === "true") target[key] = true;
|
|
1299
|
+
else if (key && value === "false") target[key] = false;
|
|
1300
|
+
else if (key) target[key] = value;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
return config;
|
|
1304
|
+
} catch {
|
|
1305
|
+
return {};
|
|
1306
|
+
}
|
|
1307
|
+
};
|
|
1308
|
+
const writeGitConfig = (config) => {
|
|
1309
|
+
const lines = [];
|
|
1310
|
+
const sections = Object.keys(config).sort();
|
|
1311
|
+
for (const section of sections) {
|
|
1312
|
+
const value = config[section];
|
|
1313
|
+
if (!value || typeof value !== "object") continue;
|
|
1314
|
+
if (Object.keys(value).length === 0) continue;
|
|
1315
|
+
let isSubsection = false;
|
|
1316
|
+
for (const key of Object.keys(value)) if (typeof value[key] === "object" && key !== "alias") {
|
|
1317
|
+
isSubsection = true;
|
|
1318
|
+
break;
|
|
1319
|
+
}
|
|
1320
|
+
if (isSubsection) for (const [subsection, subValue] of Object.entries(value)) {
|
|
1321
|
+
if (typeof subValue !== "object" || subValue === null) continue;
|
|
1322
|
+
lines.push(`[${section} "${subsection}"]`);
|
|
1323
|
+
for (const [key, val] of Object.entries(subValue)) lines.push(`\t${key} = ${val}`);
|
|
1324
|
+
lines.push("");
|
|
1325
|
+
}
|
|
1326
|
+
else {
|
|
1327
|
+
lines.push(`[${section}]`);
|
|
1328
|
+
for (const [key, val] of Object.entries(value)) if (typeof val === "object" && val !== null) for (const [subKey, subVal] of Object.entries(val)) lines.push(`\t${subKey} = ${subVal}`);
|
|
1329
|
+
else lines.push(`\t${key} = ${val}`);
|
|
1330
|
+
lines.push("");
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
writeFileSync(GITCONFIG_PATH, lines.join("\n"), "utf-8");
|
|
1334
|
+
};
|
|
1335
|
+
const displayConfig = (config) => {
|
|
1336
|
+
const lines = [];
|
|
1337
|
+
if (config.user) {
|
|
1338
|
+
lines.push("👤 User Information:");
|
|
1339
|
+
if (config.user.name) lines.push(` Name: ${config.user.name}`);
|
|
1340
|
+
if (config.user.email) lines.push(` Email: ${config.user.email}`);
|
|
1341
|
+
lines.push("");
|
|
1342
|
+
}
|
|
1343
|
+
if (config.init?.defaultBranch) {
|
|
1344
|
+
lines.push("🌳 Initial Branch:");
|
|
1345
|
+
lines.push(` Default: ${config.init.defaultBranch}`);
|
|
1346
|
+
lines.push("");
|
|
1347
|
+
}
|
|
1348
|
+
if (config.pull) {
|
|
1349
|
+
lines.push("📥 Pull Strategy:");
|
|
1350
|
+
if (config.pull.rebase !== void 0) lines.push(` Rebase: ${config.pull.rebase}`);
|
|
1351
|
+
lines.push("");
|
|
1352
|
+
}
|
|
1353
|
+
if (config.push) {
|
|
1354
|
+
lines.push("📤 Push Strategy:");
|
|
1355
|
+
if (config.push.default) lines.push(` Default: ${config.push.default}`);
|
|
1356
|
+
if (config.push.autoSetupRemote !== void 0) lines.push(` Auto Setup Remote: ${config.push.autoSetupRemote}`);
|
|
1357
|
+
lines.push("");
|
|
1358
|
+
}
|
|
1359
|
+
if (config.color) {
|
|
1360
|
+
lines.push("🎨 Color:");
|
|
1361
|
+
if (config.color.ui !== void 0) lines.push(` UI: ${config.color.ui}`);
|
|
1362
|
+
lines.push("");
|
|
1363
|
+
}
|
|
1364
|
+
if (config.alias && Object.keys(config.alias).length > 0) {
|
|
1365
|
+
lines.push("⚡ Aliases:");
|
|
1366
|
+
for (const [alias, command] of Object.entries(config.alias)) lines.push(` ${alias} = ${String(command)}`);
|
|
1367
|
+
lines.push("");
|
|
1368
|
+
}
|
|
1369
|
+
return lines.join("\n");
|
|
1370
|
+
};
|
|
1371
|
+
const configureUserInfo = async (config) => {
|
|
1372
|
+
const action = await createSelect({
|
|
1373
|
+
message: "What would you like to configure?",
|
|
1374
|
+
options: [
|
|
1375
|
+
{
|
|
1376
|
+
label: "Name",
|
|
1377
|
+
value: "name"
|
|
1378
|
+
},
|
|
1379
|
+
{
|
|
1380
|
+
label: "Email",
|
|
1381
|
+
value: "email"
|
|
1382
|
+
},
|
|
1383
|
+
{
|
|
1384
|
+
label: "Both",
|
|
1385
|
+
value: "both"
|
|
1386
|
+
}
|
|
1387
|
+
]
|
|
1388
|
+
});
|
|
1389
|
+
if (action === "name" || action === "both") {
|
|
1390
|
+
const name = await createInput({
|
|
1391
|
+
message: "Enter your name:",
|
|
1392
|
+
initialValue: config.user?.name || "",
|
|
1393
|
+
placeholder: config.user?.name || "Your name"
|
|
1394
|
+
});
|
|
1395
|
+
if (!config.user) config.user = {};
|
|
1396
|
+
config.user.name = name || config.user.name;
|
|
1397
|
+
}
|
|
1398
|
+
if (action === "email" || action === "both") {
|
|
1399
|
+
const email = await createInput({
|
|
1400
|
+
message: "Enter your email:",
|
|
1401
|
+
initialValue: config.user?.email || "",
|
|
1402
|
+
placeholder: config.user?.email || "your@email.com"
|
|
1403
|
+
});
|
|
1404
|
+
if (!config.user) config.user = {};
|
|
1405
|
+
config.user.email = email || config.user.email;
|
|
1406
|
+
}
|
|
1407
|
+
return config;
|
|
1408
|
+
};
|
|
1409
|
+
const configureCommonOptions = async (config) => {
|
|
1410
|
+
const options = await createCheckbox({
|
|
1411
|
+
message: "Select options to configure:",
|
|
1412
|
+
options: [
|
|
1413
|
+
{
|
|
1414
|
+
label: "Default branch name",
|
|
1415
|
+
value: "defaultBranch"
|
|
1416
|
+
},
|
|
1417
|
+
{
|
|
1418
|
+
label: "Pull rebase strategy",
|
|
1419
|
+
value: "pullRebase"
|
|
1420
|
+
},
|
|
1421
|
+
{
|
|
1422
|
+
label: "Push default strategy",
|
|
1423
|
+
value: "pushDefault"
|
|
1424
|
+
},
|
|
1425
|
+
{
|
|
1426
|
+
label: "Enable color output",
|
|
1427
|
+
value: "colorUi"
|
|
1428
|
+
},
|
|
1429
|
+
{
|
|
1430
|
+
label: "Auto stash on rebase",
|
|
1431
|
+
value: "autoStash"
|
|
1432
|
+
}
|
|
1433
|
+
],
|
|
1434
|
+
required: false
|
|
1435
|
+
});
|
|
1436
|
+
for (const option of options) switch (option) {
|
|
1437
|
+
case "defaultBranch": {
|
|
1438
|
+
const branch = await createInput({
|
|
1439
|
+
message: "Enter default branch name:",
|
|
1440
|
+
initialValue: config.init?.defaultBranch || "main",
|
|
1441
|
+
placeholder: "main"
|
|
1442
|
+
});
|
|
1443
|
+
if (!config.init) config.init = {};
|
|
1444
|
+
config.init.defaultBranch = branch || config.init.defaultBranch || "main";
|
|
1445
|
+
break;
|
|
1446
|
+
}
|
|
1447
|
+
case "pullRebase": {
|
|
1448
|
+
const rebase = await createSelect({
|
|
1449
|
+
message: "Select pull rebase strategy:",
|
|
1450
|
+
options: [
|
|
1451
|
+
{
|
|
1452
|
+
label: "False (merge)",
|
|
1453
|
+
value: "false"
|
|
1454
|
+
},
|
|
1455
|
+
{
|
|
1456
|
+
label: "True (rebase)",
|
|
1457
|
+
value: "true"
|
|
1458
|
+
},
|
|
1459
|
+
{
|
|
1460
|
+
label: "Interactive",
|
|
1461
|
+
value: "interactive"
|
|
1462
|
+
}
|
|
1463
|
+
]
|
|
1464
|
+
});
|
|
1465
|
+
if (!config.pull) config.pull = {};
|
|
1466
|
+
config.pull.rebase = rebase === "false" ? false : rebase;
|
|
1467
|
+
break;
|
|
1468
|
+
}
|
|
1469
|
+
case "pushDefault": {
|
|
1470
|
+
const pushDefault = await createSelect({
|
|
1471
|
+
message: "Select push default strategy:",
|
|
1472
|
+
options: [
|
|
1473
|
+
{
|
|
1474
|
+
label: "current",
|
|
1475
|
+
value: "current"
|
|
1476
|
+
},
|
|
1477
|
+
{
|
|
1478
|
+
label: "upstream",
|
|
1479
|
+
value: "upstream"
|
|
1480
|
+
},
|
|
1481
|
+
{
|
|
1482
|
+
label: "simple",
|
|
1483
|
+
value: "simple"
|
|
1484
|
+
},
|
|
1485
|
+
{
|
|
1486
|
+
label: "matching",
|
|
1487
|
+
value: "matching"
|
|
1488
|
+
}
|
|
1489
|
+
]
|
|
1490
|
+
});
|
|
1491
|
+
if (!config.push) config.push = {};
|
|
1492
|
+
config.push.default = pushDefault;
|
|
1493
|
+
break;
|
|
1494
|
+
}
|
|
1495
|
+
case "colorUi": {
|
|
1496
|
+
const color = await createSelect({
|
|
1497
|
+
message: "Select color output:",
|
|
1498
|
+
options: [
|
|
1499
|
+
{
|
|
1500
|
+
label: "Auto",
|
|
1501
|
+
value: "auto"
|
|
1502
|
+
},
|
|
1503
|
+
{
|
|
1504
|
+
label: "Always",
|
|
1505
|
+
value: "always"
|
|
1506
|
+
},
|
|
1507
|
+
{
|
|
1508
|
+
label: "Never",
|
|
1509
|
+
value: "false"
|
|
1510
|
+
}
|
|
1511
|
+
]
|
|
1512
|
+
});
|
|
1513
|
+
if (!config.color) config.color = {};
|
|
1514
|
+
config.color.ui = color === "false" ? false : color;
|
|
1515
|
+
break;
|
|
1516
|
+
}
|
|
1517
|
+
case "autoStash": {
|
|
1518
|
+
const autoStash = await createConfirm({
|
|
1519
|
+
message: "Enable auto stash on rebase?",
|
|
1520
|
+
initialValue: config.rebase?.autoStash === true
|
|
1521
|
+
});
|
|
1522
|
+
if (!config.rebase) config.rebase = {};
|
|
1523
|
+
config.rebase.autoStash = autoStash;
|
|
1524
|
+
break;
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
return config;
|
|
1528
|
+
};
|
|
1529
|
+
const configureAliases = async (config) => {
|
|
1530
|
+
const action = await createSelect({
|
|
1531
|
+
message: "What would you like to do?",
|
|
1532
|
+
options: [
|
|
1533
|
+
{
|
|
1534
|
+
label: "Add new alias",
|
|
1535
|
+
value: "add"
|
|
1536
|
+
},
|
|
1537
|
+
{
|
|
1538
|
+
label: "Remove alias",
|
|
1539
|
+
value: "remove"
|
|
1540
|
+
},
|
|
1541
|
+
{
|
|
1542
|
+
label: "List all aliases",
|
|
1543
|
+
value: "list"
|
|
1544
|
+
}
|
|
1545
|
+
]
|
|
1546
|
+
});
|
|
1547
|
+
if (action === "add") {
|
|
1548
|
+
const alias = await createInput({
|
|
1549
|
+
message: "Enter alias name:",
|
|
1550
|
+
initialValue: "",
|
|
1551
|
+
placeholder: "st"
|
|
1552
|
+
});
|
|
1553
|
+
const command = await createInput({
|
|
1554
|
+
message: "Enter git command:",
|
|
1555
|
+
initialValue: "",
|
|
1556
|
+
placeholder: "status"
|
|
1557
|
+
});
|
|
1558
|
+
if (!alias || !command) {
|
|
1559
|
+
Message({ text: "Alias name and command cannot be empty." });
|
|
1560
|
+
return config;
|
|
1561
|
+
}
|
|
1562
|
+
if (!config.alias) config.alias = {};
|
|
1563
|
+
config.alias[alias] = command;
|
|
1564
|
+
Message({ text: `Added alias: ${alias} = ${command}` });
|
|
1565
|
+
} else if (action === "remove") {
|
|
1566
|
+
if (!config.alias || Object.keys(config.alias).length === 0) {
|
|
1567
|
+
Message({ text: "No aliases to remove." });
|
|
1568
|
+
return config;
|
|
1569
|
+
}
|
|
1570
|
+
const toRemove = await createSelect({
|
|
1571
|
+
message: "Select alias to remove:",
|
|
1572
|
+
options: Object.keys(config.alias).map((key) => ({
|
|
1573
|
+
label: `${key} = ${config.alias[key]}`,
|
|
1574
|
+
value: key
|
|
1575
|
+
}))
|
|
1576
|
+
});
|
|
1577
|
+
delete config.alias[toRemove];
|
|
1578
|
+
Message({ text: `Removed alias: ${toRemove}` });
|
|
1579
|
+
} else if (action === "list") if (!config.alias || Object.keys(config.alias).length === 0) Message({ text: "No aliases configured." });
|
|
1580
|
+
else {
|
|
1581
|
+
const aliasList = Object.entries(config.alias).map(([alias, command]) => `${alias} = ${command}`).join("\n");
|
|
1582
|
+
log.show(aliasList, { type: "info" });
|
|
1583
|
+
}
|
|
1584
|
+
return config;
|
|
1585
|
+
};
|
|
1586
|
+
const initializeConfig = async (config) => {
|
|
1587
|
+
Message({ text: "🚀 Let's set up your git configuration!" });
|
|
1588
|
+
const name = await createInput({
|
|
1589
|
+
message: "What is your name?",
|
|
1590
|
+
initialValue: config.user?.name || "",
|
|
1591
|
+
placeholder: "GaoZimeng"
|
|
1592
|
+
});
|
|
1593
|
+
if (name) {
|
|
1594
|
+
if (!config.user) config.user = {};
|
|
1595
|
+
config.user.name = name;
|
|
1596
|
+
}
|
|
1597
|
+
const email = await createInput({
|
|
1598
|
+
message: "What is your email?",
|
|
1599
|
+
initialValue: config.user?.email || "",
|
|
1600
|
+
placeholder: "your@email.com"
|
|
1601
|
+
});
|
|
1602
|
+
if (email) {
|
|
1603
|
+
if (!config.user) config.user = {};
|
|
1604
|
+
config.user.email = email;
|
|
1605
|
+
}
|
|
1606
|
+
const defaultBranch = await createInput({
|
|
1607
|
+
message: "What should be the default branch name?",
|
|
1608
|
+
initialValue: config.init?.defaultBranch || "main",
|
|
1609
|
+
placeholder: "main"
|
|
1610
|
+
});
|
|
1611
|
+
if (defaultBranch) {
|
|
1612
|
+
if (!config.init) config.init = {};
|
|
1613
|
+
config.init.defaultBranch = defaultBranch;
|
|
1614
|
+
}
|
|
1615
|
+
const enableColor = await createConfirm({
|
|
1616
|
+
message: "Enable colored output?",
|
|
1617
|
+
initialValue: config.color?.ui !== false
|
|
1618
|
+
});
|
|
1619
|
+
if (!config.color) config.color = {};
|
|
1620
|
+
config.color.ui = enableColor ? "auto" : false;
|
|
1621
|
+
const autoSetupRemote = await createConfirm({
|
|
1622
|
+
message: "Automatically setup remote branch when pushing?",
|
|
1623
|
+
initialValue: config.push?.autoSetupRemote !== false
|
|
1624
|
+
});
|
|
1625
|
+
if (!config.push) config.push = {};
|
|
1626
|
+
config.push.autoSetupRemote = autoSetupRemote;
|
|
1627
|
+
const autoStash = await createConfirm({
|
|
1628
|
+
message: "Auto stash before rebase?",
|
|
1629
|
+
initialValue: config.rebase?.autoStash !== false
|
|
1630
|
+
});
|
|
1631
|
+
if (!config.rebase) config.rebase = {};
|
|
1632
|
+
config.rebase.autoStash = autoStash;
|
|
1633
|
+
return config;
|
|
1634
|
+
};
|
|
1635
|
+
const configCommand = (command) => {
|
|
1636
|
+
const subCommand = command.command("config").description("Interactive git configuration manager");
|
|
1637
|
+
subCommand.command("init").description("Quick setup basic git configuration").action(async () => {
|
|
1638
|
+
const updatedConfig = await initializeConfig(readGitConfig());
|
|
1639
|
+
const spinner = createSpinner("Saving configuration...");
|
|
1640
|
+
writeGitConfig(updatedConfig);
|
|
1641
|
+
spinner.stop("✓ Git configuration initialized successfully!");
|
|
1642
|
+
const display = displayConfig(updatedConfig);
|
|
1643
|
+
if (display) log.show(`\n${display}`);
|
|
1644
|
+
});
|
|
1645
|
+
subCommand.action(async () => {
|
|
1646
|
+
let continueRunning = true;
|
|
1647
|
+
while (continueRunning) {
|
|
1648
|
+
const action = await createSelect({
|
|
1649
|
+
message: "What would you like to do?",
|
|
1650
|
+
options: [
|
|
1651
|
+
{
|
|
1652
|
+
label: "📖 View current config",
|
|
1653
|
+
value: "view"
|
|
1654
|
+
},
|
|
1655
|
+
{
|
|
1656
|
+
label: "👤 Configure user info",
|
|
1657
|
+
value: "user"
|
|
1658
|
+
},
|
|
1659
|
+
{
|
|
1660
|
+
label: "⚙️ Configure options",
|
|
1661
|
+
value: "options"
|
|
1662
|
+
},
|
|
1663
|
+
{
|
|
1664
|
+
label: "⚡ Manage aliases",
|
|
1665
|
+
value: "aliases"
|
|
1666
|
+
},
|
|
1667
|
+
{
|
|
1668
|
+
label: "🧹 Clear all config",
|
|
1669
|
+
value: "clear"
|
|
1670
|
+
},
|
|
1671
|
+
{
|
|
1672
|
+
label: "❌ Exit",
|
|
1673
|
+
value: "exit"
|
|
1674
|
+
}
|
|
1675
|
+
]
|
|
1676
|
+
});
|
|
1677
|
+
if (action === "exit") {
|
|
1678
|
+
continueRunning = false;
|
|
1679
|
+
continue;
|
|
1680
|
+
}
|
|
1681
|
+
if (action === "view") {
|
|
1682
|
+
const display = displayConfig(readGitConfig());
|
|
1683
|
+
if (display) log.show(`\n${display}`);
|
|
1684
|
+
else Message({ text: "No configuration found." });
|
|
1685
|
+
} else if (action === "user") {
|
|
1686
|
+
const updatedConfig = await configureUserInfo(readGitConfig());
|
|
1687
|
+
const spinner = createSpinner("Saving configuration...");
|
|
1688
|
+
writeGitConfig(updatedConfig);
|
|
1689
|
+
spinner.stop("✓ Configuration saved successfully!");
|
|
1690
|
+
} else if (action === "options") {
|
|
1691
|
+
const updatedConfig = await configureCommonOptions(readGitConfig());
|
|
1692
|
+
const spinner = createSpinner("Saving configuration...");
|
|
1693
|
+
writeGitConfig(updatedConfig);
|
|
1694
|
+
spinner.stop("✓ Configuration saved successfully!");
|
|
1695
|
+
} else if (action === "aliases") {
|
|
1696
|
+
const updatedConfig = await configureAliases(readGitConfig());
|
|
1697
|
+
const spinner = createSpinner("Saving configuration...");
|
|
1698
|
+
writeGitConfig(updatedConfig);
|
|
1699
|
+
spinner.stop("✓ Configuration saved successfully!");
|
|
1700
|
+
} else if (action === "clear") {
|
|
1701
|
+
if (await createConfirm({ message: "Are you sure you want to clear all git configuration?" })) {
|
|
1702
|
+
const spinner = createSpinner("Clearing configuration...");
|
|
1703
|
+
writeGitConfig({});
|
|
1704
|
+
spinner.stop("✓ Configuration cleared successfully!");
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
});
|
|
1709
|
+
return command;
|
|
1710
|
+
};
|
|
1711
|
+
|
|
768
1712
|
//#endregion
|
|
769
1713
|
//#region src/commands/diff.ts
|
|
770
1714
|
const handleDiff = async (branch, { isLocal }) => {
|
|
@@ -807,6 +1751,15 @@ function diffCommand(command) {
|
|
|
807
1751
|
});
|
|
808
1752
|
}
|
|
809
1753
|
|
|
1754
|
+
//#endregion
|
|
1755
|
+
//#region src/commands/hist.ts
|
|
1756
|
+
const histCommand = (command) => {
|
|
1757
|
+
command.command("hist").alias("history").description("Show git history with beautiful graph format").option("-n, --number <count>", "Limit number of commits to show").action(async (options) => {
|
|
1758
|
+
await renderHistViewer(options.number ? Number.parseInt(options.number, 10) : void 0);
|
|
1759
|
+
});
|
|
1760
|
+
return command;
|
|
1761
|
+
};
|
|
1762
|
+
|
|
810
1763
|
//#endregion
|
|
811
1764
|
//#region src/commands/list.ts
|
|
812
1765
|
function listCommand(command) {
|
|
@@ -854,18 +1807,43 @@ function listCommand(command) {
|
|
|
854
1807
|
//#region src/commands/merge.ts
|
|
855
1808
|
const handleMerge = async (branch) => {
|
|
856
1809
|
const spinner = createSpinner(`Merging branch ${branch}...`);
|
|
857
|
-
const args = [
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
1810
|
+
const args = [
|
|
1811
|
+
"merge",
|
|
1812
|
+
"--no-edit",
|
|
1813
|
+
branch
|
|
1814
|
+
];
|
|
1815
|
+
const stashResult = await handleGitStash(void 0, {
|
|
1816
|
+
branch,
|
|
1817
|
+
operation: "merge"
|
|
1818
|
+
});
|
|
1819
|
+
if (!stashResult) {
|
|
1820
|
+
spinner.stop("Cannot proceed with merge: failed to stash changes.");
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
try {
|
|
1824
|
+
const [error, result] = await xASync("git", args, { nodeOptions: { stdio: "inherit" } });
|
|
1825
|
+
if (error) {
|
|
1826
|
+
spinner.stop();
|
|
1827
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1828
|
+
throw new Error(`Failed to merge branch ${branch}: ${errorMessage}`);
|
|
1829
|
+
}
|
|
1830
|
+
spinner.stop();
|
|
1831
|
+
if (result.stdout.includes("Merge branch") || result.stdout.includes("Merge made by")) await handleMergeCommit();
|
|
1832
|
+
log.show(colors.green(`Successfully merged branch ${colors.bgGreen(branch)}.`), { type: "success" });
|
|
1833
|
+
} catch (error) {
|
|
1834
|
+
spinner.stop();
|
|
1835
|
+
log.show(`Failed to merge branch ${branch}.`, { type: "error" });
|
|
1836
|
+
log.error(error);
|
|
1837
|
+
throw error;
|
|
1838
|
+
} finally {
|
|
1839
|
+
if (stashResult) await handleGitPop(stashResult);
|
|
1840
|
+
}
|
|
863
1841
|
};
|
|
864
1842
|
function mergeCommand(command) {
|
|
865
1843
|
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
1844
|
let isLocal = params.local;
|
|
867
1845
|
if (branch) {
|
|
868
|
-
handleMerge(branch);
|
|
1846
|
+
await handleMerge(branch);
|
|
869
1847
|
return;
|
|
870
1848
|
}
|
|
871
1849
|
if (isEmpty(params)) isLocal = await createSelect({
|
|
@@ -880,18 +1858,20 @@ function mergeCommand(command) {
|
|
|
880
1858
|
initialValue: false
|
|
881
1859
|
});
|
|
882
1860
|
if (isLocal) {
|
|
883
|
-
const { options } = await
|
|
884
|
-
handleMerge(await
|
|
1861
|
+
const { options, currentBranch } = await getLocalBranchOptions();
|
|
1862
|
+
await handleMerge(await createSelect({
|
|
885
1863
|
message: "Select the branch to merge",
|
|
886
|
-
options
|
|
1864
|
+
options,
|
|
1865
|
+
initialValue: currentBranch
|
|
887
1866
|
}));
|
|
888
1867
|
} else {
|
|
889
|
-
const { options } = await
|
|
890
|
-
const selectedBranch = await
|
|
1868
|
+
const { options, currentBranch } = await getRemoteBranchOptions();
|
|
1869
|
+
const selectedBranch = await createSelect({
|
|
891
1870
|
message: "Select the branch to merge",
|
|
892
|
-
options
|
|
1871
|
+
options,
|
|
1872
|
+
initialValue: currentBranch
|
|
893
1873
|
});
|
|
894
|
-
if (await createConfirm({ message: `Do you want to merge ${colors.bgRed(selectedBranch)}?` })) handleMerge(selectedBranch);
|
|
1874
|
+
if (await createConfirm({ message: `Do you want to merge ${colors.bgRed(selectedBranch)}?` })) await handleMerge(selectedBranch);
|
|
895
1875
|
}
|
|
896
1876
|
});
|
|
897
1877
|
}
|
|
@@ -900,7 +1880,40 @@ function mergeCommand(command) {
|
|
|
900
1880
|
//#region src/commands/pull.ts
|
|
901
1881
|
function pullCommand(command) {
|
|
902
1882
|
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
|
|
1883
|
+
const [error, result] = await safeAwait(getRemoteRepositoryOptions());
|
|
1884
|
+
if (error) {
|
|
1885
|
+
log.error(`Failed to get remote repositories: ${error.message}`);
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
const repositories = result.remotes;
|
|
1889
|
+
if (repositories.length === 0) {
|
|
1890
|
+
log.error("No remote repositories found. Aborting pull operation.");
|
|
1891
|
+
log.show("Hint: Use \"git remote add <name> <url>\" to add a remote repository.", { type: "info" });
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1894
|
+
const selectedRepository = repositories.length > 1 ? (await safeAwait(createSelect({
|
|
1895
|
+
message: "Select remote repository",
|
|
1896
|
+
options: result.options,
|
|
1897
|
+
initialValue: repositories[0]
|
|
1898
|
+
})))[1] : repositories[0];
|
|
1899
|
+
if (!selectedRepository) {
|
|
1900
|
+
log.error("No remote selected. Aborting pull operation.");
|
|
1901
|
+
return;
|
|
1902
|
+
}
|
|
1903
|
+
const spinner = createSpinner("Fetching latest remote branches...");
|
|
1904
|
+
const [fetchError] = await xASync("git", [
|
|
1905
|
+
"fetch",
|
|
1906
|
+
selectedRepository,
|
|
1907
|
+
"--prune"
|
|
1908
|
+
], { timeout: 3e4 });
|
|
1909
|
+
if (fetchError) {
|
|
1910
|
+
spinner.stop("Failed to fetch latest remote branches");
|
|
1911
|
+
log.error(`Failed to fetch from ${selectedRepository}: ${fetchError.message}`);
|
|
1912
|
+
log.show("Please check your network connection and try again.", { type: "info" });
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
spinner.stop("Successfully fetched latest remote branches");
|
|
1916
|
+
const { options: branchOptions, currentBranch } = await getRemoteBranchOptions();
|
|
904
1917
|
if (!branchOptions.length) {
|
|
905
1918
|
log.error("No branches found. Please check your git repository.");
|
|
906
1919
|
return;
|
|
@@ -920,24 +1933,30 @@ function pullCommand(command) {
|
|
|
920
1933
|
options: [{
|
|
921
1934
|
label: "Merge (default)",
|
|
922
1935
|
value: "merge",
|
|
923
|
-
hint:
|
|
1936
|
+
hint: `git pull ${selectedRepository} ${selectedBranch}`
|
|
924
1937
|
}, {
|
|
925
1938
|
label: "Rebase",
|
|
926
1939
|
value: "rebase",
|
|
927
|
-
hint:
|
|
1940
|
+
hint: `git pull --rebase ${selectedRepository} ${selectedBranch}`
|
|
928
1941
|
}],
|
|
929
1942
|
initialValue: "merge"
|
|
930
1943
|
}) === "rebase";
|
|
931
|
-
const
|
|
932
|
-
|
|
933
|
-
|
|
1944
|
+
const stashResult = await handleGitStash(void 0, {
|
|
1945
|
+
branch: selectedBranch,
|
|
1946
|
+
operation: "pull"
|
|
1947
|
+
});
|
|
1948
|
+
await handleGitPull(selectedBranch, {
|
|
1949
|
+
remote: selectedRepository,
|
|
1950
|
+
rebase: useRebase
|
|
1951
|
+
});
|
|
1952
|
+
stashResult && handleGitPop(stashResult);
|
|
934
1953
|
});
|
|
935
1954
|
}
|
|
936
1955
|
|
|
937
1956
|
//#endregion
|
|
938
1957
|
//#region src/constants/stash.ts
|
|
939
1958
|
const HELP_MESSAGE = {
|
|
940
|
-
main: createHelpExample("ng stash", "ng stash save \"work in progress\"", "ng stash ls", "ng stash pop", "ng stash drop"),
|
|
1959
|
+
main: createHelpExample("ng stash", "ng blame [file]", "ng blame [file] [line]", "ng stash save \"work in progress\"", "ng stash ls", "ng stash pop", "ng stash drop"),
|
|
941
1960
|
save: createHelpExample("ng stash save \"work in progress\""),
|
|
942
1961
|
list: createHelpExample("ng stash ls"),
|
|
943
1962
|
pop: createHelpExample("ng stash pop"),
|
|
@@ -1002,17 +2021,31 @@ const handlePop = handleCheck(async (stashes) => {
|
|
|
1002
2021
|
else log.show("Successfully popped changes.", { type: "success" });
|
|
1003
2022
|
}
|
|
1004
2023
|
});
|
|
2024
|
+
const parseStashEntry = (stashEntry) => {
|
|
2025
|
+
const match = stashEntry.match(/^(stash@\{\d+\}):\s+On\s+(\S+):\s+(.+)$/);
|
|
2026
|
+
if (match) return {
|
|
2027
|
+
ref: match[1] || stashEntry,
|
|
2028
|
+
branch: match[2] || "unknown",
|
|
2029
|
+
message: match[3] || stashEntry
|
|
2030
|
+
};
|
|
2031
|
+
return {
|
|
2032
|
+
ref: stashEntry.match(/^(stash@\{\d+\})/)?.[1] ?? stashEntry.split(":")[0] ?? stashEntry,
|
|
2033
|
+
branch: "unknown",
|
|
2034
|
+
message: stashEntry
|
|
2035
|
+
};
|
|
2036
|
+
};
|
|
1005
2037
|
const handleList = handleCheck(async (stashes) => {
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
const files = await getStashFiles(
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
2038
|
+
await renderStashList(await Promise.all(stashes.map(async (stash) => {
|
|
2039
|
+
const stashInfo = parseStashEntry(stash);
|
|
2040
|
+
const files = await getStashFiles(stashInfo.ref);
|
|
2041
|
+
return {
|
|
2042
|
+
ref: stashInfo.ref,
|
|
2043
|
+
branch: stashInfo.branch,
|
|
2044
|
+
message: stashInfo.message,
|
|
2045
|
+
files,
|
|
2046
|
+
fileCount: files.length
|
|
2047
|
+
};
|
|
2048
|
+
})));
|
|
1016
2049
|
});
|
|
1017
2050
|
const handleDrop = handleCheck(async (stashes) => {
|
|
1018
2051
|
const selectedStashes = await createCheckbox({
|
|
@@ -1039,10 +2072,58 @@ const handleClear = handleCheck(async () => {
|
|
|
1039
2072
|
if (error) log.show("Failed to clear stashes.", { type: "error" });
|
|
1040
2073
|
else log.show("Successfully cleared stashes.", { type: "success" });
|
|
1041
2074
|
});
|
|
2075
|
+
const handleHistory = async (options) => {
|
|
2076
|
+
if (options.clean) {
|
|
2077
|
+
const days = Number.parseInt(options.clean, 10) || 30;
|
|
2078
|
+
const count = await cleanOldStashes(days);
|
|
2079
|
+
log.show(`Cleaned ${count} old stash records (${days} days)`, { type: "success" });
|
|
2080
|
+
return;
|
|
2081
|
+
}
|
|
2082
|
+
const stashes = await getAllStashes(options.active ? "active" : void 0);
|
|
2083
|
+
const displayList = options.all ? stashes : stashes.slice(0, 10);
|
|
2084
|
+
displayList.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
2085
|
+
if (displayList.length === 0) {
|
|
2086
|
+
log.show("No stash records found.", { type: "info" });
|
|
2087
|
+
return;
|
|
2088
|
+
}
|
|
2089
|
+
log.show(`\n${colors.bold(`📚 Stash History (${displayList.length} records)`)}\n`);
|
|
2090
|
+
for (const stash of displayList) {
|
|
2091
|
+
const statusEmoji = stash.status === "active" ? "📦" : "✅";
|
|
2092
|
+
const statusColor = stash.status === "active" ? colors.yellow : colors.green;
|
|
2093
|
+
log.show(colors.cyan(`━━━ ${statusEmoji} ${stash.message} ━━━`));
|
|
2094
|
+
log.show(colors.dim(` Operation: ${stash.operation || "unknown"}`));
|
|
2095
|
+
log.show(colors.dim(` Status: ${statusColor(stash.status || "unknown")}`));
|
|
2096
|
+
log.show(colors.dim(` Branch: ${stash.currentBranch || "unknown"}`));
|
|
2097
|
+
log.show(colors.dim(` Time: ${new Date(stash.timestamp).toLocaleString()}`));
|
|
2098
|
+
if (stash.files && stash.files.length > 0) {
|
|
2099
|
+
log.show(colors.dim(` Files (${stash.files.length}):`));
|
|
2100
|
+
const filesToShow = stash.files.slice(0, 5);
|
|
2101
|
+
for (const file of filesToShow) log.show(colors.yellow(` • ${file}`));
|
|
2102
|
+
if (stash.files.length > 5) log.show(colors.dim(` ... and ${stash.files.length - 5} more`));
|
|
2103
|
+
}
|
|
2104
|
+
if (stash.error) log.show(colors.red(` Error: ${stash.error}`));
|
|
2105
|
+
log.show("");
|
|
2106
|
+
}
|
|
2107
|
+
if (!options.all && stashes.length > 10) log.show(colors.dim(`\nShowing 10 of ${stashes.length} records. Use --all to see all.\n`));
|
|
2108
|
+
};
|
|
1042
2109
|
const stashCommand = (command) => {
|
|
1043
2110
|
const stashCmd = command.command("stash").alias("st").description("Git stash management").addHelpText("after", HELP_MESSAGE.main);
|
|
2111
|
+
stashCmd.action(async () => {
|
|
2112
|
+
if (!await handleGitStash(void 0, { operation: "manual" })) log.show("No changes to stash.", { type: "info" });
|
|
2113
|
+
});
|
|
1044
2114
|
stashCmd.command("save [message]").alias("s").description("Save current changes to stash").action(async (message) => {
|
|
1045
|
-
|
|
2115
|
+
if (message) {
|
|
2116
|
+
if (!await handleGitStash(message, { operation: "manual" })) log.show("No changes to stash.", { type: "info" });
|
|
2117
|
+
} else try {
|
|
2118
|
+
if (!await handleGitStash(await createInput({
|
|
2119
|
+
message: "Enter stash message (press Enter to use default)",
|
|
2120
|
+
validate: (value) => {
|
|
2121
|
+
if (!value?.trim()) return "Message cannot be empty";
|
|
2122
|
+
}
|
|
2123
|
+
}), { operation: "manual" })) log.show("No changes to stash.", { type: "info" });
|
|
2124
|
+
} catch (error) {
|
|
2125
|
+
log.show("Stash cancelled.", { type: "info" });
|
|
2126
|
+
}
|
|
1046
2127
|
});
|
|
1047
2128
|
stashCmd.command("list").alias("ls").alias("l").description("List all stashes").action(async () => {
|
|
1048
2129
|
await handleList();
|
|
@@ -1056,9 +2137,57 @@ const stashCommand = (command) => {
|
|
|
1056
2137
|
stashCmd.command("clear").alias("c").description("clear stashes").action(async () => {
|
|
1057
2138
|
await handleClear();
|
|
1058
2139
|
});
|
|
2140
|
+
stashCmd.command("history").alias("his").alias("h").description("View stash history from persistent index").option("--all", "Show all records (no limit)").option("--active", "Show only active records").option("--clean [days]", "Clean old records (default: 30 days)").action(async (options) => {
|
|
2141
|
+
await handleHistory(options);
|
|
2142
|
+
});
|
|
1059
2143
|
return stashCmd;
|
|
1060
2144
|
};
|
|
1061
2145
|
|
|
2146
|
+
//#endregion
|
|
2147
|
+
//#region src/commands/status.ts
|
|
2148
|
+
/**
|
|
2149
|
+
* 获取修改的文件列表
|
|
2150
|
+
*/
|
|
2151
|
+
const getStatusFiles = async () => {
|
|
2152
|
+
const [error, result] = await xASync("git", ["status", "--porcelain"], { quiet: true });
|
|
2153
|
+
if (error) return [];
|
|
2154
|
+
const lines = result.stdout.split("\n").filter((line) => line.trim());
|
|
2155
|
+
const files = [];
|
|
2156
|
+
for (const line of lines) {
|
|
2157
|
+
if (line.length < 3) continue;
|
|
2158
|
+
const stagedStatus = line[0] ?? " ";
|
|
2159
|
+
const unstagedStatus = line[1] ?? " ";
|
|
2160
|
+
const path = line.slice(3).trim();
|
|
2161
|
+
let status = unstagedStatus;
|
|
2162
|
+
let staged = false;
|
|
2163
|
+
if (unstagedStatus === " " && stagedStatus !== " " && stagedStatus !== "?") {
|
|
2164
|
+
status = stagedStatus;
|
|
2165
|
+
staged = true;
|
|
2166
|
+
} else if (unstagedStatus !== " ") {
|
|
2167
|
+
status = unstagedStatus;
|
|
2168
|
+
staged = false;
|
|
2169
|
+
}
|
|
2170
|
+
if (status === "?") continue;
|
|
2171
|
+
files.push({
|
|
2172
|
+
path,
|
|
2173
|
+
status,
|
|
2174
|
+
staged
|
|
2175
|
+
});
|
|
2176
|
+
}
|
|
2177
|
+
return files;
|
|
2178
|
+
};
|
|
2179
|
+
const statusCommand = (command) => {
|
|
2180
|
+
command.command("status").alias("s").description("Show working tree status (interactive viewer)").action(async () => {
|
|
2181
|
+
const files = await getStatusFiles();
|
|
2182
|
+
if (files.length === 0) {
|
|
2183
|
+
log.show("✓ Working directory clean", { type: "success" });
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
renderStatusViewer(files);
|
|
2187
|
+
});
|
|
2188
|
+
return command;
|
|
2189
|
+
};
|
|
2190
|
+
|
|
1062
2191
|
//#endregion
|
|
1063
2192
|
//#region src/index.ts
|
|
1064
2193
|
const pkg = readPackage(import.meta, "..");
|
|
@@ -1072,7 +2201,11 @@ const init = () => {
|
|
|
1072
2201
|
diffCommand(command);
|
|
1073
2202
|
mergeCommand(command);
|
|
1074
2203
|
stashCommand(command);
|
|
2204
|
+
blameCommand(command);
|
|
1075
2205
|
commitCommand(command);
|
|
2206
|
+
statusCommand(command);
|
|
2207
|
+
histCommand(command);
|
|
2208
|
+
configCommand(command);
|
|
1076
2209
|
return command;
|
|
1077
2210
|
};
|
|
1078
2211
|
const run = async () => {
|