@nemo-cli/git 0.1.2 → 0.1.3
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 +419 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1128 -52
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,10 +1,220 @@
|
|
|
1
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";
|
|
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",
|
|
@@ -56,6 +399,18 @@ const getLocalOptions = async () => {
|
|
|
56
399
|
currentBranch
|
|
57
400
|
};
|
|
58
401
|
};
|
|
402
|
+
const getRemoteOptionsForRemotes = 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
|
*/
|
|
@@ -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) => {
|
|
@@ -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 remotes;
|
|
1093
|
+
try {
|
|
1094
|
+
remotes = (await getRemoteOptionsForRemotes()).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 (remotes.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 selectedRemote = remotes[0];
|
|
1106
|
+
if (remotes.length > 1) selectedRemote = await createSelect({
|
|
1107
|
+
message: "Select remote repository",
|
|
1108
|
+
options: remotes.map((remote) => ({
|
|
1109
|
+
label: remote,
|
|
1110
|
+
value: remote
|
|
1111
|
+
})),
|
|
1112
|
+
initialValue: remotes[0]
|
|
1113
|
+
});
|
|
1114
|
+
if (await createConfirm({ message: `Do you want to push ${colors.bgGreen(currentBranch)} to ${selectedRemote}?` })) {
|
|
1115
|
+
await handlePush(currentBranch, selectedRemote);
|
|
637
1116
|
return;
|
|
638
1117
|
}
|
|
639
1118
|
const { options } = await getRemoteOptions();
|
|
640
|
-
|
|
1119
|
+
const selectedBranch = await createSelect({
|
|
641
1120
|
message: "Select the branch to push",
|
|
642
1121
|
options,
|
|
643
1122
|
initialValue: "main"
|
|
644
|
-
})
|
|
1123
|
+
});
|
|
1124
|
+
let pushRemote = selectedRemote;
|
|
1125
|
+
if (remotes.length > 1) {
|
|
1126
|
+
if (!await createConfirm({ message: `Push ${colors.bgGreen(selectedBranch)} to ${selectedRemote}?` })) pushRemote = await createSelect({
|
|
1127
|
+
message: "Select remote repository",
|
|
1128
|
+
options: remotes.map((remote) => ({
|
|
1129
|
+
label: remote,
|
|
1130
|
+
value: remote
|
|
1131
|
+
})),
|
|
1132
|
+
initialValue: selectedRemote
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
await handlePush(selectedBranch, pushRemote);
|
|
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) {
|
|
@@ -855,11 +1808,17 @@ function listCommand(command) {
|
|
|
855
1808
|
const handleMerge = async (branch) => {
|
|
856
1809
|
const spinner = createSpinner(`Merging branch ${branch}...`);
|
|
857
1810
|
const args = ["merge", branch];
|
|
858
|
-
const
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
1811
|
+
const stashResult = await handleGitStash(void 0, {
|
|
1812
|
+
branch,
|
|
1813
|
+
operation: "merge"
|
|
1814
|
+
});
|
|
1815
|
+
try {
|
|
1816
|
+
const [error] = await xASync("git", args, { nodeOptions: { stdio: "inherit" } });
|
|
1817
|
+
if (error) log.show(`Failed to merge branch ${branch}.`, { type: "error" });
|
|
1818
|
+
else spinner.stop(`Successfully merged branch ${branch}.`);
|
|
1819
|
+
} finally {
|
|
1820
|
+
stashResult && handleGitPop(stashResult);
|
|
1821
|
+
}
|
|
863
1822
|
};
|
|
864
1823
|
function mergeCommand(command) {
|
|
865
1824
|
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) => {
|
|
@@ -928,16 +1887,19 @@ function pullCommand(command) {
|
|
|
928
1887
|
}],
|
|
929
1888
|
initialValue: "merge"
|
|
930
1889
|
}) === "rebase";
|
|
931
|
-
const
|
|
1890
|
+
const stashResult = await handleGitStash(void 0, {
|
|
1891
|
+
branch: selectedBranch,
|
|
1892
|
+
operation: "pull"
|
|
1893
|
+
});
|
|
932
1894
|
await handleGitPull(selectedBranch, { rebase: useRebase });
|
|
933
|
-
|
|
1895
|
+
stashResult && handleGitPop(stashResult);
|
|
934
1896
|
});
|
|
935
1897
|
}
|
|
936
1898
|
|
|
937
1899
|
//#endregion
|
|
938
1900
|
//#region src/constants/stash.ts
|
|
939
1901
|
const HELP_MESSAGE = {
|
|
940
|
-
main: createHelpExample("ng stash", "ng stash save \"work in progress\"", "ng stash ls", "ng stash pop", "ng stash drop"),
|
|
1902
|
+
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
1903
|
save: createHelpExample("ng stash save \"work in progress\""),
|
|
942
1904
|
list: createHelpExample("ng stash ls"),
|
|
943
1905
|
pop: createHelpExample("ng stash pop"),
|
|
@@ -1002,17 +1964,31 @@ const handlePop = handleCheck(async (stashes) => {
|
|
|
1002
1964
|
else log.show("Successfully popped changes.", { type: "success" });
|
|
1003
1965
|
}
|
|
1004
1966
|
});
|
|
1967
|
+
const parseStashEntry = (stashEntry) => {
|
|
1968
|
+
const match = stashEntry.match(/^(stash@\{\d+\}):\s+On\s+(\S+):\s+(.+)$/);
|
|
1969
|
+
if (match) return {
|
|
1970
|
+
ref: match[1] || stashEntry,
|
|
1971
|
+
branch: match[2] || "unknown",
|
|
1972
|
+
message: match[3] || stashEntry
|
|
1973
|
+
};
|
|
1974
|
+
return {
|
|
1975
|
+
ref: stashEntry.match(/^(stash@\{\d+\})/)?.[1] ?? stashEntry.split(":")[0] ?? stashEntry,
|
|
1976
|
+
branch: "unknown",
|
|
1977
|
+
message: stashEntry
|
|
1978
|
+
};
|
|
1979
|
+
};
|
|
1005
1980
|
const handleList = handleCheck(async (stashes) => {
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
const files = await getStashFiles(
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1981
|
+
await renderStashList(await Promise.all(stashes.map(async (stash) => {
|
|
1982
|
+
const stashInfo = parseStashEntry(stash);
|
|
1983
|
+
const files = await getStashFiles(stashInfo.ref);
|
|
1984
|
+
return {
|
|
1985
|
+
ref: stashInfo.ref,
|
|
1986
|
+
branch: stashInfo.branch,
|
|
1987
|
+
message: stashInfo.message,
|
|
1988
|
+
files,
|
|
1989
|
+
fileCount: files.length
|
|
1990
|
+
};
|
|
1991
|
+
})));
|
|
1016
1992
|
});
|
|
1017
1993
|
const handleDrop = handleCheck(async (stashes) => {
|
|
1018
1994
|
const selectedStashes = await createCheckbox({
|
|
@@ -1039,10 +2015,58 @@ const handleClear = handleCheck(async () => {
|
|
|
1039
2015
|
if (error) log.show("Failed to clear stashes.", { type: "error" });
|
|
1040
2016
|
else log.show("Successfully cleared stashes.", { type: "success" });
|
|
1041
2017
|
});
|
|
2018
|
+
const handleHistory = async (options) => {
|
|
2019
|
+
if (options.clean) {
|
|
2020
|
+
const days = Number.parseInt(options.clean, 10) || 30;
|
|
2021
|
+
const count = await cleanOldStashes(days);
|
|
2022
|
+
log.show(`Cleaned ${count} old stash records (${days} days)`, { type: "success" });
|
|
2023
|
+
return;
|
|
2024
|
+
}
|
|
2025
|
+
const stashes = await getAllStashes(options.active ? "active" : void 0);
|
|
2026
|
+
const displayList = options.all ? stashes : stashes.slice(0, 10);
|
|
2027
|
+
displayList.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
2028
|
+
if (displayList.length === 0) {
|
|
2029
|
+
log.show("No stash records found.", { type: "info" });
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
log.show(`\n${colors.bold(`📚 Stash History (${displayList.length} records)`)}\n`);
|
|
2033
|
+
for (const stash of displayList) {
|
|
2034
|
+
const statusEmoji = stash.status === "active" ? "📦" : "✅";
|
|
2035
|
+
const statusColor = stash.status === "active" ? colors.yellow : colors.green;
|
|
2036
|
+
log.show(colors.cyan(`━━━ ${statusEmoji} ${stash.message} ━━━`));
|
|
2037
|
+
log.show(colors.dim(` Operation: ${stash.operation || "unknown"}`));
|
|
2038
|
+
log.show(colors.dim(` Status: ${statusColor(stash.status || "unknown")}`));
|
|
2039
|
+
log.show(colors.dim(` Branch: ${stash.currentBranch || "unknown"}`));
|
|
2040
|
+
log.show(colors.dim(` Time: ${new Date(stash.timestamp).toLocaleString()}`));
|
|
2041
|
+
if (stash.files && stash.files.length > 0) {
|
|
2042
|
+
log.show(colors.dim(` Files (${stash.files.length}):`));
|
|
2043
|
+
const filesToShow = stash.files.slice(0, 5);
|
|
2044
|
+
for (const file of filesToShow) log.show(colors.yellow(` • ${file}`));
|
|
2045
|
+
if (stash.files.length > 5) log.show(colors.dim(` ... and ${stash.files.length - 5} more`));
|
|
2046
|
+
}
|
|
2047
|
+
if (stash.error) log.show(colors.red(` Error: ${stash.error}`));
|
|
2048
|
+
log.show("");
|
|
2049
|
+
}
|
|
2050
|
+
if (!options.all && stashes.length > 10) log.show(colors.dim(`\nShowing 10 of ${stashes.length} records. Use --all to see all.\n`));
|
|
2051
|
+
};
|
|
1042
2052
|
const stashCommand = (command) => {
|
|
1043
2053
|
const stashCmd = command.command("stash").alias("st").description("Git stash management").addHelpText("after", HELP_MESSAGE.main);
|
|
2054
|
+
stashCmd.action(async () => {
|
|
2055
|
+
if (!await handleGitStash(void 0, { operation: "manual" })) log.show("No changes to stash.", { type: "info" });
|
|
2056
|
+
});
|
|
1044
2057
|
stashCmd.command("save [message]").alias("s").description("Save current changes to stash").action(async (message) => {
|
|
1045
|
-
|
|
2058
|
+
if (message) {
|
|
2059
|
+
if (!await handleGitStash(message, { operation: "manual" })) log.show("No changes to stash.", { type: "info" });
|
|
2060
|
+
} else try {
|
|
2061
|
+
if (!await handleGitStash(await createInput({
|
|
2062
|
+
message: "Enter stash message (press Enter to use default)",
|
|
2063
|
+
validate: (value) => {
|
|
2064
|
+
if (!value?.trim()) return "Message cannot be empty";
|
|
2065
|
+
}
|
|
2066
|
+
}), { operation: "manual" })) log.show("No changes to stash.", { type: "info" });
|
|
2067
|
+
} catch (error) {
|
|
2068
|
+
log.show("Stash cancelled.", { type: "info" });
|
|
2069
|
+
}
|
|
1046
2070
|
});
|
|
1047
2071
|
stashCmd.command("list").alias("ls").alias("l").description("List all stashes").action(async () => {
|
|
1048
2072
|
await handleList();
|
|
@@ -1056,9 +2080,57 @@ const stashCommand = (command) => {
|
|
|
1056
2080
|
stashCmd.command("clear").alias("c").description("clear stashes").action(async () => {
|
|
1057
2081
|
await handleClear();
|
|
1058
2082
|
});
|
|
2083
|
+
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) => {
|
|
2084
|
+
await handleHistory(options);
|
|
2085
|
+
});
|
|
1059
2086
|
return stashCmd;
|
|
1060
2087
|
};
|
|
1061
2088
|
|
|
2089
|
+
//#endregion
|
|
2090
|
+
//#region src/commands/status.ts
|
|
2091
|
+
/**
|
|
2092
|
+
* 获取修改的文件列表
|
|
2093
|
+
*/
|
|
2094
|
+
const getStatusFiles = async () => {
|
|
2095
|
+
const [error, result] = await xASync("git", ["status", "--porcelain"], { quiet: true });
|
|
2096
|
+
if (error) return [];
|
|
2097
|
+
const lines = result.stdout.split("\n").filter((line) => line.trim());
|
|
2098
|
+
const files = [];
|
|
2099
|
+
for (const line of lines) {
|
|
2100
|
+
if (line.length < 3) continue;
|
|
2101
|
+
const stagedStatus = line[0] ?? " ";
|
|
2102
|
+
const unstagedStatus = line[1] ?? " ";
|
|
2103
|
+
const path = line.slice(3).trim();
|
|
2104
|
+
let status = unstagedStatus;
|
|
2105
|
+
let staged = false;
|
|
2106
|
+
if (unstagedStatus === " " && stagedStatus !== " " && stagedStatus !== "?") {
|
|
2107
|
+
status = stagedStatus;
|
|
2108
|
+
staged = true;
|
|
2109
|
+
} else if (unstagedStatus !== " ") {
|
|
2110
|
+
status = unstagedStatus;
|
|
2111
|
+
staged = false;
|
|
2112
|
+
}
|
|
2113
|
+
if (status === "?") continue;
|
|
2114
|
+
files.push({
|
|
2115
|
+
path,
|
|
2116
|
+
status,
|
|
2117
|
+
staged
|
|
2118
|
+
});
|
|
2119
|
+
}
|
|
2120
|
+
return files;
|
|
2121
|
+
};
|
|
2122
|
+
const statusCommand = (command) => {
|
|
2123
|
+
command.command("status").alias("s").description("Show working tree status (interactive viewer)").action(async () => {
|
|
2124
|
+
const files = await getStatusFiles();
|
|
2125
|
+
if (files.length === 0) {
|
|
2126
|
+
log.show("✓ Working directory clean", { type: "success" });
|
|
2127
|
+
return;
|
|
2128
|
+
}
|
|
2129
|
+
renderStatusViewer(files);
|
|
2130
|
+
});
|
|
2131
|
+
return command;
|
|
2132
|
+
};
|
|
2133
|
+
|
|
1062
2134
|
//#endregion
|
|
1063
2135
|
//#region src/index.ts
|
|
1064
2136
|
const pkg = readPackage(import.meta, "..");
|
|
@@ -1072,7 +2144,11 @@ const init = () => {
|
|
|
1072
2144
|
diffCommand(command);
|
|
1073
2145
|
mergeCommand(command);
|
|
1074
2146
|
stashCommand(command);
|
|
2147
|
+
blameCommand(command);
|
|
1075
2148
|
commitCommand(command);
|
|
2149
|
+
statusCommand(command);
|
|
2150
|
+
histCommand(command);
|
|
2151
|
+
configCommand(command);
|
|
1076
2152
|
return command;
|
|
1077
2153
|
};
|
|
1078
2154
|
const run = async () => {
|