@co0ontty/wand 1.26.0 → 1.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config.js +24 -0
- package/dist/git-quick-commit.d.ts +3 -2
- package/dist/git-quick-commit.js +170 -133
- package/dist/server.js +214 -5
- package/dist/structured-session-manager.d.ts +20 -0
- package/dist/structured-session-manager.js +192 -18
- package/dist/types.d.ts +25 -7
- package/dist/web-ui/content/scripts.js +375 -79
- package/dist/ws-broadcast.d.ts +10 -0
- package/dist/ws-broadcast.js +75 -0
- package/package.json +1 -1
package/dist/config.js
CHANGED
|
@@ -41,6 +41,7 @@ export const defaultConfig = () => ({
|
|
|
41
41
|
shortcutLogMaxBytes: 10 * 1024 * 1024,
|
|
42
42
|
language: "",
|
|
43
43
|
android: defaultAndroidApkConfig(),
|
|
44
|
+
macos: defaultMacosDmgConfig(),
|
|
44
45
|
cardDefaults: defaultCardExpandDefaults(),
|
|
45
46
|
defaultModel: "",
|
|
46
47
|
structuredRunner: "cli",
|
|
@@ -301,6 +302,28 @@ function normalizeAndroidApkConfig(input) {
|
|
|
301
302
|
: defaults.currentApkFile,
|
|
302
303
|
};
|
|
303
304
|
}
|
|
305
|
+
function defaultMacosDmgConfig() {
|
|
306
|
+
return {
|
|
307
|
+
enabled: false,
|
|
308
|
+
dmgDir: "macos",
|
|
309
|
+
currentDmgFile: "",
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function normalizeMacosDmgConfig(input) {
|
|
313
|
+
if (!input || typeof input !== "object")
|
|
314
|
+
return undefined;
|
|
315
|
+
const defaults = defaultMacosDmgConfig();
|
|
316
|
+
const macosInput = input;
|
|
317
|
+
return {
|
|
318
|
+
enabled: typeof macosInput.enabled === "boolean" ? macosInput.enabled : defaults.enabled,
|
|
319
|
+
dmgDir: typeof macosInput.dmgDir === "string" && macosInput.dmgDir.trim()
|
|
320
|
+
? macosInput.dmgDir.trim()
|
|
321
|
+
: defaults.dmgDir,
|
|
322
|
+
currentDmgFile: typeof macosInput.currentDmgFile === "string"
|
|
323
|
+
? macosInput.currentDmgFile.trim()
|
|
324
|
+
: defaults.currentDmgFile,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
304
327
|
function normalizeStructuredChatPersona(input) {
|
|
305
328
|
if (!input || typeof input !== "object")
|
|
306
329
|
return undefined;
|
|
@@ -358,6 +381,7 @@ function mergeWithDefaults(input) {
|
|
|
358
381
|
? input.appSecret
|
|
359
382
|
: crypto.randomBytes(32).toString("hex"),
|
|
360
383
|
android: normalizeAndroidApkConfig(input.android) ?? defaults.android,
|
|
384
|
+
macos: normalizeMacosDmgConfig(input.macos) ?? defaults.macos,
|
|
361
385
|
cardDefaults: normalizeCardDefaults(input.cardDefaults),
|
|
362
386
|
defaultModel: typeof input.defaultModel === "string" ? input.defaultModel.trim() : defaults.defaultModel,
|
|
363
387
|
structuredRunner: (input.structuredRunner === "sdk" || input.structuredRunner === "cli") ? input.structuredRunner : defaults.structuredRunner,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { GitStatusResult, PushResult, QuickCommitResult, TagHeadResult } from "./types.js";
|
|
2
|
+
export type QuickCommitErrorCode = "CWD_MISSING" | "NO_CWD" | "NOT_A_GIT_REPO" | "NO_COMMIT" | "NOTHING_TO_COMMIT" | "NOTHING_TO_PUSH" | "EMPTY_MESSAGE" | "EMPTY_TAG" | "EMPTY_AI_MESSAGE" | "INVALID_AI_TAG" | "TAG_EXISTS" | "GIT_ADD_FAILED" | "GIT_DIFF_FAILED" | "GIT_COMMIT_FAILED" | "GIT_TAG_FAILED" | "CLAUDE_CLI_MISSING" | "CLAUDE_CLI_FAILED" | "CLAUDE_TIMEOUT";
|
|
2
3
|
export declare function getGitStatus(cwd: string): GitStatusResult;
|
|
3
4
|
interface QuickCommitOptions {
|
|
4
5
|
cwd: string;
|
|
@@ -11,8 +12,8 @@ interface QuickCommitOptions {
|
|
|
11
12
|
push?: boolean;
|
|
12
13
|
}
|
|
13
14
|
export declare class QuickCommitError extends Error {
|
|
14
|
-
readonly code:
|
|
15
|
-
constructor(message: string, code:
|
|
15
|
+
readonly code: QuickCommitErrorCode;
|
|
16
|
+
constructor(message: string, code: QuickCommitErrorCode);
|
|
16
17
|
}
|
|
17
18
|
export interface GenerateCommitMessageResult {
|
|
18
19
|
message: string;
|
package/dist/git-quick-commit.js
CHANGED
|
@@ -31,6 +31,38 @@ function getGitErrorMessage(error) {
|
|
|
31
31
|
return e.message;
|
|
32
32
|
return String(error);
|
|
33
33
|
}
|
|
34
|
+
/** Throws `QuickCommitError` if `cwd` isn't an existing path inside a git work tree. */
|
|
35
|
+
function assertGitWorkTree(cwd) {
|
|
36
|
+
if (!cwd || !existsSync(cwd)) {
|
|
37
|
+
throw new QuickCommitError("工作目录不存在。", "CWD_MISSING");
|
|
38
|
+
}
|
|
39
|
+
let isInside;
|
|
40
|
+
try {
|
|
41
|
+
isInside = runGit(["rev-parse", "--is-inside-work-tree"], cwd);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
throw new QuickCommitError(getGitErrorMessage(error), "NOT_A_GIT_REPO");
|
|
45
|
+
}
|
|
46
|
+
if (isInside !== "true") {
|
|
47
|
+
throw new QuickCommitError("当前目录不在 git 仓库内。", "NOT_A_GIT_REPO");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Resolve the remote to push to. Prefers `branch.<name>.remote` config for the
|
|
52
|
+
* current branch, falls back to `origin`. Never throws.
|
|
53
|
+
*/
|
|
54
|
+
function resolvePushRemote(cwd) {
|
|
55
|
+
try {
|
|
56
|
+
const branch = runGit(["branch", "--show-current"], cwd);
|
|
57
|
+
if (branch) {
|
|
58
|
+
return runGit(["config", "--get", `branch.${branch}.remote`], cwd) || "origin";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// ignore — fall through to default
|
|
63
|
+
}
|
|
64
|
+
return "origin";
|
|
65
|
+
}
|
|
34
66
|
function unquotePath(raw) {
|
|
35
67
|
if (raw.startsWith("\"") && raw.endsWith("\"")) {
|
|
36
68
|
return raw.slice(1, -1).replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
|
|
@@ -141,20 +173,18 @@ export function getGitStatus(cwd) {
|
|
|
141
173
|
}
|
|
142
174
|
const allEntries = parsePorcelainV2(porcelain);
|
|
143
175
|
const files = allEntries.slice(0, MAX_FILE_ENTRIES);
|
|
144
|
-
// Upstream / ahead / behind ──────────────────────────────────────────
|
|
145
|
-
let hasUpstream = false;
|
|
146
176
|
let upstream;
|
|
147
177
|
let ahead;
|
|
148
178
|
let behind;
|
|
179
|
+
let lastCommit;
|
|
149
180
|
if (!initialCommit) {
|
|
150
181
|
try {
|
|
151
182
|
upstream = runGit(["rev-parse", "--abbrev-ref", "@{upstream}"], cwd) || undefined;
|
|
152
|
-
hasUpstream = !!upstream;
|
|
153
183
|
}
|
|
154
184
|
catch {
|
|
155
|
-
|
|
185
|
+
upstream = undefined;
|
|
156
186
|
}
|
|
157
|
-
if (
|
|
187
|
+
if (upstream) {
|
|
158
188
|
try {
|
|
159
189
|
const counts = runGit(["rev-list", "--left-right", "--count", "@{upstream}...HEAD"], cwd);
|
|
160
190
|
const parts = counts.split(/\s+/).filter(Boolean);
|
|
@@ -168,13 +198,9 @@ export function getGitStatus(cwd) {
|
|
|
168
198
|
}
|
|
169
199
|
}
|
|
170
200
|
catch {
|
|
171
|
-
// ignore —
|
|
201
|
+
// ignore — counts stay undefined
|
|
172
202
|
}
|
|
173
203
|
}
|
|
174
|
-
}
|
|
175
|
-
// HEAD commit info ───────────────────────────────────────────────────
|
|
176
|
-
let lastCommit;
|
|
177
|
-
if (!initialCommit) {
|
|
178
204
|
try {
|
|
179
205
|
const raw = runGit(["log", "-1", "--pretty=format:%H%x09%h%x09%s"], cwd);
|
|
180
206
|
const parts = raw.split("\t");
|
|
@@ -186,41 +212,10 @@ export function getGitStatus(cwd) {
|
|
|
186
212
|
// ignore
|
|
187
213
|
}
|
|
188
214
|
}
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const remoteName = slashIdx > 0 ? upstream.slice(0, slashIdx) : "origin";
|
|
194
|
-
try {
|
|
195
|
-
const localTagsRaw = runGit(["for-each-ref", "--format=%(refname:short)", "refs/tags"], cwd);
|
|
196
|
-
const localTags = localTagsRaw.split(/\r?\n/).filter((t) => t.trim().length > 0);
|
|
197
|
-
if (localTags.length === 0) {
|
|
198
|
-
unpushedTagCount = 0;
|
|
199
|
-
}
|
|
200
|
-
else {
|
|
201
|
-
const remoteTagsRaw = runGit(["ls-remote", "--tags", remoteName], cwd, 3000);
|
|
202
|
-
const remoteTags = new Set();
|
|
203
|
-
for (const line of remoteTagsRaw.split(/\r?\n/)) {
|
|
204
|
-
// format: <sha>\trefs/tags/<name>{^}? — strip annotated suffix
|
|
205
|
-
const tabIdx = line.indexOf("\t");
|
|
206
|
-
if (tabIdx === -1)
|
|
207
|
-
continue;
|
|
208
|
-
const ref = line.slice(tabIdx + 1).trim();
|
|
209
|
-
if (!ref.startsWith("refs/tags/"))
|
|
210
|
-
continue;
|
|
211
|
-
let name = ref.slice("refs/tags/".length);
|
|
212
|
-
if (name.endsWith("^{}"))
|
|
213
|
-
name = name.slice(0, -3);
|
|
214
|
-
if (name)
|
|
215
|
-
remoteTags.add(name);
|
|
216
|
-
}
|
|
217
|
-
unpushedTagCount = localTags.filter((t) => !remoteTags.has(t)).length;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
catch {
|
|
221
|
-
// remote unreachable — leave undefined so UI can hide the chip
|
|
222
|
-
}
|
|
223
|
-
}
|
|
215
|
+
// NOTE: we intentionally do NOT probe the remote for unpushed tags here.
|
|
216
|
+
// `ls-remote` is a synchronous network call that can block the event loop
|
|
217
|
+
// for seconds. The "unpushed tag" UI chip is best-effort and a separate
|
|
218
|
+
// async endpoint should compute it on demand if reintroduced.
|
|
224
219
|
return {
|
|
225
220
|
isGit: true,
|
|
226
221
|
branch,
|
|
@@ -229,12 +224,10 @@ export function getGitStatus(cwd) {
|
|
|
229
224
|
head,
|
|
230
225
|
repoRoot,
|
|
231
226
|
initialCommit,
|
|
232
|
-
hasUpstream,
|
|
233
227
|
upstream,
|
|
234
228
|
ahead,
|
|
235
229
|
behind,
|
|
236
230
|
lastCommit,
|
|
237
|
-
unpushedTagCount,
|
|
238
231
|
};
|
|
239
232
|
}
|
|
240
233
|
export class QuickCommitError extends Error {
|
|
@@ -446,27 +439,19 @@ ${diff}`;
|
|
|
446
439
|
* Push current branch (with upstream auto-setup) and/or tags.
|
|
447
440
|
* Errors are returned via `error` so callers can present partial-success states.
|
|
448
441
|
*/
|
|
449
|
-
function doPush(
|
|
442
|
+
function doPush(opts) {
|
|
443
|
+
const { cwd, pushCommits, pushTags } = opts;
|
|
450
444
|
let pushedCommits = false;
|
|
451
445
|
let pushedTags = false;
|
|
452
446
|
let hasUpstream = false;
|
|
453
|
-
let pushRemote = "origin";
|
|
454
447
|
try {
|
|
455
448
|
runGit(["rev-parse", "--abbrev-ref", "@{upstream}"], cwd);
|
|
456
449
|
hasUpstream = true;
|
|
457
|
-
try {
|
|
458
|
-
const currentBranch = runGit(["branch", "--show-current"], cwd);
|
|
459
|
-
if (currentBranch) {
|
|
460
|
-
pushRemote = runGit(["config", "--get", `branch.${currentBranch}.remote`], cwd) || "origin";
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
catch {
|
|
464
|
-
pushRemote = "origin";
|
|
465
|
-
}
|
|
466
450
|
}
|
|
467
451
|
catch {
|
|
468
452
|
hasUpstream = false;
|
|
469
453
|
}
|
|
454
|
+
const pushRemote = resolvePushRemote(cwd);
|
|
470
455
|
try {
|
|
471
456
|
if (pushCommits) {
|
|
472
457
|
if (hasUpstream) {
|
|
@@ -478,7 +463,14 @@ function doPush(cwd, pushCommits, pushTags) {
|
|
|
478
463
|
pushedCommits = true;
|
|
479
464
|
}
|
|
480
465
|
if (pushTags) {
|
|
481
|
-
|
|
466
|
+
if (Array.isArray(pushTags)) {
|
|
467
|
+
for (const name of pushTags) {
|
|
468
|
+
runGit(["push", pushRemote, `refs/tags/${name}`], cwd, GIT_PUSH_TIMEOUT_MS);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
runGit(["push", pushRemote, "--tags"], cwd, GIT_PUSH_TIMEOUT_MS);
|
|
473
|
+
}
|
|
482
474
|
pushedTags = true;
|
|
483
475
|
}
|
|
484
476
|
return { pushedCommits, pushedTags };
|
|
@@ -489,20 +481,7 @@ function doPush(cwd, pushCommits, pushTags) {
|
|
|
489
481
|
}
|
|
490
482
|
export async function runTagHead(opts) {
|
|
491
483
|
const { cwd, language, tag, autoTag, push } = opts;
|
|
492
|
-
|
|
493
|
-
throw new QuickCommitError("工作目录不存在。", "CWD_MISSING");
|
|
494
|
-
}
|
|
495
|
-
let isInside;
|
|
496
|
-
try {
|
|
497
|
-
isInside = runGit(["rev-parse", "--is-inside-work-tree"], cwd);
|
|
498
|
-
}
|
|
499
|
-
catch (error) {
|
|
500
|
-
throw new QuickCommitError(getGitErrorMessage(error), "NOT_A_GIT_REPO");
|
|
501
|
-
}
|
|
502
|
-
if (isInside !== "true") {
|
|
503
|
-
throw new QuickCommitError("当前目录不在 git 仓库内。", "NOT_A_GIT_REPO");
|
|
504
|
-
}
|
|
505
|
-
// Need an existing commit to tag
|
|
484
|
+
assertGitWorkTree(cwd);
|
|
506
485
|
let headHash;
|
|
507
486
|
try {
|
|
508
487
|
headHash = runGit(["rev-parse", "HEAD"], cwd);
|
|
@@ -512,7 +491,6 @@ export async function runTagHead(opts) {
|
|
|
512
491
|
}
|
|
513
492
|
let tagName = (tag || "").trim();
|
|
514
493
|
if (!tagName && autoTag) {
|
|
515
|
-
// Reuse the post-commit generator — it inspects `git show HEAD` directly.
|
|
516
494
|
let headSubject = "";
|
|
517
495
|
try {
|
|
518
496
|
headSubject = runGit(["log", "-1", "--pretty=format:%s"], cwd);
|
|
@@ -544,29 +522,9 @@ export async function runTagHead(opts) {
|
|
|
544
522
|
let pushed = false;
|
|
545
523
|
let pushError;
|
|
546
524
|
if (push) {
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
if (currentBranch) {
|
|
551
|
-
try {
|
|
552
|
-
pushRemote = runGit(["config", "--get", `branch.${currentBranch}.remote`], cwd) || "origin";
|
|
553
|
-
}
|
|
554
|
-
catch {
|
|
555
|
-
pushRemote = "origin";
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
catch {
|
|
560
|
-
pushRemote = "origin";
|
|
561
|
-
}
|
|
562
|
-
try {
|
|
563
|
-
// Push just this one tag — cheaper and more targeted than `--tags`.
|
|
564
|
-
runGit(["push", pushRemote, `refs/tags/${tagName}`], cwd, GIT_PUSH_TIMEOUT_MS);
|
|
565
|
-
pushed = true;
|
|
566
|
-
}
|
|
567
|
-
catch (error) {
|
|
568
|
-
pushError = getGitErrorMessage(error);
|
|
569
|
-
}
|
|
525
|
+
const outcome = doPush({ cwd, pushCommits: false, pushTags: [tagName] });
|
|
526
|
+
pushed = outcome.pushedTags;
|
|
527
|
+
pushError = outcome.error;
|
|
570
528
|
}
|
|
571
529
|
return {
|
|
572
530
|
ok: true,
|
|
@@ -577,23 +535,11 @@ export async function runTagHead(opts) {
|
|
|
577
535
|
}
|
|
578
536
|
export async function runPush(opts) {
|
|
579
537
|
const { cwd, pushCommits = true, pushTags = false } = opts;
|
|
580
|
-
|
|
581
|
-
throw new QuickCommitError("工作目录不存在。", "CWD_MISSING");
|
|
582
|
-
}
|
|
538
|
+
assertGitWorkTree(cwd);
|
|
583
539
|
if (!pushCommits && !pushTags) {
|
|
584
540
|
throw new QuickCommitError("没有要推送的内容。", "NOTHING_TO_PUSH");
|
|
585
541
|
}
|
|
586
|
-
|
|
587
|
-
try {
|
|
588
|
-
isInside = runGit(["rev-parse", "--is-inside-work-tree"], cwd);
|
|
589
|
-
}
|
|
590
|
-
catch (error) {
|
|
591
|
-
throw new QuickCommitError(getGitErrorMessage(error), "NOT_A_GIT_REPO");
|
|
592
|
-
}
|
|
593
|
-
if (isInside !== "true") {
|
|
594
|
-
throw new QuickCommitError("当前目录不在 git 仓库内。", "NOT_A_GIT_REPO");
|
|
595
|
-
}
|
|
596
|
-
const outcome = doPush(cwd, pushCommits, pushTags);
|
|
542
|
+
const outcome = doPush({ cwd, pushCommits, pushTags });
|
|
597
543
|
return {
|
|
598
544
|
ok: !outcome.error,
|
|
599
545
|
pushedCommits: outcome.pushedCommits,
|
|
@@ -601,29 +547,85 @@ export async function runPush(opts) {
|
|
|
601
547
|
error: outcome.error,
|
|
602
548
|
};
|
|
603
549
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
550
|
+
/**
|
|
551
|
+
* 在 commit 父仓库之前,先在每个内部 dirty / untracked 的 submodule 里
|
|
552
|
+
* 执行一次 `git add -A` + `git commit -m <msg>`,让父仓库的 add -A
|
|
553
|
+
* 能正确捕捉到新的 submodule 指针。纯指针变化(仅 commitChanged)的
|
|
554
|
+
* submodule 不会进入这条路径——那种情况父仓库 add 已经够了。
|
|
555
|
+
*
|
|
556
|
+
* 任一 submodule 提交失败都会被收集为非致命错误,不阻塞父仓库继续 commit;
|
|
557
|
+
* 调用方可以在结果里读到具体哪几个 submodule 失败。
|
|
558
|
+
*/
|
|
559
|
+
function commitDirtySubmodules(parentCwd, message) {
|
|
560
|
+
const commits = [];
|
|
561
|
+
const errors = [];
|
|
562
|
+
let porcelain;
|
|
610
563
|
try {
|
|
611
|
-
|
|
564
|
+
porcelain = runGitAllowEmpty(["status", "--porcelain=v2", "--untracked-files=all"], parentCwd);
|
|
612
565
|
}
|
|
613
|
-
catch
|
|
614
|
-
|
|
566
|
+
catch {
|
|
567
|
+
return { commits, errors };
|
|
615
568
|
}
|
|
616
|
-
|
|
617
|
-
|
|
569
|
+
const entries = parsePorcelainV2(porcelain);
|
|
570
|
+
for (const entry of entries) {
|
|
571
|
+
if (!entry.isSubmodule)
|
|
572
|
+
continue;
|
|
573
|
+
const state = entry.submoduleState;
|
|
574
|
+
if (!state)
|
|
575
|
+
continue;
|
|
576
|
+
// 只有内部脏 / 未跟踪才需要进入子目录提交;纯指针变化父仓库自己就能 add。
|
|
577
|
+
if (!state.hasTrackedChanges && !state.hasUntracked)
|
|
578
|
+
continue;
|
|
579
|
+
const subCwd = `${parentCwd}/${entry.path}`;
|
|
580
|
+
if (!existsSync(subCwd)) {
|
|
581
|
+
errors.push(`submodule ${entry.path} 路径不存在`);
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
try {
|
|
585
|
+
runGit(["add", "-A"], subCwd, 5000);
|
|
586
|
+
}
|
|
587
|
+
catch (error) {
|
|
588
|
+
errors.push(`submodule ${entry.path} add 失败:${getGitErrorMessage(error)}`);
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
// 子仓 add 之后再判断是否真的有 staged 内容:极端情况下 .gitignore 把所有 dirty
|
|
592
|
+
// 文件都过滤掉了,会得到一个空 diff,此时跳过避免空 commit。
|
|
593
|
+
let staged;
|
|
594
|
+
try {
|
|
595
|
+
staged = runGitAllowEmpty(["diff", "--cached", "--name-only"], subCwd).trim();
|
|
596
|
+
}
|
|
597
|
+
catch {
|
|
598
|
+
staged = "";
|
|
599
|
+
}
|
|
600
|
+
if (!staged)
|
|
601
|
+
continue;
|
|
602
|
+
try {
|
|
603
|
+
runGit(["commit", "-m", message], subCwd, 10_000);
|
|
604
|
+
}
|
|
605
|
+
catch (error) {
|
|
606
|
+
errors.push(`submodule ${entry.path} commit 失败:${getGitErrorMessage(error)}`);
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
let hash = "";
|
|
610
|
+
try {
|
|
611
|
+
hash = runGit(["rev-parse", "--short", "HEAD"], subCwd);
|
|
612
|
+
}
|
|
613
|
+
catch { /* ignore */ }
|
|
614
|
+
commits.push({ path: entry.path, hash });
|
|
618
615
|
}
|
|
619
|
-
|
|
616
|
+
return { commits, errors };
|
|
617
|
+
}
|
|
618
|
+
export async function runQuickCommit(opts) {
|
|
619
|
+
const { cwd, language, autoMessage, customMessage, tag, autoTag, push } = opts;
|
|
620
|
+
assertGitWorkTree(cwd);
|
|
621
|
+
// 先 add 一次让我们能在 collectStagedDiff 看到完整改动(包含 submodule 指针),
|
|
622
|
+
// AI 生成 message 时也基于这个 staged diff。
|
|
620
623
|
try {
|
|
621
624
|
runGit(["add", "-A"], cwd, 5000);
|
|
622
625
|
}
|
|
623
626
|
catch (error) {
|
|
624
627
|
throw new QuickCommitError(`git add 失败:${getGitErrorMessage(error)}`, "GIT_ADD_FAILED");
|
|
625
628
|
}
|
|
626
|
-
// Step 2: check if anything to commit
|
|
627
629
|
let stagedFiles;
|
|
628
630
|
try {
|
|
629
631
|
stagedFiles = runGitAllowEmpty(["diff", "--cached", "--name-only"], cwd).trim();
|
|
@@ -631,10 +633,18 @@ export async function runQuickCommit(opts) {
|
|
|
631
633
|
catch (error) {
|
|
632
634
|
throw new QuickCommitError(getGitErrorMessage(error), "GIT_DIFF_FAILED");
|
|
633
635
|
}
|
|
634
|
-
|
|
636
|
+
// 父仓库本身可能没有 staged 文件,但 submodule 内部有 dirty / untracked——
|
|
637
|
+
// 此时也应该允许走 submodule 提交流程。
|
|
638
|
+
let parentHasStaged = stagedFiles.length > 0;
|
|
639
|
+
let submoduleHasDirty = false;
|
|
640
|
+
try {
|
|
641
|
+
const porcelain = runGitAllowEmpty(["status", "--porcelain=v2", "--untracked-files=all"], cwd);
|
|
642
|
+
submoduleHasDirty = parsePorcelainV2(porcelain).some((e) => e.isSubmodule && (e.submoduleState?.hasTrackedChanges || e.submoduleState?.hasUntracked));
|
|
643
|
+
}
|
|
644
|
+
catch { /* keep submoduleHasDirty=false */ }
|
|
645
|
+
if (!parentHasStaged && !submoduleHasDirty) {
|
|
635
646
|
throw new QuickCommitError("没有任何改动可以提交。", "NOTHING_TO_COMMIT");
|
|
636
647
|
}
|
|
637
|
-
// Step 3: get commit message
|
|
638
648
|
let message;
|
|
639
649
|
if (autoMessage) {
|
|
640
650
|
message = await generateCommitMessage(cwd, language);
|
|
@@ -645,7 +655,32 @@ export async function runQuickCommit(opts) {
|
|
|
645
655
|
throw new QuickCommitError("commit message 不能为空。", "EMPTY_MESSAGE");
|
|
646
656
|
}
|
|
647
657
|
}
|
|
648
|
-
//
|
|
658
|
+
// 先提交 submodule 内部改动;父仓库随后再 add 一次,picks up 新的 submodule 指针。
|
|
659
|
+
const submoduleOutcome = commitDirtySubmodules(cwd, message);
|
|
660
|
+
if (submoduleOutcome.commits.length > 0) {
|
|
661
|
+
try {
|
|
662
|
+
runGit(["add", "-A"], cwd, 5000);
|
|
663
|
+
}
|
|
664
|
+
catch (error) {
|
|
665
|
+
throw new QuickCommitError(`父仓库 add submodule 指针失败:${getGitErrorMessage(error)}`, "GIT_ADD_FAILED");
|
|
666
|
+
}
|
|
667
|
+
// 重新评估父仓库是否有 staged 内容:如果 submodule 是新引入的或指针变了,
|
|
668
|
+
// 这里应当为真;如果完全没变就走 commit --allow-empty 路径不合适,直接报错。
|
|
669
|
+
try {
|
|
670
|
+
stagedFiles = runGitAllowEmpty(["diff", "--cached", "--name-only"], cwd).trim();
|
|
671
|
+
parentHasStaged = stagedFiles.length > 0;
|
|
672
|
+
}
|
|
673
|
+
catch { /* keep stale value */ }
|
|
674
|
+
}
|
|
675
|
+
if (!parentHasStaged) {
|
|
676
|
+
// submodule 都提交了但父仓库还是没有 staged —— 通常意味着 .gitmodules 没动
|
|
677
|
+
// 而 submodule 指针被 ignore(罕见配置)。这种情况返回成功,但用 SUBMODULE_ONLY
|
|
678
|
+
// 的语义;上层 UI 可以决定是否继续 push。这里保持向后兼容,沿用 commit 路径
|
|
679
|
+
// 但用 --allow-empty 会有副作用,干脆抛 NOTHING_TO_COMMIT。
|
|
680
|
+
throw new QuickCommitError(submoduleOutcome.commits.length > 0
|
|
681
|
+
? `已提交 ${submoduleOutcome.commits.length} 个 submodule,但父仓库没有改动可提交。`
|
|
682
|
+
: "没有任何改动可以提交。", "NOTHING_TO_COMMIT");
|
|
683
|
+
}
|
|
649
684
|
try {
|
|
650
685
|
runGit(["commit", "-m", message], cwd, 10_000);
|
|
651
686
|
}
|
|
@@ -659,10 +694,7 @@ export async function runQuickCommit(opts) {
|
|
|
659
694
|
catch {
|
|
660
695
|
commitHash = "";
|
|
661
696
|
}
|
|
662
|
-
//
|
|
663
|
-
// - explicit `tag` wins
|
|
664
|
-
// - if `tag` is empty and `autoTag` is on, ask Claude to generate one
|
|
665
|
-
// - otherwise no tag
|
|
697
|
+
// Tag: explicit `tag` wins; if empty + autoTag, ask Claude; otherwise skip.
|
|
666
698
|
let tagName = (tag || "").trim();
|
|
667
699
|
if (!tagName && autoTag) {
|
|
668
700
|
tagName = await generateTagAfterCommit(cwd, language, message);
|
|
@@ -675,11 +707,15 @@ export async function runQuickCommit(opts) {
|
|
|
675
707
|
throw new QuickCommitError(`git tag 失败:${getGitErrorMessage(error)}`, "GIT_TAG_FAILED");
|
|
676
708
|
}
|
|
677
709
|
}
|
|
678
|
-
// Step 6: push
|
|
679
710
|
let pushed = false;
|
|
680
711
|
let pushError;
|
|
681
712
|
if (push) {
|
|
682
|
-
const outcome = doPush(
|
|
713
|
+
const outcome = doPush({
|
|
714
|
+
cwd,
|
|
715
|
+
pushCommits: true,
|
|
716
|
+
// Push only the freshly-created tag — avoids surprising users by pushing stale local tags.
|
|
717
|
+
pushTags: tagName ? [tagName] : false,
|
|
718
|
+
});
|
|
683
719
|
pushed = outcome.pushedCommits && (tagName ? outcome.pushedTags : true);
|
|
684
720
|
pushError = outcome.error;
|
|
685
721
|
}
|
|
@@ -689,5 +725,6 @@ export async function runQuickCommit(opts) {
|
|
|
689
725
|
tag: tagName ? { name: tagName } : undefined,
|
|
690
726
|
pushed,
|
|
691
727
|
pushError,
|
|
728
|
+
submoduleCommits: submoduleOutcome.commits.length > 0 ? submoduleOutcome.commits : undefined,
|
|
692
729
|
};
|
|
693
730
|
}
|