git-sync-tui 0.1.4 → 0.1.6

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.
Files changed (2) hide show
  1. package/dist/cli.js +881 -224
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -11,9 +11,9 @@ import { render } from "ink";
11
11
  import meow from "meow";
12
12
 
13
13
  // src/app.tsx
14
- import { useState as useState7, useEffect as useEffect4, useRef as useRef2, useCallback as useCallback2 } from "react";
15
- import { Box as Box9, useApp } from "ink";
16
- import { Spinner as Spinner6 } from "@inkjs/ui";
14
+ import { useState as useState8, useEffect as useEffect6, useRef as useRef5, useCallback as useCallback3 } from "react";
15
+ import { Box as Box10, useApp } from "ink";
16
+ import { Spinner as Spinner7 } from "@inkjs/ui";
17
17
 
18
18
  // src/components/ui.tsx
19
19
  import React from "react";
@@ -32,14 +32,18 @@ function StepProgress({ current }) {
32
32
  " ",
33
33
  isActive ? /* @__PURE__ */ jsx(Text, { bold: true, children: label }) : label
34
34
  ] }),
35
- !isLast && /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: " \u2500 " })
35
+ !isLast && /* @__PURE__ */ jsxs(Text, { color: isDone ? "green" : "gray", dimColor: !isDone, children: [
36
+ " ",
37
+ "\u2500\u2500\u2500",
38
+ " "
39
+ ] })
36
40
  ] }, label);
37
41
  }) });
38
42
  }
39
43
  function SectionHeader({ title, subtitle }) {
40
44
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
41
45
  /* @__PURE__ */ jsxs(Text, { bold: true, color: "cyan", children: [
42
- "\u25B8 ",
46
+ "\u25BE ",
43
47
  title
44
48
  ] }),
45
49
  subtitle && /* @__PURE__ */ jsxs(Text, { color: "gray", dimColor: true, children: [
@@ -50,10 +54,8 @@ function SectionHeader({ title, subtitle }) {
50
54
  }
51
55
  function KeyHints({ hints }) {
52
56
  return /* @__PURE__ */ jsx(Box, { gap: 1, flexWrap: "wrap", children: hints.map(({ key, label }) => /* @__PURE__ */ jsxs(Box, { children: [
53
- /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "[" }),
54
- /* @__PURE__ */ jsx(Text, { color: "cyan", children: key }),
55
- /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "]" }),
56
- /* @__PURE__ */ jsxs(Text, { color: "gray", dimColor: true, children: [
57
+ /* @__PURE__ */ jsx(Text, { backgroundColor: "gray", color: "white", bold: true, children: ` ${key} ` }),
58
+ /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
57
59
  " ",
58
60
  label
59
61
  ] })
@@ -61,7 +63,7 @@ function KeyHints({ hints }) {
61
63
  }
62
64
  function InlineKeys({ hints }) {
63
65
  return /* @__PURE__ */ jsx(Box, { gap: 1, children: hints.map(({ key, label }, i) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
64
- /* @__PURE__ */ jsxs(Text, { color: "green", children: [
66
+ /* @__PURE__ */ jsxs(Text, { color: "green", bold: true, children: [
65
67
  "[",
66
68
  key,
67
69
  "]"
@@ -90,15 +92,17 @@ function StatusPanel({ type, title, children }) {
90
92
  children
91
93
  ] });
92
94
  }
93
- function AppHeader({ step, stashed }) {
95
+ function AppHeader({ step, stashed, noCommit }) {
94
96
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
95
- /* @__PURE__ */ jsxs(Box, { children: [
96
- /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "\u25C7 " }),
97
- /* @__PURE__ */ jsx(Text, { bold: true, children: "git-sync-tui" }),
98
- /* @__PURE__ */ jsx(Text, { color: "gray", children: " cherry-pick --no-commit" }),
99
- stashed && /* @__PURE__ */ jsx(Text, { color: "yellow", children: " \u25AA stashed" })
97
+ /* @__PURE__ */ jsxs(Box, { gap: 1, children: [
98
+ /* @__PURE__ */ jsx(Text, { backgroundColor: "cyan", color: "white", bold: true, children: " git-sync-tui " }),
99
+ /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
100
+ "cherry-pick",
101
+ noCommit ? " --no-commit" : ""
102
+ ] }),
103
+ stashed && /* @__PURE__ */ jsx(Text, { backgroundColor: "yellow", color: "white", bold: true, children: " STASHED " })
100
104
  ] }),
101
- /* @__PURE__ */ jsxs(Box, { children: [
105
+ /* @__PURE__ */ jsxs(Box, { marginTop: 0, children: [
102
106
  /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: " " }),
103
107
  /* @__PURE__ */ jsx(StepProgress, { current: step })
104
108
  ] })
@@ -158,28 +162,20 @@ async function addRemote(name, url) {
158
162
  }
159
163
  async function getRemoteBranches(remote2) {
160
164
  const git = getGit();
161
- let fetchOk = false;
162
165
  try {
163
- await git.fetch(remote2);
164
- fetchOk = true;
166
+ const lsResult = await git.raw(["ls-remote", "--heads", remote2]);
167
+ if (lsResult.trim()) {
168
+ return lsResult.trim().split("\n").map((line) => line.replace(/^.*refs\/heads\//, "")).filter(Boolean).sort();
169
+ }
165
170
  } catch {
166
171
  }
167
172
  const result = await git.branch(["-r"]);
168
173
  const prefix = `${remote2}/`;
169
174
  const branches = result.all.filter((b) => b.startsWith(prefix) && !b.includes("HEAD")).map((b) => b.replace(prefix, "")).sort();
170
175
  if (branches.length > 0) return branches;
171
- try {
172
- const lsResult = await git.raw(["ls-remote", "--heads", remote2]);
173
- if (!lsResult.trim()) return [];
174
- return lsResult.trim().split("\n").map((line) => line.replace(/^.*refs\/heads\//, "")).filter(Boolean).sort();
175
- } catch {
176
- if (!fetchOk) {
177
- throw new Error(`\u65E0\u6CD5\u8FDE\u63A5\u8FDC\u7A0B\u4ED3\u5E93 '${remote2}'\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u6216\u4ED3\u5E93\u5730\u5740`);
178
- }
179
- return [];
180
- }
176
+ throw new Error(`\u65E0\u6CD5\u83B7\u53D6\u8FDC\u7A0B\u4ED3\u5E93 '${remote2}' \u7684\u5206\u652F\u5217\u8868\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u6216\u4ED3\u5E93\u5730\u5740`);
181
177
  }
182
- async function getCommits(remote2, branch2, count2 = 30) {
178
+ async function getCommits(remote2, branch2, count2 = 100) {
183
179
  const git = getGit();
184
180
  const ref = `${remote2}/${branch2}`;
185
181
  try {
@@ -194,6 +190,8 @@ async function getCommits(remote2, branch2, count2 = 30) {
194
190
  const result = await git.raw([
195
191
  "log",
196
192
  ref,
193
+ "--first-parent",
194
+ "--no-merges",
197
195
  `--max-count=${count2}`,
198
196
  "--format=%H%n%h%n%s%n%an%n%ar%n---"
199
197
  ]);
@@ -204,6 +202,27 @@ async function getCommits(remote2, branch2, count2 = 30) {
204
202
  return { hash, shortHash, message: message || "", author: author || "", date: date || "" };
205
203
  });
206
204
  }
205
+ async function getUnsyncedCommits(remote2, branch2, count2 = 100) {
206
+ const git = getGit();
207
+ const ref = `${remote2}/${branch2}`;
208
+ const allCommits = await getCommits(remote2, branch2, count2);
209
+ try {
210
+ const result = await git.raw([
211
+ "cherry",
212
+ "HEAD",
213
+ ref
214
+ ]);
215
+ const unsyncedHashes = new Set(
216
+ result.trim().split("\n").filter((line) => line.startsWith("+ ")).map((line) => line.substring(2).trim())
217
+ );
218
+ return allCommits.map((c) => ({
219
+ ...c,
220
+ synced: !unsyncedHashes.has(c.hash)
221
+ }));
222
+ } catch {
223
+ return allCommits;
224
+ }
225
+ }
207
226
  async function getMultiCommitStat(hashes) {
208
227
  if (hashes.length === 0) return "";
209
228
  const git = getGit();
@@ -267,6 +286,45 @@ async function getStagedStat() {
267
286
  return "";
268
287
  }
269
288
  }
289
+ var BACKUP_BRANCH_PREFIX = "git-sync-backup";
290
+ async function createBackupBranch() {
291
+ const git = getGit();
292
+ const timestamp = Date.now();
293
+ const branchName = `${BACKUP_BRANCH_PREFIX}-${timestamp}`;
294
+ await git.raw(["branch", branchName]);
295
+ return branchName;
296
+ }
297
+ async function restoreFromBackup(backupBranch) {
298
+ const git = getGit();
299
+ try {
300
+ await git.raw(["rev-parse", "--verify", backupBranch]);
301
+ await git.raw(["reset", "--hard", backupBranch]);
302
+ await git.raw(["branch", "-D", backupBranch]);
303
+ return { success: true };
304
+ } catch (err) {
305
+ return {
306
+ success: false,
307
+ error: `\u56DE\u9000\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u6267\u884C: git reset --hard ${backupBranch}
308
+ ${err.message}`
309
+ };
310
+ }
311
+ }
312
+ async function deleteBackupBranch(backupBranch) {
313
+ const git = getGit();
314
+ try {
315
+ await git.raw(["branch", "-D", backupBranch]);
316
+ } catch {
317
+ }
318
+ }
319
+ async function getCurrentBranch() {
320
+ const git = getGit();
321
+ const result = await git.raw(["rev-parse", "--abbrev-ref", "HEAD"]);
322
+ return result.trim();
323
+ }
324
+ async function createBranchFrom(newBranch, baseBranch) {
325
+ const git = getGit();
326
+ await git.raw(["checkout", "-b", newBranch, baseBranch]);
327
+ }
270
328
  async function isWorkingDirClean() {
271
329
  const git = getGit();
272
330
  const status = await git.status();
@@ -290,6 +348,87 @@ async function stashPop() {
290
348
  return false;
291
349
  }
292
350
  }
351
+ async function abortCherryPick() {
352
+ const git = getGit();
353
+ try {
354
+ await git.raw(["cherry-pick", "--abort"]);
355
+ } catch {
356
+ }
357
+ }
358
+ async function getConflictFiles() {
359
+ const git = getGit();
360
+ const status = await git.status();
361
+ return status.conflicted;
362
+ }
363
+ async function continueCherryPick() {
364
+ const git = getGit();
365
+ try {
366
+ await git.raw(["cherry-pick", "--continue", "--no-edit"]);
367
+ return { success: true };
368
+ } catch (err) {
369
+ const msg = err.message || "";
370
+ if (msg.includes("nothing to commit") || msg.includes("empty") || msg.includes("allow-empty")) {
371
+ return { success: false, error: msg, empty: true };
372
+ }
373
+ try {
374
+ const status = await git.status();
375
+ const conflictFiles = status.conflicted;
376
+ return {
377
+ success: false,
378
+ error: msg,
379
+ conflictFiles: conflictFiles.length > 0 ? conflictFiles : void 0
380
+ };
381
+ } catch {
382
+ return { success: false, error: msg };
383
+ }
384
+ }
385
+ }
386
+ async function clearCherryPickState() {
387
+ try {
388
+ const gitDir = await getGitDir();
389
+ const cpHead = join(gitDir, "CHERRY_PICK_HEAD");
390
+ if (existsSync(cpHead)) {
391
+ unlinkSync(cpHead);
392
+ }
393
+ } catch {
394
+ }
395
+ }
396
+ async function cherryPickOne(hash, useMainline = false, noCommit = true) {
397
+ const git = getGit();
398
+ try {
399
+ const args = ["cherry-pick"];
400
+ if (noCommit) args.push("--no-commit");
401
+ if (useMainline) args.push("-m", "1");
402
+ args.push(hash);
403
+ await git.raw(args);
404
+ return { success: true };
405
+ } catch (err) {
406
+ try {
407
+ const status = await git.status();
408
+ const conflictFiles = status.conflicted;
409
+ return {
410
+ success: false,
411
+ error: err.message,
412
+ conflictFiles: conflictFiles.length > 0 ? conflictFiles : void 0
413
+ };
414
+ } catch {
415
+ return { success: false, error: err.message };
416
+ }
417
+ }
418
+ }
419
+ async function skipCherryPick() {
420
+ const git = getGit();
421
+ await git.raw(["cherry-pick", "--skip"]);
422
+ }
423
+ async function isCherryPickInProgress() {
424
+ const git = getGit();
425
+ try {
426
+ const gitDir = await getGitDir();
427
+ return existsSync(join(gitDir, "CHERRY_PICK_HEAD"));
428
+ } catch {
429
+ return false;
430
+ }
431
+ }
293
432
  var STASH_GUARD_FILE = "git-sync-tui-stash-guard";
294
433
  async function getGitDir() {
295
434
  const git = getGit();
@@ -393,10 +532,10 @@ function StashRecovery({ timestamp, onRecover, onSkip }) {
393
532
  // src/components/remote-select.tsx
394
533
  import { useState as useState3 } from "react";
395
534
  import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
396
- import { Select, Spinner as Spinner2, TextInput } from "@inkjs/ui";
535
+ import { Spinner as Spinner2, TextInput } from "@inkjs/ui";
397
536
 
398
537
  // src/hooks/use-git.ts
399
- import { useState as useState2, useEffect as useEffect2, useCallback } from "react";
538
+ import { useState as useState2, useEffect as useEffect2, useCallback, useRef } from "react";
400
539
  function useAsync(fn, deps = []) {
401
540
  const [state, setState] = useState2({
402
541
  data: null,
@@ -426,11 +565,49 @@ function useBranches(remote2) {
426
565
  [remote2]
427
566
  );
428
567
  }
429
- function useCommits(remote2, branch2, count2 = 30) {
430
- return useAsync(
431
- () => remote2 && branch2 ? getCommits(remote2, branch2, count2) : Promise.resolve([]),
432
- [remote2, branch2, count2]
433
- );
568
+ function useCommits(remote2, branch2, pageSize = 100) {
569
+ const [data, setData] = useState2(null);
570
+ const [loading, setLoading] = useState2(true);
571
+ const [loadingMore, setLoadingMore] = useState2(false);
572
+ const [error2, setError] = useState2(null);
573
+ const [hasMore, setHasMore] = useState2(true);
574
+ const loadedRef = useRef(0);
575
+ useEffect2(() => {
576
+ if (!remote2 || !branch2) {
577
+ setData([]);
578
+ setLoading(false);
579
+ setHasMore(false);
580
+ return;
581
+ }
582
+ setData(null);
583
+ setLoading(true);
584
+ setError(null);
585
+ setHasMore(true);
586
+ loadedRef.current = 0;
587
+ getUnsyncedCommits(remote2, branch2, pageSize).then((commits2) => {
588
+ setData(commits2);
589
+ setLoading(false);
590
+ loadedRef.current = commits2.length;
591
+ setHasMore(commits2.length >= pageSize);
592
+ }).catch((err) => {
593
+ setError(err.message);
594
+ setLoading(false);
595
+ });
596
+ }, [remote2, branch2, pageSize]);
597
+ const loadMore = useCallback(async () => {
598
+ if (!remote2 || !branch2 || loadingMore || !hasMore) return;
599
+ setLoadingMore(true);
600
+ try {
601
+ const nextCount = loadedRef.current + pageSize;
602
+ const allCommits = await getUnsyncedCommits(remote2, branch2, nextCount);
603
+ setData(allCommits);
604
+ setHasMore(allCommits.length >= nextCount);
605
+ loadedRef.current = allCommits.length;
606
+ } catch {
607
+ }
608
+ setLoadingMore(false);
609
+ }, [remote2, branch2, pageSize, loadingMore, hasMore]);
610
+ return { data, loading, loadingMore, error: error2, hasMore, loadMore };
434
611
  }
435
612
  function useCommitStat(hashes) {
436
613
  const [stat, setStat] = useState2("");
@@ -462,16 +639,32 @@ function extractRemoteName(url) {
462
639
  function RemoteSelect({ onSelect, onBack }) {
463
640
  const { data: remotes, loading, error: error2, reload } = useRemotes();
464
641
  const [phase, setPhase] = useState3("list");
642
+ const [cursorIndex, setCursorIndex] = useState3(0);
465
643
  const [customUrl, setCustomUrl] = useState3("");
466
644
  const [addError, setAddError] = useState3(null);
467
- useInput3((_input, key) => {
645
+ const totalItems = (remotes?.length || 0) + 1;
646
+ useInput3((input, key) => {
647
+ if (phase !== "list") {
648
+ if (key.escape) {
649
+ if (phase === "input-name") {
650
+ setPhase("input-url");
651
+ } else if (phase === "input-url") {
652
+ setPhase("list");
653
+ }
654
+ }
655
+ return;
656
+ }
468
657
  if (key.escape) {
469
- if (phase === "input-name") {
658
+ onBack?.();
659
+ } else if (key.upArrow) {
660
+ setCursorIndex((prev) => Math.max(0, prev - 1));
661
+ } else if (key.downArrow) {
662
+ setCursorIndex((prev) => Math.min(totalItems - 1, prev + 1));
663
+ } else if (key.return) {
664
+ if (remotes && cursorIndex < remotes.length) {
665
+ onSelect(remotes[cursorIndex].name);
666
+ } else {
470
667
  setPhase("input-url");
471
- } else if (phase === "input-url") {
472
- setPhase("list");
473
- } else if (phase === "list") {
474
- onBack?.();
475
668
  }
476
669
  }
477
670
  });
@@ -480,7 +673,8 @@ function RemoteSelect({ onSelect, onBack }) {
480
673
  }
481
674
  if (error2) {
482
675
  return /* @__PURE__ */ jsxs4(Text4, { color: "red", children: [
483
- "\u2716 \u83B7\u53D6\u8FDC\u7A0B\u4ED3\u5E93\u5931\u8D25: ",
676
+ "\u2716 ",
677
+ "\u83B7\u53D6\u8FDC\u7A0B\u4ED3\u5E93\u5931\u8D25: ",
484
678
  error2
485
679
  ] });
486
680
  }
@@ -511,7 +705,10 @@ function RemoteSelect({ onSelect, onBack }) {
511
705
  }
512
706
  )
513
707
  ] }),
514
- /* @__PURE__ */ jsx4(Text4, { color: "gray", dimColor: true, children: " \u652F\u6301 HTTPS / SSH \u5730\u5740" })
708
+ /* @__PURE__ */ jsxs4(Text4, { color: "gray", dimColor: true, children: [
709
+ " ",
710
+ "\u652F\u6301 HTTPS / SSH \u5730\u5740"
711
+ ] })
515
712
  ] });
516
713
  }
517
714
  if (phase === "input-name") {
@@ -555,99 +752,187 @@ function RemoteSelect({ onSelect, onBack }) {
555
752
  ] })
556
753
  ] });
557
754
  }
558
- const options = [
559
- ...(remotes || []).map((r) => ({
560
- label: `${r.name} ${r.fetchUrl}`,
561
- value: r.name
562
- })),
563
- {
564
- label: "+ \u6DFB\u52A0\u8FDC\u7A0B\u4ED3\u5E93...",
565
- value: "__add_custom__"
566
- }
567
- ];
568
- return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", gap: 1, children: [
755
+ const maxNameLen = Math.max(...(remotes || []).map((r) => r.name.length), 0);
756
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
569
757
  /* @__PURE__ */ jsx4(SectionHeader, { title: "\u9009\u62E9\u8FDC\u7A0B\u4ED3\u5E93" }),
570
- /* @__PURE__ */ jsx4(
571
- Select,
572
- {
573
- options,
574
- onChange: (value) => {
575
- if (value === "__add_custom__") {
576
- setPhase("input-url");
577
- } else {
578
- onSelect(value);
579
- }
580
- }
581
- }
582
- )
758
+ /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginTop: 1, children: [
759
+ (remotes || []).map((r, i) => {
760
+ const isCursor = i === cursorIndex;
761
+ return /* @__PURE__ */ jsxs4(Box4, { children: [
762
+ /* @__PURE__ */ jsx4(Text4, { color: isCursor ? "cyan" : "gray", children: isCursor ? "\u203A" : " " }),
763
+ /* @__PURE__ */ jsx4(Text4, { children: " " }),
764
+ /* @__PURE__ */ jsx4(Text4, { color: isCursor ? "cyan" : "white", bold: isCursor, children: r.name.padEnd(maxNameLen + 2) }),
765
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", dimColor: true, children: r.fetchUrl })
766
+ ] }, r.name);
767
+ }),
768
+ /* @__PURE__ */ jsxs4(Box4, { children: [
769
+ /* @__PURE__ */ jsx4(Text4, { color: cursorIndex === (remotes?.length || 0) ? "cyan" : "gray", children: cursorIndex === (remotes?.length || 0) ? "\u203A" : " " }),
770
+ /* @__PURE__ */ jsx4(Text4, { children: " " }),
771
+ /* @__PURE__ */ jsx4(Text4, { color: "green", dimColor: cursorIndex !== (remotes?.length || 0), children: "+ \u6DFB\u52A0\u8FDC\u7A0B\u4ED3\u5E93..." })
772
+ ] })
773
+ ] })
583
774
  ] });
584
775
  }
585
776
 
586
777
  // src/components/branch-select.tsx
587
778
  import { useState as useState4, useMemo } from "react";
588
779
  import { Box as Box5, Text as Text5, useInput as useInput4 } from "ink";
589
- import { Select as Select2, Spinner as Spinner3, TextInput as TextInput2 } from "@inkjs/ui";
780
+ import { Spinner as Spinner3, TextInput as TextInput2 } from "@inkjs/ui";
590
781
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
591
782
  function BranchSelect({ remote: remote2, onSelect, onBack }) {
592
783
  const { data: branches, loading, error: error2 } = useBranches(remote2);
593
784
  const [filter, setFilter] = useState4("");
594
- useInput4((_input, key) => {
595
- if (key.escape) onBack?.();
596
- });
597
- const filteredOptions = useMemo(() => {
785
+ const [cursorIndex, setCursorIndex] = useState4(0);
786
+ const filteredBranches = useMemo(() => {
598
787
  if (!branches) return [];
599
- const filtered = filter ? branches.filter((b) => b.toLowerCase().includes(filter.toLowerCase())) : branches;
600
- return filtered.map((b) => ({ label: b, value: b }));
788
+ return filter ? branches.filter((b) => b.toLowerCase().includes(filter.toLowerCase())) : branches;
601
789
  }, [branches, filter]);
790
+ const visibleCount = 10;
791
+ const startIdx = Math.max(0, Math.min(cursorIndex - Math.floor(visibleCount / 2), filteredBranches.length - visibleCount));
792
+ const visibleBranches = filteredBranches.slice(startIdx, startIdx + visibleCount);
793
+ useInput4((input, key) => {
794
+ if (key.escape) {
795
+ onBack?.();
796
+ } else if (key.upArrow) {
797
+ setCursorIndex((prev) => Math.max(0, prev - 1));
798
+ } else if (key.downArrow) {
799
+ setCursorIndex((prev) => Math.min(filteredBranches.length - 1, prev + 1));
800
+ } else if (key.return) {
801
+ if (filteredBranches.length > 0) {
802
+ onSelect(filteredBranches[cursorIndex]);
803
+ }
804
+ }
805
+ });
602
806
  if (loading) {
603
807
  return /* @__PURE__ */ jsx5(Spinner3, { label: `\u83B7\u53D6 ${remote2} \u7684\u5206\u652F\u5217\u8868...` });
604
808
  }
605
809
  if (error2) {
606
810
  return /* @__PURE__ */ jsxs5(Text5, { color: "red", children: [
607
- "\u2716 \u83B7\u53D6\u5206\u652F\u5217\u8868\u5931\u8D25: ",
811
+ "\u2716 ",
812
+ "\u83B7\u53D6\u5206\u652F\u5217\u8868\u5931\u8D25: ",
608
813
  error2
609
814
  ] });
610
815
  }
611
816
  if (!branches || branches.length === 0) {
612
- return /* @__PURE__ */ jsx5(Text5, { color: "red", children: "\u2716 \u672A\u627E\u5230\u8FDC\u7A0B\u5206\u652F" });
817
+ return /* @__PURE__ */ jsxs5(Text5, { color: "red", children: [
818
+ "\u2716 ",
819
+ "\u672A\u627E\u5230\u8FDC\u7A0B\u5206\u652F"
820
+ ] });
613
821
  }
614
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", gap: 1, children: [
615
- /* @__PURE__ */ jsx5(SectionHeader, { title: `\u9009\u62E9\u5206\u652F`, subtitle: `${remote2} \xB7 ${branches.length} \u4E2A\u5206\u652F` }),
616
- /* @__PURE__ */ jsxs5(Box5, { children: [
617
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "/ " }),
822
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
823
+ /* @__PURE__ */ jsx5(SectionHeader, { title: "\u9009\u62E9\u5206\u652F", subtitle: `${remote2} \xB7 ${branches.length} \u4E2A\u5206\u652F` }),
824
+ /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, children: [
825
+ /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: "/ " }),
618
826
  /* @__PURE__ */ jsx5(
619
827
  TextInput2,
620
828
  {
621
829
  placeholder: "\u8F93\u5165\u5173\u952E\u5B57\u8FC7\u6EE4...",
622
- onChange: setFilter
830
+ onChange: (val) => {
831
+ setFilter(val);
832
+ setCursorIndex(0);
833
+ }
623
834
  }
624
835
  ),
625
836
  filter && /* @__PURE__ */ jsxs5(Text5, { color: "gray", dimColor: true, children: [
626
837
  " \xB7 \u5339\u914D ",
627
- filteredOptions.length
838
+ filteredBranches.length
628
839
  ] })
629
840
  ] }),
630
- filteredOptions.length > 0 ? /* @__PURE__ */ jsx5(Select2, { options: filteredOptions, onChange: onSelect }) : /* @__PURE__ */ jsx5(Text5, { color: "yellow", children: "\u25B2 \u65E0\u5339\u914D\u5206\u652F" })
841
+ filteredBranches.length > 0 ? /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginTop: 1, children: [
842
+ startIdx > 0 && /* @__PURE__ */ jsxs5(Text5, { color: "gray", dimColor: true, children: [
843
+ " ",
844
+ "\u2191 ",
845
+ startIdx,
846
+ " more"
847
+ ] }),
848
+ visibleBranches.map((b, i) => {
849
+ const actualIdx = startIdx + i;
850
+ const isCursor = actualIdx === cursorIndex;
851
+ return /* @__PURE__ */ jsxs5(Box5, { children: [
852
+ /* @__PURE__ */ jsx5(Text5, { color: isCursor ? "cyan" : "gray", children: isCursor ? "\u203A" : " " }),
853
+ /* @__PURE__ */ jsx5(Text5, { children: " " }),
854
+ /* @__PURE__ */ jsx5(Text5, { color: isCursor ? "cyan" : "white", bold: isCursor, children: b })
855
+ ] }, b);
856
+ }),
857
+ startIdx + visibleCount < filteredBranches.length && /* @__PURE__ */ jsxs5(Text5, { color: "gray", dimColor: true, children: [
858
+ " ",
859
+ "\u2193 ",
860
+ filteredBranches.length - startIdx - visibleCount,
861
+ " more"
862
+ ] })
863
+ ] }) : /* @__PURE__ */ jsxs5(Text5, { color: "yellow", children: [
864
+ "\u25B2 ",
865
+ "\u65E0\u5339\u914D\u5206\u652F"
866
+ ] })
631
867
  ] });
632
868
  }
633
869
 
634
870
  // src/components/commit-list.tsx
635
- import { useState as useState5, useMemo as useMemo2, useRef } from "react";
871
+ import { useState as useState5, useMemo as useMemo2, useRef as useRef2, useEffect as useEffect3 } from "react";
636
872
  import { Box as Box6, Text as Text6, useInput as useInput5 } from "ink";
637
873
  import { Spinner as Spinner4 } from "@inkjs/ui";
638
874
  import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
875
+ function StatPanel({ stat, loading, count: count2 }) {
876
+ const STAT_HEIGHT = 8;
877
+ const [scrollOffset, setScrollOffset] = useState5(0);
878
+ const lines = useMemo2(() => stat ? stat.split("\n") : [], [stat]);
879
+ const canScroll = lines.length > STAT_HEIGHT;
880
+ useEffect3(() => {
881
+ setScrollOffset(0);
882
+ }, [stat]);
883
+ useInput5((input) => {
884
+ if (!canScroll) return;
885
+ if (input === "j") {
886
+ setScrollOffset((prev) => Math.min(prev + 1, lines.length - STAT_HEIGHT));
887
+ } else if (input === "k") {
888
+ setScrollOffset((prev) => Math.max(0, prev - 1));
889
+ }
890
+ });
891
+ const visibleLines = canScroll ? lines.slice(scrollOffset, scrollOffset + STAT_HEIGHT) : lines;
892
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
893
+ /* @__PURE__ */ jsxs6(Box6, { justifyContent: "space-between", children: [
894
+ /* @__PURE__ */ jsxs6(Text6, { bold: true, color: "cyan", children: [
895
+ "\u25C6 ",
896
+ "\u5DF2\u9009 ",
897
+ count2,
898
+ " \u4E2A commit \xB7 diff --stat"
899
+ ] }),
900
+ canScroll && /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
901
+ scrollOffset + 1,
902
+ "-",
903
+ Math.min(scrollOffset + STAT_HEIGHT, lines.length),
904
+ "/",
905
+ lines.length,
906
+ " [j/k]"
907
+ ] })
908
+ ] }),
909
+ /* @__PURE__ */ jsx6(Box6, { flexDirection: "column", height: STAT_HEIGHT, children: loading ? /* @__PURE__ */ jsx6(Spinner4, { label: "\u52A0\u8F7D\u4E2D..." }) : visibleLines.length > 0 ? visibleLines.map((line, i) => /* @__PURE__ */ jsx6(Text6, { color: "gray", children: line }, scrollOffset + i)) : /* @__PURE__ */ jsx6(Text6, { color: "gray", dimColor: true, children: "(\u65E0\u53D8\u66F4)" }) })
910
+ ] });
911
+ }
639
912
  function CommitList({ remote: remote2, branch: branch2, onSelect, onBack }) {
640
- const { data: commits2, loading, error: error2 } = useCommits(remote2, branch2, 30);
913
+ const { data: commits2, loading, loadingMore, error: error2, hasMore, loadMore } = useCommits(remote2, branch2, 100);
641
914
  const [selectedIndex, setSelectedIndex] = useState5(0);
642
915
  const [selectedHashes, setSelectedHashes] = useState5(/* @__PURE__ */ new Set());
643
916
  const [shiftMode, setShiftMode] = useState5(false);
644
- const anchorIndexRef = useRef(null);
917
+ const anchorIndexRef = useRef2(null);
645
918
  const selectedKey = useMemo2(() => Array.from(selectedHashes).sort().join(","), [selectedHashes]);
646
919
  const selectedArray = useMemo2(() => Array.from(selectedHashes), [selectedKey]);
647
920
  const { stat, loading: statLoading } = useCommitStat(selectedArray);
921
+ const syncedCount = useMemo2(() => {
922
+ if (!commits2) return 0;
923
+ return commits2.filter((c) => c.synced).length;
924
+ }, [commits2]);
925
+ useEffect3(() => {
926
+ if (!commits2 || !hasMore || loadingMore) return;
927
+ if (selectedIndex >= commits2.length - 5) {
928
+ loadMore();
929
+ }
930
+ }, [selectedIndex, commits2?.length, hasMore, loadingMore, loadMore]);
648
931
  const toggleCurrent = () => {
649
932
  if (!commits2 || commits2.length === 0) return;
650
- const hash = commits2[selectedIndex].hash;
933
+ const commit = commits2[selectedIndex];
934
+ if (commit.synced) return;
935
+ const hash = commit.hash;
651
936
  setSelectedHashes((prev) => {
652
937
  const next = new Set(prev);
653
938
  if (next.has(hash)) {
@@ -667,19 +952,22 @@ function CommitList({ remote: remote2, branch: branch2, onSelect, onBack }) {
667
952
  setSelectedHashes((prev) => {
668
953
  const next = new Set(prev);
669
954
  for (let i = start; i <= end; i++) {
670
- next.add(commits2[i].hash);
955
+ if (!commits2[i].synced) {
956
+ next.add(commits2[i].hash);
957
+ }
671
958
  }
672
959
  return next;
673
960
  });
674
961
  };
675
962
  const toggleAll = () => {
676
963
  if (!commits2 || commits2.length === 0) return;
964
+ const unsyncedCommits = commits2.filter((c) => !c.synced);
677
965
  setSelectedHashes((prev) => {
678
- if (prev.size === commits2.length) {
966
+ if (prev.size === unsyncedCommits.length) {
679
967
  anchorIndexRef.current = null;
680
968
  return /* @__PURE__ */ new Set();
681
969
  }
682
- return new Set(commits2.map((c) => c.hash));
970
+ return new Set(unsyncedCommits.map((c) => c.hash));
683
971
  });
684
972
  };
685
973
  const invertSelection = () => {
@@ -687,7 +975,7 @@ function CommitList({ remote: remote2, branch: branch2, onSelect, onBack }) {
687
975
  setSelectedHashes((prev) => {
688
976
  const next = /* @__PURE__ */ new Set();
689
977
  for (const c of commits2) {
690
- if (!prev.has(c.hash)) next.add(c.hash);
978
+ if (!c.synced && !prev.has(c.hash)) next.add(c.hash);
691
979
  }
692
980
  return next;
693
981
  });
@@ -697,7 +985,9 @@ function CommitList({ remote: remote2, branch: branch2, onSelect, onBack }) {
697
985
  setSelectedHashes((prev) => {
698
986
  const next = new Set(prev);
699
987
  for (let i = 0; i <= selectedIndex; i++) {
700
- next.add(commits2[i].hash);
988
+ if (!commits2[i].synced) {
989
+ next.add(commits2[i].hash);
990
+ }
701
991
  }
702
992
  return next;
703
993
  });
@@ -754,16 +1044,21 @@ function CommitList({ remote: remote2, branch: branch2, onSelect, onBack }) {
754
1044
  }
755
1045
  if (error2) {
756
1046
  return /* @__PURE__ */ jsxs6(Text6, { color: "red", children: [
757
- "\u2716 \u83B7\u53D6 commit \u5217\u8868\u5931\u8D25: ",
1047
+ "\u2716 ",
1048
+ "\u83B7\u53D6 commit \u5217\u8868\u5931\u8D25: ",
758
1049
  error2
759
1050
  ] });
760
1051
  }
761
1052
  if (!commits2 || commits2.length === 0) {
762
- return /* @__PURE__ */ jsx6(Text6, { color: "yellow", children: "\u25B2 \u8BE5\u5206\u652F\u6CA1\u6709 commit" });
1053
+ return /* @__PURE__ */ jsxs6(Text6, { color: "yellow", children: [
1054
+ "\u25B2 ",
1055
+ "\u8BE5\u5206\u652F\u6CA1\u6709 commit"
1056
+ ] });
763
1057
  }
764
1058
  const visibleCount = 10;
765
1059
  const startIdx = Math.max(0, Math.min(selectedIndex - Math.floor(visibleCount / 2), commits2.length - visibleCount));
766
1060
  const visibleCommits = commits2.slice(startIdx, startIdx + visibleCount);
1061
+ const unsyncedTotal = commits2.length - syncedCount;
767
1062
  return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", gap: 1, children: [
768
1063
  /* @__PURE__ */ jsx6(SectionHeader, { title: "\u9009\u62E9\u8981\u540C\u6B65\u7684 commit" }),
769
1064
  /* @__PURE__ */ jsxs6(Box6, { gap: 2, children: [
@@ -774,17 +1069,28 @@ function CommitList({ remote: remote2, branch: branch2, onSelect, onBack }) {
774
1069
  ] }),
775
1070
  /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
776
1071
  commits2.length,
777
- " commits"
1072
+ " commits",
1073
+ hasMore ? "+" : ""
1074
+ ] }),
1075
+ syncedCount > 0 && /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
1076
+ syncedCount,
1077
+ " \u5DF2\u540C\u6B65"
1078
+ ] }),
1079
+ /* @__PURE__ */ jsxs6(Text6, { color: "green", children: [
1080
+ unsyncedTotal,
1081
+ " \u5F85\u540C\u6B65"
778
1082
  ] }),
779
1083
  /* @__PURE__ */ jsxs6(Text6, { color: selectedHashes.size > 0 ? "cyan" : "gray", bold: selectedHashes.size > 0, children: [
780
1084
  "\u5DF2\u9009 ",
781
1085
  selectedHashes.size
782
1086
  ] }),
783
- shiftMode && /* @__PURE__ */ jsx6(Text6, { color: "yellow", bold: true, children: "SHIFT" })
1087
+ shiftMode && /* @__PURE__ */ jsx6(Text6, { color: "yellow", bold: true, children: "SHIFT" }),
1088
+ loadingMore && /* @__PURE__ */ jsx6(Text6, { color: "gray", dimColor: true, children: "\u52A0\u8F7D\u4E2D..." })
784
1089
  ] }),
785
1090
  /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
786
1091
  startIdx > 0 && /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
787
- " \u2191 ",
1092
+ " ",
1093
+ "\u2191 ",
788
1094
  startIdx,
789
1095
  " more"
790
1096
  ] }),
@@ -793,30 +1099,60 @@ function CommitList({ remote: remote2, branch: branch2, onSelect, onBack }) {
793
1099
  const isSelected = selectedHashes.has(c.hash);
794
1100
  const isCursor = actualIdx === selectedIndex;
795
1101
  const isAnchor = actualIdx === anchorIndexRef.current;
1102
+ const isSynced = !!c.synced;
796
1103
  return /* @__PURE__ */ jsxs6(Box6, { children: [
797
- /* @__PURE__ */ jsxs6(Text6, { backgroundColor: isCursor ? "blue" : void 0, color: isSelected ? "green" : "white", children: [
798
- isCursor ? "\u25B8 " : " ",
799
- isAnchor ? "\u2693" : isSelected ? "\u25CF" : "\u25CB",
800
- " "
801
- ] }),
802
- /* @__PURE__ */ jsx6(Text6, { backgroundColor: isCursor ? "blue" : void 0, color: "yellow", children: c.shortHash }),
803
- /* @__PURE__ */ jsxs6(Text6, { backgroundColor: isCursor ? "blue" : void 0, color: isSelected ? "green" : "white", children: [
804
- " ",
805
- c.message
806
- ] }),
1104
+ /* @__PURE__ */ jsxs6(
1105
+ Text6,
1106
+ {
1107
+ backgroundColor: isCursor ? isSynced ? "gray" : "blue" : void 0,
1108
+ color: isSynced ? "gray" : isSelected ? "green" : "white",
1109
+ dimColor: isSynced,
1110
+ children: [
1111
+ isCursor ? "\u25B8 " : " ",
1112
+ isSynced ? "\u2713" : isAnchor ? "\u2693" : isSelected ? "\u25CF" : "\u25CB",
1113
+ " "
1114
+ ]
1115
+ }
1116
+ ),
1117
+ /* @__PURE__ */ jsx6(
1118
+ Text6,
1119
+ {
1120
+ backgroundColor: isCursor ? isSynced ? "gray" : "blue" : void 0,
1121
+ color: isSynced ? "gray" : "yellow",
1122
+ dimColor: isSynced,
1123
+ children: c.shortHash
1124
+ }
1125
+ ),
1126
+ /* @__PURE__ */ jsxs6(
1127
+ Text6,
1128
+ {
1129
+ backgroundColor: isCursor ? isSynced ? "gray" : "blue" : void 0,
1130
+ color: isSynced ? "gray" : isSelected ? "green" : "white",
1131
+ dimColor: isSynced,
1132
+ children: [
1133
+ " ",
1134
+ c.message
1135
+ ]
1136
+ }
1137
+ ),
807
1138
  /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
808
1139
  " ",
809
1140
  c.author,
810
1141
  " \xB7 ",
811
1142
  c.date
812
- ] })
1143
+ ] }),
1144
+ isSynced && /* @__PURE__ */ jsx6(Text6, { color: "gray", dimColor: true, children: " [\u5DF2\u540C\u6B65]" })
813
1145
  ] }, c.hash);
814
1146
  }),
815
- startIdx + visibleCount < commits2.length && /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
816
- " \u2193 ",
1147
+ startIdx + visibleCount < commits2.length ? /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
1148
+ " ",
1149
+ "\u2193 ",
817
1150
  commits2.length - startIdx - visibleCount,
818
1151
  " more"
819
- ] })
1152
+ ] }) : hasMore ? /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
1153
+ " ",
1154
+ "\u2193 \u6EDA\u52A8\u52A0\u8F7D\u66F4\u591A..."
1155
+ ] }) : null
820
1156
  ] }),
821
1157
  /* @__PURE__ */ jsx6(KeyHints, { hints: [
822
1158
  { key: "\u2191\u2193", label: "\u5BFC\u822A" },
@@ -828,15 +1164,111 @@ function CommitList({ remote: remote2, branch: branch2, onSelect, onBack }) {
828
1164
  { key: "Enter", label: "\u786E\u8BA4" },
829
1165
  { key: "Esc", label: "\u8FD4\u56DE" }
830
1166
  ] }),
831
- selectedHashes.size > 0 && /* @__PURE__ */ jsx6(StatusPanel, { type: "info", title: `\u5DF2\u9009 ${selectedHashes.size} \u4E2A commit \xB7 diff --stat`, children: statLoading ? /* @__PURE__ */ jsx6(Spinner4, { label: "\u52A0\u8F7D\u4E2D..." }) : /* @__PURE__ */ jsx6(Text6, { color: "gray", children: stat || "(\u65E0\u53D8\u66F4)" }) })
1167
+ selectedHashes.size > 0 && /* @__PURE__ */ jsx6(StatPanel, { stat, loading: statLoading, count: selectedHashes.size })
832
1168
  ] });
833
1169
  }
834
1170
 
835
- // src/components/confirm-panel.tsx
1171
+ // src/components/branch-check.tsx
1172
+ import { useState as useState6, useEffect as useEffect4, useRef as useRef3 } from "react";
836
1173
  import { Box as Box7, Text as Text7, useInput as useInput6 } from "ink";
1174
+ import { Spinner as Spinner5 } from "@inkjs/ui";
837
1175
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
838
- function ConfirmPanel({ commits: commits2, selectedHashes, hasMerge, useMainline, onToggleMainline, onConfirm, onCancel }) {
1176
+ var BASE_BRANCHES = ["main", "master"];
1177
+ function BranchCheck({ targetBranch, onContinue, onBack }) {
1178
+ const [currentBranch, setCurrentBranch] = useState6(null);
1179
+ const [creating, setCreating] = useState6(false);
1180
+ const [error2, setError] = useState6(null);
1181
+ const [matched, setMatched] = useState6(false);
1182
+ const autoCreated = useRef3(false);
1183
+ useEffect4(() => {
1184
+ getCurrentBranch().then((branch2) => {
1185
+ setCurrentBranch(branch2);
1186
+ if (branch2 === targetBranch) {
1187
+ setMatched(true);
1188
+ }
1189
+ });
1190
+ }, [targetBranch]);
1191
+ useEffect4(() => {
1192
+ if (matched) onContinue();
1193
+ }, [matched]);
1194
+ useEffect4(() => {
1195
+ if (currentBranch === null || matched || autoCreated.current) return;
1196
+ if (!BASE_BRANCHES.includes(currentBranch)) return;
1197
+ autoCreated.current = true;
1198
+ setCreating(true);
1199
+ createBranchFrom(targetBranch, currentBranch).then(() => {
1200
+ onContinue();
1201
+ }).catch((err) => {
1202
+ setCreating(false);
1203
+ setError(err.message);
1204
+ });
1205
+ }, [currentBranch, matched, targetBranch]);
1206
+ const doCreate = () => {
1207
+ if (!currentBranch) return;
1208
+ setCreating(true);
1209
+ setError(null);
1210
+ createBranchFrom(targetBranch, currentBranch).then(() => {
1211
+ onContinue();
1212
+ }).catch((err) => {
1213
+ setCreating(false);
1214
+ setError(err.message);
1215
+ });
1216
+ };
839
1217
  useInput6((input, key) => {
1218
+ if (creating || currentBranch === null || matched) return;
1219
+ if (BASE_BRANCHES.includes(currentBranch)) return;
1220
+ if (input === "y" || input === "Y") {
1221
+ doCreate();
1222
+ } else if (input === "n" || input === "N") {
1223
+ onContinue();
1224
+ } else if (key.escape) {
1225
+ onBack?.();
1226
+ }
1227
+ });
1228
+ if (currentBranch === null || matched) {
1229
+ return /* @__PURE__ */ jsx7(Spinner5, { label: "\u68C0\u67E5\u5F53\u524D\u5206\u652F..." });
1230
+ }
1231
+ if (creating) {
1232
+ return /* @__PURE__ */ jsx7(Spinner5, { label: `\u6B63\u5728\u4ECE ${currentBranch} \u521B\u5EFA\u5206\u652F ${targetBranch}...` });
1233
+ }
1234
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", gap: 1, children: [
1235
+ /* @__PURE__ */ jsx7(SectionHeader, { title: "\u5206\u652F\u68C0\u67E5" }),
1236
+ /* @__PURE__ */ jsxs7(StatusPanel, { type: "warn", title: "\u5F53\u524D\u5206\u652F\u4E0E\u76EE\u6807\u5206\u652F\u4E0D\u4E00\u81F4", children: [
1237
+ /* @__PURE__ */ jsxs7(Box7, { children: [
1238
+ /* @__PURE__ */ jsx7(Text7, { color: "gray", children: " \u5F53\u524D\u5206\u652F: " }),
1239
+ /* @__PURE__ */ jsx7(Text7, { color: "yellow", bold: true, children: currentBranch })
1240
+ ] }),
1241
+ /* @__PURE__ */ jsxs7(Box7, { children: [
1242
+ /* @__PURE__ */ jsx7(Text7, { color: "gray", children: " \u76EE\u6807\u5206\u652F: " }),
1243
+ /* @__PURE__ */ jsx7(Text7, { color: "cyan", bold: true, children: targetBranch })
1244
+ ] })
1245
+ ] }),
1246
+ error2 && /* @__PURE__ */ jsxs7(Text7, { color: "red", children: [
1247
+ "\u2716 ",
1248
+ error2
1249
+ ] }),
1250
+ /* @__PURE__ */ jsxs7(Box7, { children: [
1251
+ /* @__PURE__ */ jsxs7(Text7, { bold: true, children: [
1252
+ "\u4ECE ",
1253
+ currentBranch,
1254
+ " \u521B\u5EFA ",
1255
+ targetBranch,
1256
+ " \u5206\u652F? "
1257
+ ] }),
1258
+ /* @__PURE__ */ jsx7(InlineKeys, { hints: [
1259
+ { key: "y", label: "\u521B\u5EFA\u5E76\u5207\u6362" },
1260
+ { key: "n", label: "\u8DF3\u8FC7" },
1261
+ { key: "Esc", label: "\u8FD4\u56DE" }
1262
+ ] })
1263
+ ] })
1264
+ ] });
1265
+ }
1266
+
1267
+ // src/components/confirm-panel.tsx
1268
+ import { Box as Box8, Text as Text8, useInput as useInput7 } from "ink";
1269
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1270
+ function ConfirmPanel({ commits: commits2, selectedHashes, hasMerge, useMainline, noCommit, onToggleMainline, onToggleNoCommit, onConfirm, onCancel }) {
1271
+ useInput7((input, key) => {
840
1272
  if (key.escape) {
841
1273
  onCancel();
842
1274
  } else if (input === "y" || input === "Y") {
@@ -845,43 +1277,54 @@ function ConfirmPanel({ commits: commits2, selectedHashes, hasMerge, useMainline
845
1277
  onCancel();
846
1278
  } else if (hasMerge && (input === "m" || input === "M")) {
847
1279
  onToggleMainline();
1280
+ } else if (input === "c" || input === "C") {
1281
+ onToggleNoCommit();
848
1282
  }
849
1283
  });
850
1284
  const selectedCommits = selectedHashes.map((hash) => commits2.find((c) => c.hash === hash)).filter(Boolean);
851
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", gap: 1, children: [
852
- /* @__PURE__ */ jsx7(SectionHeader, { title: "\u786E\u8BA4\u6267\u884C" }),
853
- /* @__PURE__ */ jsx7(StatusPanel, { type: "info", title: `cherry-pick --no-commit \xB7 ${selectedCommits.length} \u4E2A commit`, children: selectedCommits.map((c) => /* @__PURE__ */ jsxs7(Box7, { children: [
854
- /* @__PURE__ */ jsxs7(Text7, { color: "yellow", children: [
1285
+ const modeLabel = noCommit ? "--no-commit" : "\u9010\u4E2A\u63D0\u4EA4";
1286
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", gap: 1, children: [
1287
+ /* @__PURE__ */ jsx8(SectionHeader, { title: "\u786E\u8BA4\u6267\u884C" }),
1288
+ /* @__PURE__ */ jsx8(StatusPanel, { type: "info", title: `cherry-pick \xB7 ${modeLabel} \xB7 ${selectedCommits.length} \u4E2A commit`, children: selectedCommits.map((c) => /* @__PURE__ */ jsxs8(Box8, { children: [
1289
+ /* @__PURE__ */ jsxs8(Text8, { color: "yellow", children: [
855
1290
  " ",
856
1291
  c.shortHash
857
1292
  ] }),
858
- /* @__PURE__ */ jsxs7(Text7, { children: [
1293
+ /* @__PURE__ */ jsxs8(Text8, { children: [
859
1294
  " ",
860
1295
  c.message
861
1296
  ] }),
862
- /* @__PURE__ */ jsxs7(Text7, { color: "gray", dimColor: true, children: [
1297
+ /* @__PURE__ */ jsxs8(Text8, { color: "gray", dimColor: true, children: [
863
1298
  " ",
864
1299
  c.author
865
1300
  ] })
866
1301
  ] }, c.hash)) }),
867
- hasMerge && /* @__PURE__ */ jsxs7(StatusPanel, { type: "warn", title: "\u68C0\u6D4B\u5230 Merge Commit", children: [
868
- /* @__PURE__ */ jsx7(Text7, { color: "gray", children: " Cherry-pick \u5408\u5E76\u63D0\u4EA4\u9700\u8981\u6307\u5B9A\u7236\u8282\u70B9 (-m 1)" }),
869
- /* @__PURE__ */ jsxs7(Box7, { children: [
870
- /* @__PURE__ */ jsx7(Text7, { children: " " }),
871
- /* @__PURE__ */ jsx7(Text7, { color: "cyan", children: "[m]" }),
872
- /* @__PURE__ */ jsx7(Text7, { children: " \u5207\u6362 -m 1: " }),
873
- useMainline ? /* @__PURE__ */ jsx7(Text7, { color: "green", bold: true, children: "\u5DF2\u542F\u7528" }) : /* @__PURE__ */ jsx7(Text7, { color: "gray", children: "\u672A\u542F\u7528" })
1302
+ /* @__PURE__ */ jsxs8(Box8, { children: [
1303
+ /* @__PURE__ */ jsx8(Text8, { color: "cyan", children: "[c]" }),
1304
+ /* @__PURE__ */ jsx8(Text8, { children: " \u63D0\u4EA4\u6A21\u5F0F: " }),
1305
+ noCommit ? /* @__PURE__ */ jsxs8(Box8, { children: [
1306
+ /* @__PURE__ */ jsx8(Text8, { color: "yellow", bold: true, children: "--no-commit" }),
1307
+ /* @__PURE__ */ jsx8(Text8, { color: "gray", dimColor: true, children: " (\u6539\u52A8\u6682\u5B58\u5230\u5DE5\u4F5C\u533A\uFF0C\u9700\u624B\u52A8 commit)" })
1308
+ ] }) : /* @__PURE__ */ jsxs8(Box8, { children: [
1309
+ /* @__PURE__ */ jsx8(Text8, { color: "green", bold: true, children: "\u9010\u4E2A\u63D0\u4EA4" }),
1310
+ /* @__PURE__ */ jsx8(Text8, { color: "gray", dimColor: true, children: " (\u4FDD\u7559\u539F\u59CB commit \u4FE1\u606F)" })
874
1311
  ] })
875
1312
  ] }),
876
- /* @__PURE__ */ jsxs7(Box7, { children: [
877
- /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: "\u25B2 " }),
878
- /* @__PURE__ */ jsx7(Text7, { color: "gray", children: "--no-commit \u6A21\u5F0F\uFF0C\u6539\u52A8\u5C06\u6682\u5B58\u5230\u5DE5\u4F5C\u533A\uFF0C\u9700\u624B\u52A8 commit" })
1313
+ hasMerge && /* @__PURE__ */ jsxs8(StatusPanel, { type: "warn", title: "\u68C0\u6D4B\u5230 Merge Commit", children: [
1314
+ /* @__PURE__ */ jsx8(Text8, { color: "gray", children: " Cherry-pick \u5408\u5E76\u63D0\u4EA4\u9700\u8981\u6307\u5B9A\u7236\u8282\u70B9 (-m 1)" }),
1315
+ /* @__PURE__ */ jsxs8(Box8, { children: [
1316
+ /* @__PURE__ */ jsx8(Text8, { children: " " }),
1317
+ /* @__PURE__ */ jsx8(Text8, { color: "cyan", children: "[m]" }),
1318
+ /* @__PURE__ */ jsx8(Text8, { children: " \u5207\u6362 -m 1: " }),
1319
+ useMainline ? /* @__PURE__ */ jsx8(Text8, { color: "green", bold: true, children: "\u5DF2\u542F\u7528" }) : /* @__PURE__ */ jsx8(Text8, { color: "gray", children: "\u672A\u542F\u7528" })
1320
+ ] })
879
1321
  ] }),
880
- /* @__PURE__ */ jsxs7(Box7, { children: [
881
- /* @__PURE__ */ jsx7(Text7, { bold: true, children: "\u786E\u8BA4\u6267\u884C? " }),
882
- /* @__PURE__ */ jsx7(InlineKeys, { hints: [
1322
+ /* @__PURE__ */ jsxs8(Box8, { children: [
1323
+ /* @__PURE__ */ jsx8(Text8, { bold: true, children: "\u786E\u8BA4\u6267\u884C? " }),
1324
+ /* @__PURE__ */ jsx8(InlineKeys, { hints: [
883
1325
  { key: "y", label: "\u786E\u8BA4" },
884
1326
  { key: "n", label: "\u53D6\u6D88" },
1327
+ { key: "c", label: "\u5207\u6362\u63D0\u4EA4\u6A21\u5F0F" },
885
1328
  ...hasMerge ? [{ key: "m", label: "\u5207\u6362 -m 1" }] : [],
886
1329
  { key: "Esc", label: "\u8FD4\u56DE" }
887
1330
  ] })
@@ -890,92 +1333,295 @@ function ConfirmPanel({ commits: commits2, selectedHashes, hasMerge, useMainline
890
1333
  }
891
1334
 
892
1335
  // src/components/result-panel.tsx
893
- import { useState as useState6, useEffect as useEffect3 } from "react";
894
- import { Box as Box8, Text as Text8 } from "ink";
895
- import { Spinner as Spinner5 } from "@inkjs/ui";
896
- import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
897
- function ResultPanel({ selectedHashes, useMainline, stashed, onStashRestored, onDone }) {
898
- const [phase, setPhase] = useState6("executing");
899
- const [result, setResult] = useState6(null);
900
- const [stagedStat, setStagedStat] = useState6("");
901
- const [stashRestored, setStashRestored] = useState6(null);
902
- const tryRestoreStash = async () => {
1336
+ import { useState as useState7, useEffect as useEffect5, useCallback as useCallback2, useRef as useRef4 } from "react";
1337
+ import { Box as Box9, Text as Text9, useInput as useInput8 } from "ink";
1338
+ import { Spinner as Spinner6 } from "@inkjs/ui";
1339
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1340
+ function ResultPanel({ selectedHashes, useMainline, noCommit, stashed, onStashRestored, onDone }) {
1341
+ const [phase, setPhase] = useState7("executing");
1342
+ const [conflictFiles, setConflictFiles] = useState7([]);
1343
+ const [currentIndex, setCurrentIndex] = useState7(0);
1344
+ const [stagedStat, setStagedStat] = useState7("");
1345
+ const [stashRestored, setStashRestored] = useState7(null);
1346
+ const [errorMsg, setErrorMsg] = useState7("");
1347
+ const [completedCount, setCompletedCount] = useState7(0);
1348
+ const [skippedCount, setSkippedCount] = useState7(0);
1349
+ const remainingRef = useRef4([]);
1350
+ const backupBranchRef = useRef4("");
1351
+ const orderedHashes = useRef4([...selectedHashes].reverse());
1352
+ const tryRestoreStash = useCallback2(async () => {
903
1353
  if (!stashed) return true;
904
1354
  setPhase("restoring");
905
1355
  const ok = await stashPop();
906
1356
  setStashRestored(ok);
907
1357
  if (ok) onStashRestored();
908
1358
  return ok;
909
- };
910
- useEffect3(() => {
911
- async function run() {
912
- const res = await cherryPick(selectedHashes, useMainline);
913
- setResult(res);
914
- if (res.success) {
915
- const stat = await getStagedStat();
916
- setStagedStat(stat);
917
- await tryRestoreStash();
918
- setPhase("done");
919
- } else {
920
- await tryRestoreStash();
921
- setPhase("error");
1359
+ }, [stashed, onStashRestored]);
1360
+ const finishAll = useCallback2(async () => {
1361
+ if (noCommit) {
1362
+ const stat = await getStagedStat();
1363
+ setStagedStat(stat);
1364
+ }
1365
+ if (backupBranchRef.current) {
1366
+ await deleteBackupBranch(backupBranchRef.current);
1367
+ }
1368
+ await tryRestoreStash();
1369
+ setPhase("done");
1370
+ }, [noCommit, tryRestoreStash]);
1371
+ const executeFrom = useCallback2(async (startIndex) => {
1372
+ const hashes = orderedHashes.current;
1373
+ for (let i = startIndex; i < hashes.length; i++) {
1374
+ setCurrentIndex(i);
1375
+ const res = await cherryPickOne(hashes[i], useMainline, noCommit);
1376
+ if (!res.success) {
1377
+ setConflictFiles(res.conflictFiles || []);
1378
+ remainingRef.current = hashes.slice(i + 1);
1379
+ setPhase("conflict");
1380
+ return;
922
1381
  }
1382
+ setCompletedCount((c) => c + 1);
923
1383
  }
924
- run();
1384
+ await finishAll();
1385
+ }, [useMainline, noCommit, finishAll]);
1386
+ useEffect5(() => {
1387
+ createBackupBranch().then((branch2) => {
1388
+ backupBranchRef.current = branch2;
1389
+ executeFrom(0);
1390
+ });
925
1391
  }, []);
1392
+ const continueRemaining = useCallback2(async () => {
1393
+ const remaining = remainingRef.current;
1394
+ if (remaining.length === 0) {
1395
+ await finishAll();
1396
+ return;
1397
+ }
1398
+ setPhase("executing");
1399
+ for (let i = 0; i < remaining.length; i++) {
1400
+ setCurrentIndex(orderedHashes.current.length - remaining.length + i);
1401
+ const res = await cherryPickOne(remaining[i], useMainline, noCommit);
1402
+ if (!res.success) {
1403
+ setConflictFiles(res.conflictFiles || []);
1404
+ remainingRef.current = remaining.slice(i + 1);
1405
+ setPhase("conflict");
1406
+ return;
1407
+ }
1408
+ setCompletedCount((c) => c + 1);
1409
+ }
1410
+ await finishAll();
1411
+ }, [useMainline, noCommit, finishAll]);
1412
+ const handleContinue = useCallback2(async () => {
1413
+ const conflicts = await getConflictFiles();
1414
+ if (conflicts.length > 0) {
1415
+ setConflictFiles(conflicts);
1416
+ setErrorMsg("\u4ECD\u6709\u672A\u89E3\u51B3\u7684\u51B2\u7A81\u6587\u4EF6\uFF0C\u8BF7\u5148\u89E3\u51B3\u5E76 git add");
1417
+ return;
1418
+ }
1419
+ setPhase("continuing");
1420
+ setErrorMsg("");
1421
+ const inProgress = await isCherryPickInProgress();
1422
+ if (noCommit) {
1423
+ if (inProgress) await clearCherryPickState();
1424
+ setCompletedCount((c) => c + 1);
1425
+ await continueRemaining();
1426
+ } else if (!inProgress) {
1427
+ setCompletedCount((c) => c + 1);
1428
+ await continueRemaining();
1429
+ } else {
1430
+ const contResult = await continueCherryPick();
1431
+ if (contResult.empty) {
1432
+ setPhase("empty");
1433
+ return;
1434
+ }
1435
+ if (!contResult.success) {
1436
+ setConflictFiles(contResult.conflictFiles || []);
1437
+ setErrorMsg("cherry-pick --continue \u5931\u8D25: " + (contResult.error || "").substring(0, 100));
1438
+ setPhase("conflict");
1439
+ return;
1440
+ }
1441
+ setCompletedCount((c) => c + 1);
1442
+ await continueRemaining();
1443
+ }
1444
+ }, [noCommit, continueRemaining]);
1445
+ const handleSkip = useCallback2(async () => {
1446
+ setPhase("continuing");
1447
+ setErrorMsg("");
1448
+ await skipCherryPick();
1449
+ setSkippedCount((c) => c + 1);
1450
+ await continueRemaining();
1451
+ }, [continueRemaining]);
1452
+ const handleAbort = useCallback2(async () => {
1453
+ setPhase("aborting");
1454
+ await abortCherryPick();
1455
+ if (backupBranchRef.current) {
1456
+ const result = await restoreFromBackup(backupBranchRef.current);
1457
+ if (!result.success) {
1458
+ setErrorMsg(result.error || "");
1459
+ }
1460
+ backupBranchRef.current = "";
1461
+ }
1462
+ await tryRestoreStash();
1463
+ setPhase("aborted");
1464
+ }, [tryRestoreStash]);
1465
+ useInput8((input, key) => {
1466
+ if (phase === "conflict") {
1467
+ if (input === "c" || input === "C") {
1468
+ handleContinue();
1469
+ } else if (input === "a" || input === "A") {
1470
+ handleAbort();
1471
+ } else if (input === "q" || input === "Q") {
1472
+ onDone();
1473
+ }
1474
+ } else if (phase === "empty") {
1475
+ if (input === "s" || input === "S") {
1476
+ handleSkip();
1477
+ } else if (input === "a" || input === "A") {
1478
+ handleAbort();
1479
+ } else if (input === "q" || input === "Q") {
1480
+ onDone();
1481
+ }
1482
+ }
1483
+ });
926
1484
  if (phase === "executing") {
927
- return /* @__PURE__ */ jsx8(Spinner5, { label: `cherry-pick --no-commit (${selectedHashes.length} \u4E2A commit)...` });
1485
+ const mode = noCommit ? "--no-commit" : "";
1486
+ return /* @__PURE__ */ jsx9(Spinner6, { label: `cherry-pick ${mode} (${currentIndex + 1}/${orderedHashes.current.length})...` });
1487
+ }
1488
+ if (phase === "continuing") {
1489
+ return /* @__PURE__ */ jsx9(Spinner6, { label: "\u7EE7\u7EED cherry-pick..." });
1490
+ }
1491
+ if (phase === "aborting") {
1492
+ return /* @__PURE__ */ jsx9(Spinner6, { label: "\u6B63\u5728\u653E\u5F03 cherry-pick..." });
928
1493
  }
929
1494
  if (phase === "restoring") {
930
- return /* @__PURE__ */ jsx8(Spinner5, { label: "\u6062\u590D\u5DE5\u4F5C\u533A (git stash pop)..." });
1495
+ return /* @__PURE__ */ jsx9(Spinner6, { label: "\u6062\u590D\u5DE5\u4F5C\u533A (git stash pop)..." });
931
1496
  }
932
- if (phase === "error" && result) {
933
- return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", gap: 1, children: [
934
- /* @__PURE__ */ jsx8(SectionHeader, { title: "Cherry-pick \u9047\u5230\u51B2\u7A81" }),
935
- result.conflictFiles && result.conflictFiles.length > 0 && /* @__PURE__ */ jsx8(StatusPanel, { type: "error", title: "\u51B2\u7A81\u6587\u4EF6", children: result.conflictFiles.map((f) => /* @__PURE__ */ jsxs8(Text8, { color: "red", children: [
1497
+ if (phase === "empty") {
1498
+ const total2 = orderedHashes.current.length;
1499
+ const currentHash = orderedHashes.current[total2 - remainingRef.current.length - 1];
1500
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", gap: 1, children: [
1501
+ /* @__PURE__ */ jsx9(SectionHeader, { title: "\u7A7A\u63D0\u4EA4" }),
1502
+ /* @__PURE__ */ jsx9(StatusPanel, { type: "warn", title: `commit ${currentHash?.substring(0, 7)} \u89E3\u51B3\u51B2\u7A81\u540E\u65E0\u5B9E\u9645\u53D8\u66F4`, children: /* @__PURE__ */ jsx9(Text9, { color: "gray", children: " \u8BE5 commit \u7684\u6240\u6709\u66F4\u6539\u5728\u51B2\u7A81\u89E3\u51B3\u8FC7\u7A0B\u4E2D\u5DF2\u88AB\u4E22\u5F03" }) }),
1503
+ /* @__PURE__ */ jsx9(Box9, { children: /* @__PURE__ */ jsx9(InlineKeys, { hints: [
1504
+ { key: "s", label: "\u8DF3\u8FC7\u6B64 commit (skip)" },
1505
+ { key: "a", label: "\u653E\u5F03\u5168\u90E8 (abort)" },
1506
+ { key: "q", label: "\u9000\u51FA (\u4FDD\u7559\u5F53\u524D\u72B6\u6001)" }
1507
+ ] }) })
1508
+ ] });
1509
+ }
1510
+ if (phase === "conflict") {
1511
+ const total2 = orderedHashes.current.length;
1512
+ const doneCount = total2 - remainingRef.current.length - 1;
1513
+ const currentHash = orderedHashes.current[total2 - remainingRef.current.length - 1];
1514
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", gap: 1, children: [
1515
+ /* @__PURE__ */ jsx9(SectionHeader, { title: "Cherry-pick \u9047\u5230\u51B2\u7A81" }),
1516
+ /* @__PURE__ */ jsxs9(Box9, { gap: 2, children: [
1517
+ /* @__PURE__ */ jsxs9(Text9, { color: "gray", dimColor: true, children: [
1518
+ "\u8FDB\u5EA6: ",
1519
+ doneCount,
1520
+ "/",
1521
+ total2
1522
+ ] }),
1523
+ /* @__PURE__ */ jsxs9(Text9, { color: "yellow", children: [
1524
+ "\u51B2\u7A81 commit: ",
1525
+ currentHash?.substring(0, 7)
1526
+ ] }),
1527
+ remainingRef.current.length > 0 && /* @__PURE__ */ jsxs9(Text9, { color: "gray", dimColor: true, children: [
1528
+ "\u5269\u4F59: ",
1529
+ remainingRef.current.length
1530
+ ] })
1531
+ ] }),
1532
+ conflictFiles.length > 0 && /* @__PURE__ */ jsx9(StatusPanel, { type: "error", title: "\u51B2\u7A81\u6587\u4EF6", children: conflictFiles.map((f) => /* @__PURE__ */ jsxs9(Text9, { color: "red", children: [
936
1533
  " ",
937
1534
  f
938
1535
  ] }, f)) }),
939
- /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
940
- /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: "\u25B8 \u624B\u52A8\u89E3\u51B3\u51B2\u7A81\u540E\u6267\u884C git add \u548C git commit" }),
941
- /* @__PURE__ */ jsx8(Text8, { color: "gray", dimColor: true, children: "\u25B8 \u6216\u6267\u884C git cherry-pick --abort \u653E\u5F03\u64CD\u4F5C" })
1536
+ /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
1537
+ /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "\u25B8 \u8BF7\u5728\u53E6\u4E00\u4E2A\u7EC8\u7AEF\u4E2D\u624B\u52A8\u89E3\u51B3\u51B2\u7A81" }),
1538
+ /* @__PURE__ */ jsxs9(Text9, { color: "gray", dimColor: true, children: [
1539
+ " 1. \u7F16\u8F91\u51B2\u7A81\u6587\u4EF6\uFF0C\u89E3\u51B3\u51B2\u7A81\u6807\u8BB0 ",
1540
+ "<<<<<<< / ======= / >>>>>>>"
1541
+ ] }),
1542
+ /* @__PURE__ */ jsxs9(Text9, { color: "gray", dimColor: true, children: [
1543
+ " 2. \u6267\u884C git add ",
1544
+ "<file>",
1545
+ " \u6807\u8BB0\u5DF2\u89E3\u51B3"
1546
+ ] }),
1547
+ /* @__PURE__ */ jsx9(Text9, { color: "gray", dimColor: true, children: " 3. \u56DE\u5230\u6B64\u5904\u6309 [c] \u7EE7\u7EED" })
942
1548
  ] }),
943
- stashed && stashRestored === false && /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: "\u25B2 stash \u6062\u590D\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8 git stash pop" }),
944
- stashed && stashRestored === true && /* @__PURE__ */ jsx8(Text8, { color: "green", children: "\u2714 \u5DF2\u6062\u590D\u5DE5\u4F5C\u533A\u53D8\u66F4 (stash pop)" })
1549
+ errorMsg && /* @__PURE__ */ jsxs9(Text9, { color: "red", children: [
1550
+ "\u2716 ",
1551
+ errorMsg
1552
+ ] }),
1553
+ /* @__PURE__ */ jsx9(Box9, { children: /* @__PURE__ */ jsx9(InlineKeys, { hints: [
1554
+ { key: "c", label: "\u7EE7\u7EED (\u51B2\u7A81\u5DF2\u89E3\u51B3)" },
1555
+ { key: "a", label: "\u653E\u5F03 (abort)" },
1556
+ { key: "q", label: "\u9000\u51FA (\u4FDD\u7559\u5F53\u524D\u72B6\u6001)" }
1557
+ ] }) })
945
1558
  ] });
946
1559
  }
947
- return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", gap: 1, children: [
948
- /* @__PURE__ */ jsx8(SectionHeader, { title: "\u540C\u6B65\u5B8C\u6210" }),
949
- /* @__PURE__ */ jsx8(StatusPanel, { type: "success", title: "\u6682\u5B58\u533A\u53D8\u66F4 (git diff --cached --stat)", children: /* @__PURE__ */ jsx8(Text8, { color: "gray", children: stagedStat || "(\u65E0\u53D8\u66F4)" }) }),
950
- stashed && (stashRestored ? /* @__PURE__ */ jsx8(Text8, { color: "green", children: "\u2714 \u5DF2\u6062\u590D\u5DE5\u4F5C\u533A\u53D8\u66F4 (stash pop)" }) : /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: "\u25B2 stash pop \u5931\u8D25\uFF0C\u8BF7\u624B\u52A8 git stash pop" })),
951
- /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
952
- /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: "\u25B2 \u6539\u52A8\u5DF2\u6682\u5B58\u5230\u5DE5\u4F5C\u533A (--no-commit \u6A21\u5F0F)" }),
953
- /* @__PURE__ */ jsx8(Text8, { color: "gray", dimColor: true, children: " \u5BA1\u67E5\u540E\u624B\u52A8\u6267\u884C:" }),
954
- /* @__PURE__ */ jsxs8(Text8, { color: "cyan", children: [
1560
+ if (phase === "aborted") {
1561
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", gap: 1, children: [
1562
+ /* @__PURE__ */ jsx9(SectionHeader, { title: "\u5DF2\u653E\u5F03\u64CD\u4F5C" }),
1563
+ errorMsg ? /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", children: /* @__PURE__ */ jsxs9(Text9, { color: "red", children: [
1564
+ "\u2716 ",
1565
+ errorMsg
1566
+ ] }) }) : /* @__PURE__ */ jsxs9(Text9, { color: "green", children: [
1567
+ "\u2714 ",
1568
+ "\u5DF2\u56DE\u9000\u5230 cherry-pick \u524D\u7684\u72B6\u6001"
1569
+ ] }),
1570
+ stashed && stashRestored === false && /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "\u25B2 stash \u6062\u590D\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8 git stash pop" }),
1571
+ stashed && stashRestored === true && /* @__PURE__ */ jsxs9(Text9, { color: "green", children: [
1572
+ "\u2714 ",
1573
+ "\u5DF2\u6062\u590D\u5DE5\u4F5C\u533A\u53D8\u66F4 (stash pop)"
1574
+ ] })
1575
+ ] });
1576
+ }
1577
+ const total = orderedHashes.current.length;
1578
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", gap: 1, children: [
1579
+ /* @__PURE__ */ jsx9(SectionHeader, { title: "\u540C\u6B65\u5B8C\u6210" }),
1580
+ /* @__PURE__ */ jsxs9(Box9, { gap: 2, children: [
1581
+ /* @__PURE__ */ jsxs9(Text9, { color: "green", bold: true, children: [
1582
+ "\u2714 ",
1583
+ completedCount,
1584
+ " \u4E2A commit \u5DF2\u540C\u6B65"
1585
+ ] }),
1586
+ skippedCount > 0 && /* @__PURE__ */ jsxs9(Text9, { color: "yellow", children: [
1587
+ skippedCount,
1588
+ " \u4E2A\u5DF2\u8DF3\u8FC7"
1589
+ ] })
1590
+ ] }),
1591
+ noCommit && /* @__PURE__ */ jsx9(StatusPanel, { type: "success", title: "\u6682\u5B58\u533A\u53D8\u66F4 (git diff --cached --stat)", children: /* @__PURE__ */ jsx9(Text9, { color: "gray", children: stagedStat || "(\u65E0\u53D8\u66F4)" }) }),
1592
+ stashed && (stashRestored ? /* @__PURE__ */ jsxs9(Text9, { color: "green", children: [
1593
+ "\u2714 ",
1594
+ "\u5DF2\u6062\u590D\u5DE5\u4F5C\u533A\u53D8\u66F4 (stash pop)"
1595
+ ] }) : /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "\u25B2 stash pop \u5931\u8D25\uFF0C\u8BF7\u624B\u52A8 git stash pop" })),
1596
+ noCommit ? /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
1597
+ /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "\u25B2 \u6539\u52A8\u5DF2\u6682\u5B58\u5230\u5DE5\u4F5C\u533A (--no-commit \u6A21\u5F0F)" }),
1598
+ /* @__PURE__ */ jsx9(Text9, { color: "gray", dimColor: true, children: " \u5BA1\u67E5\u540E\u624B\u52A8\u6267\u884C:" }),
1599
+ /* @__PURE__ */ jsxs9(Text9, { color: "cyan", children: [
955
1600
  " git diff --cached ",
956
- /* @__PURE__ */ jsx8(Text8, { color: "gray", dimColor: true, children: "# \u67E5\u770B\u8BE6\u7EC6 diff" })
1601
+ /* @__PURE__ */ jsx9(Text9, { color: "gray", dimColor: true, children: "# \u67E5\u770B\u8BE6\u7EC6 diff" })
957
1602
  ] }),
958
- /* @__PURE__ */ jsxs8(Text8, { color: "cyan", children: [
1603
+ /* @__PURE__ */ jsxs9(Text9, { color: "cyan", children: [
959
1604
  ' git commit -m "sync: ..." ',
960
- /* @__PURE__ */ jsx8(Text8, { color: "gray", dimColor: true, children: "# \u63D0\u4EA4" })
1605
+ /* @__PURE__ */ jsx9(Text9, { color: "gray", dimColor: true, children: "# \u63D0\u4EA4" })
961
1606
  ] }),
962
- /* @__PURE__ */ jsxs8(Text8, { color: "cyan", children: [
1607
+ /* @__PURE__ */ jsxs9(Text9, { color: "cyan", children: [
963
1608
  " git reset HEAD ",
964
- /* @__PURE__ */ jsx8(Text8, { color: "gray", dimColor: true, children: "# \u6216\u653E\u5F03" })
1609
+ /* @__PURE__ */ jsx9(Text9, { color: "gray", dimColor: true, children: "# \u6216\u653E\u5F03" })
965
1610
  ] })
966
- ] })
1611
+ ] }) : /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", children: /* @__PURE__ */ jsx9(Text9, { color: "gray", dimColor: true, children: " \u5DF2\u4FDD\u7559\u539F\u59CB commit \u4FE1\u606F\uFF0C\u53EF\u901A\u8FC7 git log \u67E5\u770B" }) })
967
1612
  ] });
968
1613
  }
969
1614
 
970
1615
  // src/app.tsx
971
1616
  import { execSync } from "child_process";
972
- import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1617
+ import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
973
1618
  var STEP_NUMBER = {
974
1619
  checking: 0,
975
1620
  "stash-recovery": 0,
976
1621
  "stash-prompt": 0,
977
1622
  remote: 1,
978
1623
  branch: 2,
1624
+ "branch-check": 2,
979
1625
  commits: 3,
980
1626
  confirm: 4,
981
1627
  result: 5
@@ -983,22 +1629,23 @@ var STEP_NUMBER = {
983
1629
  var STEP_DEBOUNCE = 100;
984
1630
  function App({ initialRemote, initialBranch }) {
985
1631
  const { exit } = useApp();
986
- const entryStep = initialRemote && initialBranch ? "commits" : initialRemote ? "branch" : "remote";
987
- const [step, setStepRaw] = useState7("checking");
988
- const [inputReady, setInputReady] = useState7(true);
989
- const [remote2, setRemote] = useState7(initialRemote || "");
990
- const [branch2, setBranch] = useState7(initialBranch || "");
991
- const [selectedHashes, setSelectedHashes] = useState7([]);
992
- const [commits2, setCommits] = useState7([]);
993
- const [hasMerge, setHasMerge] = useState7(false);
994
- const [useMainline, setUseMainline] = useState7(false);
995
- const [stashed, setStashed] = useState7(false);
996
- const [guardTimestamp, setGuardTimestamp] = useState7();
997
- const stashedRef = useRef2(false);
998
- const stashRestoredRef = useRef2(false);
999
- const mountedRef = useRef2(true);
1000
- const debounceTimer = useRef2(null);
1001
- const setStep = useCallback2((newStep) => {
1632
+ const entryStep = initialRemote && initialBranch ? "branch-check" : initialRemote ? "branch" : "remote";
1633
+ const [step, setStepRaw] = useState8("checking");
1634
+ const [inputReady, setInputReady] = useState8(true);
1635
+ const [remote2, setRemote] = useState8(initialRemote || "");
1636
+ const [branch2, setBranch] = useState8(initialBranch || "");
1637
+ const [selectedHashes, setSelectedHashes] = useState8([]);
1638
+ const [commits2, setCommits] = useState8([]);
1639
+ const [hasMerge, setHasMerge] = useState8(false);
1640
+ const [useMainline, setUseMainline] = useState8(false);
1641
+ const [noCommit, setNoCommit] = useState8(false);
1642
+ const [stashed, setStashed] = useState8(false);
1643
+ const [guardTimestamp, setGuardTimestamp] = useState8();
1644
+ const stashedRef = useRef5(false);
1645
+ const stashRestoredRef = useRef5(false);
1646
+ const mountedRef = useRef5(true);
1647
+ const debounceTimer = useRef5(null);
1648
+ const setStep = useCallback3((newStep) => {
1002
1649
  setInputReady(false);
1003
1650
  setStepRaw(newStep);
1004
1651
  if (debounceTimer.current) clearTimeout(debounceTimer.current);
@@ -1006,7 +1653,7 @@ function App({ initialRemote, initialBranch }) {
1006
1653
  if (mountedRef.current) setInputReady(true);
1007
1654
  }, STEP_DEBOUNCE);
1008
1655
  }, []);
1009
- const restoreStashSync = useCallback2(() => {
1656
+ const restoreStashSync = useCallback3(() => {
1010
1657
  if (stashedRef.current && !stashRestoredRef.current) {
1011
1658
  try {
1012
1659
  execSync("git stash pop", { stdio: "ignore" });
@@ -1020,11 +1667,11 @@ function App({ initialRemote, initialBranch }) {
1020
1667
  }
1021
1668
  }
1022
1669
  }, []);
1023
- const markStashRestored = useCallback2(() => {
1670
+ const markStashRestored = useCallback3(() => {
1024
1671
  stashRestoredRef.current = true;
1025
1672
  removeStashGuard();
1026
1673
  }, []);
1027
- useEffect4(() => {
1674
+ useEffect6(() => {
1028
1675
  mountedRef.current = true;
1029
1676
  async function check() {
1030
1677
  const guard = await checkStashGuard();
@@ -1079,9 +1726,10 @@ function App({ initialRemote, initialBranch }) {
1079
1726
  const clean = await isWorkingDirClean();
1080
1727
  if (mountedRef.current) setStep(clean ? entryStep : "stash-prompt");
1081
1728
  };
1082
- const goBack = useCallback2((fromStep) => {
1729
+ const goBack = useCallback3((fromStep) => {
1083
1730
  const backMap = {
1084
1731
  branch: "remote",
1732
+ "branch-check": "branch",
1085
1733
  commits: "branch",
1086
1734
  confirm: "commits"
1087
1735
  };
@@ -1093,10 +1741,10 @@ function App({ initialRemote, initialBranch }) {
1093
1741
  exit();
1094
1742
  }
1095
1743
  }, [setStep, restoreStashSync, exit]);
1096
- return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
1097
- /* @__PURE__ */ jsx9(AppHeader, { step: STEP_NUMBER[step], stashed }),
1098
- step === "checking" && /* @__PURE__ */ jsx9(Spinner6, { label: "\u68C0\u67E5\u5DE5\u4F5C\u533A\u72B6\u6001..." }),
1099
- step === "stash-recovery" && inputReady && /* @__PURE__ */ jsx9(
1744
+ return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", children: [
1745
+ /* @__PURE__ */ jsx10(AppHeader, { step: STEP_NUMBER[step], stashed, noCommit }),
1746
+ step === "checking" && /* @__PURE__ */ jsx10(Spinner7, { label: "\u68C0\u67E5\u5DE5\u4F5C\u533A\u72B6\u6001..." }),
1747
+ step === "stash-recovery" && inputReady && /* @__PURE__ */ jsx10(
1100
1748
  StashRecovery,
1101
1749
  {
1102
1750
  timestamp: guardTimestamp,
@@ -1104,14 +1752,14 @@ function App({ initialRemote, initialBranch }) {
1104
1752
  onSkip: skipStashRecover
1105
1753
  }
1106
1754
  ),
1107
- step === "stash-prompt" && inputReady && /* @__PURE__ */ jsx9(
1755
+ step === "stash-prompt" && inputReady && /* @__PURE__ */ jsx10(
1108
1756
  StashPrompt,
1109
1757
  {
1110
1758
  onConfirm: doStash,
1111
1759
  onSkip: () => setStep(entryStep)
1112
1760
  }
1113
1761
  ),
1114
- step === "remote" && inputReady && /* @__PURE__ */ jsx9(
1762
+ step === "remote" && inputReady && /* @__PURE__ */ jsx10(
1115
1763
  RemoteSelect,
1116
1764
  {
1117
1765
  onSelect: (r) => {
@@ -1121,49 +1769,58 @@ function App({ initialRemote, initialBranch }) {
1121
1769
  onBack: () => goBack("remote")
1122
1770
  }
1123
1771
  ),
1124
- step === "branch" && inputReady && /* @__PURE__ */ jsx9(
1772
+ step === "branch" && inputReady && /* @__PURE__ */ jsx10(
1125
1773
  BranchSelect,
1126
1774
  {
1127
1775
  remote: remote2,
1128
1776
  onSelect: (b) => {
1129
1777
  setBranch(b);
1130
- setStep("commits");
1778
+ setStep("branch-check");
1131
1779
  },
1132
1780
  onBack: () => goBack("branch")
1133
1781
  }
1134
1782
  ),
1135
- step === "commits" && inputReady && /* @__PURE__ */ jsx9(
1783
+ step === "branch-check" && inputReady && /* @__PURE__ */ jsx10(
1784
+ BranchCheck,
1785
+ {
1786
+ targetBranch: branch2,
1787
+ onContinue: () => setStep("commits"),
1788
+ onBack: () => goBack("branch-check")
1789
+ }
1790
+ ),
1791
+ step === "commits" && inputReady && /* @__PURE__ */ jsx10(
1136
1792
  CommitList,
1137
1793
  {
1138
1794
  remote: remote2,
1139
1795
  branch: branch2,
1140
- onSelect: async (hashes, loadedCommits) => {
1796
+ onSelect: (hashes, loadedCommits) => {
1141
1797
  setSelectedHashes(hashes);
1142
1798
  setCommits(loadedCommits);
1143
- const merge = await hasMergeCommits(hashes);
1144
- setHasMerge(merge);
1145
1799
  setStep("confirm");
1146
1800
  },
1147
1801
  onBack: () => goBack("commits")
1148
1802
  }
1149
1803
  ),
1150
- step === "confirm" && inputReady && /* @__PURE__ */ jsx9(
1804
+ step === "confirm" && inputReady && /* @__PURE__ */ jsx10(
1151
1805
  ConfirmPanel,
1152
1806
  {
1153
1807
  commits: commits2,
1154
1808
  selectedHashes,
1155
1809
  hasMerge,
1156
1810
  useMainline,
1811
+ noCommit,
1157
1812
  onToggleMainline: () => setUseMainline((v) => !v),
1813
+ onToggleNoCommit: () => setNoCommit((v) => !v),
1158
1814
  onConfirm: () => setStep("result"),
1159
1815
  onCancel: () => goBack("confirm")
1160
1816
  }
1161
1817
  ),
1162
- step === "result" && /* @__PURE__ */ jsx9(
1818
+ step === "result" && /* @__PURE__ */ jsx10(
1163
1819
  ResultPanel,
1164
1820
  {
1165
1821
  selectedHashes,
1166
1822
  useMainline,
1823
+ noCommit,
1167
1824
  stashed,
1168
1825
  onStashRestored: markStashRestored,
1169
1826
  onDone: () => {
@@ -1340,7 +1997,7 @@ async function runCli(opts) {
1340
1997
  }
1341
1998
 
1342
1999
  // src/cli.tsx
1343
- import { jsx as jsx10 } from "react/jsx-runtime";
2000
+ import { jsx as jsx11 } from "react/jsx-runtime";
1344
2001
  var cli = meow(
1345
2002
  `
1346
2003
  \u7528\u6CD5
@@ -1350,7 +2007,7 @@ var cli = meow(
1350
2007
  -r, --remote <name> \u6307\u5B9A\u8FDC\u7A0B\u4ED3\u5E93\u540D\u79F0
1351
2008
  -b, --branch <name> \u6307\u5B9A\u8FDC\u7A0B\u5206\u652F\u540D\u79F0
1352
2009
  -c, --commits <hashes> \u6307\u5B9A commit hash\uFF08\u9017\u53F7\u5206\u9694\uFF09
1353
- -n, --count <number> \u663E\u793A commit \u6570\u91CF\uFF08\u9ED8\u8BA4 30\uFF09
2010
+ -n, --count <number> \u663E\u793A commit \u6570\u91CF\uFF08\u9ED8\u8BA4 100\uFF09
1354
2011
  -m, --mainline \u5BF9 merge commit \u4F7F\u7528 -m 1
1355
2012
  -y, --yes \u8DF3\u8FC7\u786E\u8BA4\u76F4\u63A5\u6267\u884C
1356
2013
  --no-stash \u8DF3\u8FC7 stash \u63D0\u793A
@@ -1386,7 +2043,7 @@ var cli = meow(
1386
2043
  remote: { type: "string", shortFlag: "r" },
1387
2044
  branch: { type: "string", shortFlag: "b" },
1388
2045
  commits: { type: "string", shortFlag: "c" },
1389
- count: { type: "number", shortFlag: "n", default: 30 },
2046
+ count: { type: "number", shortFlag: "n", default: 100 },
1390
2047
  mainline: { type: "boolean", shortFlag: "m", default: false },
1391
2048
  yes: { type: "boolean", shortFlag: "y", default: false },
1392
2049
  noStash: { type: "boolean", default: false },
@@ -1414,5 +2071,5 @@ if (isCliMode) {
1414
2071
  process.exit(1);
1415
2072
  });
1416
2073
  } else {
1417
- render(/* @__PURE__ */ jsx10(App, { initialRemote: remote, initialBranch: branch }));
2074
+ render(/* @__PURE__ */ jsx11(App, { initialRemote: remote, initialBranch: branch }));
1418
2075
  }