@co0ontty/wand 1.25.5 → 1.26.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/git-quick-commit.d.ts +17 -1
- package/dist/git-quick-commit.js +248 -32
- package/dist/server-session-routes.js +58 -1
- package/dist/types.d.ts +32 -0
- package/dist/web-ui/content/scripts.js +499 -47
- package/dist/web-ui/content/styles.css +285 -0
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GitStatusResult, QuickCommitResult } from "./types.js";
|
|
1
|
+
import { GitStatusResult, PushResult, QuickCommitResult, TagHeadResult } from "./types.js";
|
|
2
2
|
export declare function getGitStatus(cwd: string): GitStatusResult;
|
|
3
3
|
interface QuickCommitOptions {
|
|
4
4
|
cwd: string;
|
|
@@ -20,5 +20,21 @@ export interface GenerateCommitMessageResult {
|
|
|
20
20
|
suggestedTag?: string;
|
|
21
21
|
}
|
|
22
22
|
export declare function generateCommitMessageOnly(cwd: string, language: string): Promise<GenerateCommitMessageResult>;
|
|
23
|
+
interface TagHeadOptions {
|
|
24
|
+
cwd: string;
|
|
25
|
+
language: string;
|
|
26
|
+
/** Explicit tag name. If empty and `autoTag` is true, ask Claude to generate one. */
|
|
27
|
+
tag?: string;
|
|
28
|
+
autoTag?: boolean;
|
|
29
|
+
/** Push only this tag to its upstream remote after creating it. */
|
|
30
|
+
push?: boolean;
|
|
31
|
+
}
|
|
32
|
+
export declare function runTagHead(opts: TagHeadOptions): Promise<TagHeadResult>;
|
|
33
|
+
interface PushOptions {
|
|
34
|
+
cwd: string;
|
|
35
|
+
pushCommits?: boolean;
|
|
36
|
+
pushTags?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export declare function runPush(opts: PushOptions): Promise<PushResult>;
|
|
23
39
|
export declare function runQuickCommit(opts: QuickCommitOptions): Promise<QuickCommitResult>;
|
|
24
40
|
export {};
|
package/dist/git-quick-commit.js
CHANGED
|
@@ -141,6 +141,86 @@ export function getGitStatus(cwd) {
|
|
|
141
141
|
}
|
|
142
142
|
const allEntries = parsePorcelainV2(porcelain);
|
|
143
143
|
const files = allEntries.slice(0, MAX_FILE_ENTRIES);
|
|
144
|
+
// Upstream / ahead / behind ──────────────────────────────────────────
|
|
145
|
+
let hasUpstream = false;
|
|
146
|
+
let upstream;
|
|
147
|
+
let ahead;
|
|
148
|
+
let behind;
|
|
149
|
+
if (!initialCommit) {
|
|
150
|
+
try {
|
|
151
|
+
upstream = runGit(["rev-parse", "--abbrev-ref", "@{upstream}"], cwd) || undefined;
|
|
152
|
+
hasUpstream = !!upstream;
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
hasUpstream = false;
|
|
156
|
+
}
|
|
157
|
+
if (hasUpstream) {
|
|
158
|
+
try {
|
|
159
|
+
const counts = runGit(["rev-list", "--left-right", "--count", "@{upstream}...HEAD"], cwd);
|
|
160
|
+
const parts = counts.split(/\s+/).filter(Boolean);
|
|
161
|
+
if (parts.length === 2) {
|
|
162
|
+
const b = Number.parseInt(parts[0], 10);
|
|
163
|
+
const a = Number.parseInt(parts[1], 10);
|
|
164
|
+
if (!Number.isNaN(a))
|
|
165
|
+
ahead = a;
|
|
166
|
+
if (!Number.isNaN(b))
|
|
167
|
+
behind = b;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
// ignore — keep counts undefined
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// HEAD commit info ───────────────────────────────────────────────────
|
|
176
|
+
let lastCommit;
|
|
177
|
+
if (!initialCommit) {
|
|
178
|
+
try {
|
|
179
|
+
const raw = runGit(["log", "-1", "--pretty=format:%H%x09%h%x09%s"], cwd);
|
|
180
|
+
const parts = raw.split("\t");
|
|
181
|
+
if (parts.length >= 3) {
|
|
182
|
+
lastCommit = { hash: parts[0], shortHash: parts[1], subject: parts.slice(2).join("\t") };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// ignore
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// Unpushed tag count (best-effort; remote refs may not be fresh) ─────
|
|
190
|
+
let unpushedTagCount;
|
|
191
|
+
if (hasUpstream && upstream) {
|
|
192
|
+
const slashIdx = upstream.indexOf("/");
|
|
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
|
+
}
|
|
144
224
|
return {
|
|
145
225
|
isGit: true,
|
|
146
226
|
branch,
|
|
@@ -149,6 +229,12 @@ export function getGitStatus(cwd) {
|
|
|
149
229
|
head,
|
|
150
230
|
repoRoot,
|
|
151
231
|
initialCommit,
|
|
232
|
+
hasUpstream,
|
|
233
|
+
upstream,
|
|
234
|
+
ahead,
|
|
235
|
+
behind,
|
|
236
|
+
lastCommit,
|
|
237
|
+
unpushedTagCount,
|
|
152
238
|
};
|
|
153
239
|
}
|
|
154
240
|
export class QuickCommitError extends Error {
|
|
@@ -356,7 +442,165 @@ ${diff}`;
|
|
|
356
442
|
}
|
|
357
443
|
return suggested;
|
|
358
444
|
}
|
|
359
|
-
|
|
445
|
+
/**
|
|
446
|
+
* Push current branch (with upstream auto-setup) and/or tags.
|
|
447
|
+
* Errors are returned via `error` so callers can present partial-success states.
|
|
448
|
+
*/
|
|
449
|
+
function doPush(cwd, pushCommits, pushTags) {
|
|
450
|
+
let pushedCommits = false;
|
|
451
|
+
let pushedTags = false;
|
|
452
|
+
let hasUpstream = false;
|
|
453
|
+
let pushRemote = "origin";
|
|
454
|
+
try {
|
|
455
|
+
runGit(["rev-parse", "--abbrev-ref", "@{upstream}"], cwd);
|
|
456
|
+
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
|
+
}
|
|
467
|
+
catch {
|
|
468
|
+
hasUpstream = false;
|
|
469
|
+
}
|
|
470
|
+
try {
|
|
471
|
+
if (pushCommits) {
|
|
472
|
+
if (hasUpstream) {
|
|
473
|
+
runGit(["push", "--recurse-submodules=on-demand"], cwd, GIT_PUSH_TIMEOUT_MS);
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
runGit(["push", "-u", "--recurse-submodules=on-demand", pushRemote, "HEAD"], cwd, GIT_PUSH_TIMEOUT_MS);
|
|
477
|
+
}
|
|
478
|
+
pushedCommits = true;
|
|
479
|
+
}
|
|
480
|
+
if (pushTags) {
|
|
481
|
+
runGit(["push", pushRemote, "--tags"], cwd, GIT_PUSH_TIMEOUT_MS);
|
|
482
|
+
pushedTags = true;
|
|
483
|
+
}
|
|
484
|
+
return { pushedCommits, pushedTags };
|
|
485
|
+
}
|
|
486
|
+
catch (error) {
|
|
487
|
+
return { pushedCommits, pushedTags, error: getGitErrorMessage(error) };
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
export async function runTagHead(opts) {
|
|
491
|
+
const { cwd, language, tag, autoTag, push } = opts;
|
|
492
|
+
if (!cwd || !existsSync(cwd)) {
|
|
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
|
|
506
|
+
let headHash;
|
|
507
|
+
try {
|
|
508
|
+
headHash = runGit(["rev-parse", "HEAD"], cwd);
|
|
509
|
+
}
|
|
510
|
+
catch {
|
|
511
|
+
throw new QuickCommitError("仓库还没有任何 commit,无法打 tag。", "NO_COMMIT");
|
|
512
|
+
}
|
|
513
|
+
let tagName = (tag || "").trim();
|
|
514
|
+
if (!tagName && autoTag) {
|
|
515
|
+
// Reuse the post-commit generator — it inspects `git show HEAD` directly.
|
|
516
|
+
let headSubject = "";
|
|
517
|
+
try {
|
|
518
|
+
headSubject = runGit(["log", "-1", "--pretty=format:%s"], cwd);
|
|
519
|
+
}
|
|
520
|
+
catch {
|
|
521
|
+
headSubject = "";
|
|
522
|
+
}
|
|
523
|
+
tagName = await generateTagAfterCommit(cwd, language, headSubject || "");
|
|
524
|
+
}
|
|
525
|
+
if (!tagName) {
|
|
526
|
+
throw new QuickCommitError("请填写 tag 名称,或开启 AI 生成。", "EMPTY_TAG");
|
|
527
|
+
}
|
|
528
|
+
// Refuse to overwrite an existing tag — surface a clear error code.
|
|
529
|
+
try {
|
|
530
|
+
runGit(["rev-parse", "--verify", `refs/tags/${tagName}`], cwd);
|
|
531
|
+
throw new QuickCommitError(`tag \`${tagName}\` 已存在。`, "TAG_EXISTS");
|
|
532
|
+
}
|
|
533
|
+
catch (error) {
|
|
534
|
+
if (error instanceof QuickCommitError)
|
|
535
|
+
throw error;
|
|
536
|
+
// not found — good, proceed
|
|
537
|
+
}
|
|
538
|
+
try {
|
|
539
|
+
runGit(["tag", tagName], cwd);
|
|
540
|
+
}
|
|
541
|
+
catch (error) {
|
|
542
|
+
throw new QuickCommitError(`git tag 失败:${getGitErrorMessage(error)}`, "GIT_TAG_FAILED");
|
|
543
|
+
}
|
|
544
|
+
let pushed = false;
|
|
545
|
+
let pushError;
|
|
546
|
+
if (push) {
|
|
547
|
+
let pushRemote = "origin";
|
|
548
|
+
try {
|
|
549
|
+
const currentBranch = runGit(["branch", "--show-current"], cwd);
|
|
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
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return {
|
|
572
|
+
ok: true,
|
|
573
|
+
tag: { name: tagName, commit: headHash.slice(0, 7) },
|
|
574
|
+
pushed,
|
|
575
|
+
pushError,
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
export async function runPush(opts) {
|
|
579
|
+
const { cwd, pushCommits = true, pushTags = false } = opts;
|
|
580
|
+
if (!cwd || !existsSync(cwd)) {
|
|
581
|
+
throw new QuickCommitError("工作目录不存在。", "CWD_MISSING");
|
|
582
|
+
}
|
|
583
|
+
if (!pushCommits && !pushTags) {
|
|
584
|
+
throw new QuickCommitError("没有要推送的内容。", "NOTHING_TO_PUSH");
|
|
585
|
+
}
|
|
586
|
+
let isInside;
|
|
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);
|
|
597
|
+
return {
|
|
598
|
+
ok: !outcome.error,
|
|
599
|
+
pushedCommits: outcome.pushedCommits,
|
|
600
|
+
pushedTags: outcome.pushedTags,
|
|
601
|
+
error: outcome.error,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
360
604
|
export async function runQuickCommit(opts) {
|
|
361
605
|
const { cwd, language, autoMessage, customMessage, tag, autoTag, push } = opts;
|
|
362
606
|
if (!cwd || !existsSync(cwd)) {
|
|
@@ -435,37 +679,9 @@ export async function runQuickCommit(opts) {
|
|
|
435
679
|
let pushed = false;
|
|
436
680
|
let pushError;
|
|
437
681
|
if (push) {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
try {
|
|
442
|
-
runGit(["rev-parse", "--abbrev-ref", "@{upstream}"], cwd);
|
|
443
|
-
hasUpstream = true;
|
|
444
|
-
try {
|
|
445
|
-
const currentBranch = runGit(["branch", "--show-current"], cwd);
|
|
446
|
-
if (currentBranch) {
|
|
447
|
-
pushRemote = runGit(["config", "--get", `branch.${currentBranch}.remote`], cwd) || "origin";
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
catch {
|
|
451
|
-
pushRemote = "origin";
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
catch {
|
|
455
|
-
hasUpstream = false;
|
|
456
|
-
}
|
|
457
|
-
if (hasUpstream) {
|
|
458
|
-
runGit(["push", "--recurse-submodules=on-demand"], cwd, GIT_PUSH_TIMEOUT_MS);
|
|
459
|
-
}
|
|
460
|
-
else {
|
|
461
|
-
runGit(["push", "-u", "--recurse-submodules=on-demand", pushRemote, "HEAD"], cwd, GIT_PUSH_TIMEOUT_MS);
|
|
462
|
-
}
|
|
463
|
-
runGit(["push", pushRemote, "--tags"], cwd, GIT_PUSH_TIMEOUT_MS);
|
|
464
|
-
pushed = true;
|
|
465
|
-
}
|
|
466
|
-
catch (error) {
|
|
467
|
-
pushError = getGitErrorMessage(error);
|
|
468
|
-
}
|
|
682
|
+
const outcome = doPush(cwd, true, !!tagName);
|
|
683
|
+
pushed = outcome.pushedCommits && (tagName ? outcome.pushedTags : true);
|
|
684
|
+
pushError = outcome.error;
|
|
469
685
|
}
|
|
470
686
|
return {
|
|
471
687
|
ok: true,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import express from "express";
|
|
2
2
|
import { SessionInputError } from "./process-manager.js";
|
|
3
3
|
import { checkSessionWorktreeMergeability, cleanupSessionWorktree, getWorktreeMergeErrorCode, mergeSessionWorktree, WorktreeMergeError } from "./git-worktree.js";
|
|
4
|
-
import { getGitStatus, QuickCommitError, runQuickCommit, generateCommitMessageOnly } from "./git-quick-commit.js";
|
|
4
|
+
import { getGitStatus, QuickCommitError, runQuickCommit, runTagHead, runPush, generateCommitMessageOnly, } from "./git-quick-commit.js";
|
|
5
5
|
export function getErrorMessage(error, fallback) {
|
|
6
6
|
return error instanceof Error ? error.message : fallback;
|
|
7
7
|
}
|
|
@@ -388,6 +388,63 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
388
388
|
res.status(400).json({ error: getErrorMessage(error, "生成 commit message 失败。") });
|
|
389
389
|
}
|
|
390
390
|
});
|
|
391
|
+
app.post("/api/sessions/:id/git/tag-head", express.json(), async (req, res) => {
|
|
392
|
+
const snapshot = getLatestSessionSnapshot(processes, structured, storage, req.params.id);
|
|
393
|
+
if (!snapshot) {
|
|
394
|
+
res.status(404).json({ error: "未找到该会话。" });
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (!snapshot.cwd) {
|
|
398
|
+
res.status(400).json({ error: "会话没有工作目录。", errorCode: "NO_CWD" });
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const body = (req.body ?? {});
|
|
402
|
+
try {
|
|
403
|
+
const result = await runTagHead({
|
|
404
|
+
cwd: snapshot.cwd,
|
|
405
|
+
language: config.language ?? "",
|
|
406
|
+
tag: typeof body.tag === "string" ? body.tag : undefined,
|
|
407
|
+
autoTag: !!body.autoTag,
|
|
408
|
+
push: !!body.push,
|
|
409
|
+
});
|
|
410
|
+
res.json(result);
|
|
411
|
+
}
|
|
412
|
+
catch (error) {
|
|
413
|
+
if (error instanceof QuickCommitError) {
|
|
414
|
+
const status = error.code === "TAG_EXISTS" ? 409 : 400;
|
|
415
|
+
res.status(status).json({ error: error.message, errorCode: error.code });
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
res.status(400).json({ error: getErrorMessage(error, "打 tag 失败。") });
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
app.post("/api/sessions/:id/git/push", express.json(), async (req, res) => {
|
|
422
|
+
const snapshot = getLatestSessionSnapshot(processes, structured, storage, req.params.id);
|
|
423
|
+
if (!snapshot) {
|
|
424
|
+
res.status(404).json({ error: "未找到该会话。" });
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (!snapshot.cwd) {
|
|
428
|
+
res.status(400).json({ error: "会话没有工作目录。", errorCode: "NO_CWD" });
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const body = (req.body ?? {});
|
|
432
|
+
try {
|
|
433
|
+
const result = await runPush({
|
|
434
|
+
cwd: snapshot.cwd,
|
|
435
|
+
pushCommits: body.pushCommits !== false,
|
|
436
|
+
pushTags: !!body.pushTags,
|
|
437
|
+
});
|
|
438
|
+
res.json(result);
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
if (error instanceof QuickCommitError) {
|
|
442
|
+
res.status(400).json({ error: error.message, errorCode: error.code });
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
res.status(400).json({ error: getErrorMessage(error, "推送失败。") });
|
|
446
|
+
}
|
|
447
|
+
});
|
|
391
448
|
app.post("/api/sessions/:id/worktree/cleanup", (req, res) => {
|
|
392
449
|
try {
|
|
393
450
|
const current = requireWorktreeSession(getLatestSessionSnapshot(processes, structured, storage, req.params.id));
|
package/dist/types.d.ts
CHANGED
|
@@ -169,6 +169,22 @@ export interface GitStatusResult {
|
|
|
169
169
|
repoRoot?: string;
|
|
170
170
|
/** Truthy when the repo has no commits yet (initial state). */
|
|
171
171
|
initialCommit?: boolean;
|
|
172
|
+
/** Whether current branch has an upstream tracking branch. */
|
|
173
|
+
hasUpstream?: boolean;
|
|
174
|
+
/** Upstream branch identifier (e.g. `origin/main`). Only set when `hasUpstream` is true. */
|
|
175
|
+
upstream?: string;
|
|
176
|
+
/** Number of local commits not yet on upstream. Only meaningful when `hasUpstream` is true. */
|
|
177
|
+
ahead?: number;
|
|
178
|
+
/** Number of upstream commits not yet locally. Only meaningful when `hasUpstream` is true. */
|
|
179
|
+
behind?: number;
|
|
180
|
+
/** HEAD commit subject + short hash (handy for "tag the current commit" UX). */
|
|
181
|
+
lastCommit?: {
|
|
182
|
+
hash: string;
|
|
183
|
+
shortHash: string;
|
|
184
|
+
subject: string;
|
|
185
|
+
};
|
|
186
|
+
/** Number of local tags that don't exist on the remote (best-effort, may be undefined if not reachable). */
|
|
187
|
+
unpushedTagCount?: number;
|
|
172
188
|
error?: string;
|
|
173
189
|
}
|
|
174
190
|
export interface QuickCommitResult {
|
|
@@ -184,6 +200,22 @@ export interface QuickCommitResult {
|
|
|
184
200
|
/** commit 已成功但 push 失败时填入;前端用它显示"已提交但 push 失败"。 */
|
|
185
201
|
pushError?: string;
|
|
186
202
|
}
|
|
203
|
+
export interface TagHeadResult {
|
|
204
|
+
ok: boolean;
|
|
205
|
+
tag: {
|
|
206
|
+
name: string;
|
|
207
|
+
commit: string;
|
|
208
|
+
};
|
|
209
|
+
pushed?: boolean;
|
|
210
|
+
pushError?: string;
|
|
211
|
+
}
|
|
212
|
+
export interface PushResult {
|
|
213
|
+
ok: boolean;
|
|
214
|
+
pushedCommits: boolean;
|
|
215
|
+
pushedTags: boolean;
|
|
216
|
+
/** Either operation failed — the other may still have succeeded. */
|
|
217
|
+
error?: string;
|
|
218
|
+
}
|
|
187
219
|
export interface CommandRequest {
|
|
188
220
|
command: string;
|
|
189
221
|
provider?: SessionProvider;
|