git-sync-tui 0.1.4 → 0.1.5

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 +332 -121
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -11,7 +11,7 @@ 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";
14
+ import { useState as useState7, useEffect as useEffect5, useRef as useRef3, useCallback as useCallback2 } from "react";
15
15
  import { Box as Box9, useApp } from "ink";
16
16
  import { Spinner as Spinner6 } from "@inkjs/ui";
17
17
 
@@ -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
  "]"
@@ -92,13 +94,12 @@ function StatusPanel({ type, title, children }) {
92
94
  }
93
95
  function AppHeader({ step, stashed }) {
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__ */ jsx(Text, { color: "gray", children: "cherry-pick --no-commit" }),
100
+ stashed && /* @__PURE__ */ jsx(Text, { backgroundColor: "yellow", color: "white", bold: true, children: " STASHED " })
100
101
  ] }),
101
- /* @__PURE__ */ jsxs(Box, { children: [
102
+ /* @__PURE__ */ jsxs(Box, { marginTop: 0, children: [
102
103
  /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: " " }),
103
104
  /* @__PURE__ */ jsx(StepProgress, { current: step })
104
105
  ] })
@@ -158,28 +159,20 @@ async function addRemote(name, url) {
158
159
  }
159
160
  async function getRemoteBranches(remote2) {
160
161
  const git = getGit();
161
- let fetchOk = false;
162
162
  try {
163
- await git.fetch(remote2);
164
- fetchOk = true;
163
+ const lsResult = await git.raw(["ls-remote", "--heads", remote2]);
164
+ if (lsResult.trim()) {
165
+ return lsResult.trim().split("\n").map((line) => line.replace(/^.*refs\/heads\//, "")).filter(Boolean).sort();
166
+ }
165
167
  } catch {
166
168
  }
167
169
  const result = await git.branch(["-r"]);
168
170
  const prefix = `${remote2}/`;
169
171
  const branches = result.all.filter((b) => b.startsWith(prefix) && !b.includes("HEAD")).map((b) => b.replace(prefix, "")).sort();
170
172
  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
- }
173
+ 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
174
  }
182
- async function getCommits(remote2, branch2, count2 = 30) {
175
+ async function getCommits(remote2, branch2, count2 = 100) {
183
176
  const git = getGit();
184
177
  const ref = `${remote2}/${branch2}`;
185
178
  try {
@@ -204,6 +197,27 @@ async function getCommits(remote2, branch2, count2 = 30) {
204
197
  return { hash, shortHash, message: message || "", author: author || "", date: date || "" };
205
198
  });
206
199
  }
200
+ async function getUnsyncedCommits(remote2, branch2, count2 = 100) {
201
+ const git = getGit();
202
+ const ref = `${remote2}/${branch2}`;
203
+ const allCommits = await getCommits(remote2, branch2, count2);
204
+ try {
205
+ const result = await git.raw([
206
+ "cherry",
207
+ "HEAD",
208
+ ref
209
+ ]);
210
+ const unsyncedHashes = new Set(
211
+ result.trim().split("\n").filter((line) => line.startsWith("+ ")).map((line) => line.substring(2).trim())
212
+ );
213
+ return allCommits.map((c) => ({
214
+ ...c,
215
+ synced: !unsyncedHashes.has(c.hash)
216
+ }));
217
+ } catch {
218
+ return allCommits;
219
+ }
220
+ }
207
221
  async function getMultiCommitStat(hashes) {
208
222
  if (hashes.length === 0) return "";
209
223
  const git = getGit();
@@ -393,10 +407,10 @@ function StashRecovery({ timestamp, onRecover, onSkip }) {
393
407
  // src/components/remote-select.tsx
394
408
  import { useState as useState3 } from "react";
395
409
  import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
396
- import { Select, Spinner as Spinner2, TextInput } from "@inkjs/ui";
410
+ import { Spinner as Spinner2, TextInput } from "@inkjs/ui";
397
411
 
398
412
  // src/hooks/use-git.ts
399
- import { useState as useState2, useEffect as useEffect2, useCallback } from "react";
413
+ import { useState as useState2, useEffect as useEffect2, useCallback, useRef } from "react";
400
414
  function useAsync(fn, deps = []) {
401
415
  const [state, setState] = useState2({
402
416
  data: null,
@@ -426,11 +440,49 @@ function useBranches(remote2) {
426
440
  [remote2]
427
441
  );
428
442
  }
429
- function useCommits(remote2, branch2, count2 = 30) {
430
- return useAsync(
431
- () => remote2 && branch2 ? getCommits(remote2, branch2, count2) : Promise.resolve([]),
432
- [remote2, branch2, count2]
433
- );
443
+ function useCommits(remote2, branch2, pageSize = 100) {
444
+ const [data, setData] = useState2(null);
445
+ const [loading, setLoading] = useState2(true);
446
+ const [loadingMore, setLoadingMore] = useState2(false);
447
+ const [error2, setError] = useState2(null);
448
+ const [hasMore, setHasMore] = useState2(true);
449
+ const loadedRef = useRef(0);
450
+ useEffect2(() => {
451
+ if (!remote2 || !branch2) {
452
+ setData([]);
453
+ setLoading(false);
454
+ setHasMore(false);
455
+ return;
456
+ }
457
+ setData(null);
458
+ setLoading(true);
459
+ setError(null);
460
+ setHasMore(true);
461
+ loadedRef.current = 0;
462
+ getUnsyncedCommits(remote2, branch2, pageSize).then((commits2) => {
463
+ setData(commits2);
464
+ setLoading(false);
465
+ loadedRef.current = commits2.length;
466
+ setHasMore(commits2.length >= pageSize);
467
+ }).catch((err) => {
468
+ setError(err.message);
469
+ setLoading(false);
470
+ });
471
+ }, [remote2, branch2, pageSize]);
472
+ const loadMore = useCallback(async () => {
473
+ if (!remote2 || !branch2 || loadingMore || !hasMore) return;
474
+ setLoadingMore(true);
475
+ try {
476
+ const nextCount = loadedRef.current + pageSize;
477
+ const allCommits = await getUnsyncedCommits(remote2, branch2, nextCount);
478
+ setData(allCommits);
479
+ setHasMore(allCommits.length >= nextCount);
480
+ loadedRef.current = allCommits.length;
481
+ } catch {
482
+ }
483
+ setLoadingMore(false);
484
+ }, [remote2, branch2, pageSize, loadingMore, hasMore]);
485
+ return { data, loading, loadingMore, error: error2, hasMore, loadMore };
434
486
  }
435
487
  function useCommitStat(hashes) {
436
488
  const [stat, setStat] = useState2("");
@@ -462,16 +514,32 @@ function extractRemoteName(url) {
462
514
  function RemoteSelect({ onSelect, onBack }) {
463
515
  const { data: remotes, loading, error: error2, reload } = useRemotes();
464
516
  const [phase, setPhase] = useState3("list");
517
+ const [cursorIndex, setCursorIndex] = useState3(0);
465
518
  const [customUrl, setCustomUrl] = useState3("");
466
519
  const [addError, setAddError] = useState3(null);
467
- useInput3((_input, key) => {
520
+ const totalItems = (remotes?.length || 0) + 1;
521
+ useInput3((input, key) => {
522
+ if (phase !== "list") {
523
+ if (key.escape) {
524
+ if (phase === "input-name") {
525
+ setPhase("input-url");
526
+ } else if (phase === "input-url") {
527
+ setPhase("list");
528
+ }
529
+ }
530
+ return;
531
+ }
468
532
  if (key.escape) {
469
- if (phase === "input-name") {
533
+ onBack?.();
534
+ } else if (key.upArrow) {
535
+ setCursorIndex((prev) => Math.max(0, prev - 1));
536
+ } else if (key.downArrow) {
537
+ setCursorIndex((prev) => Math.min(totalItems - 1, prev + 1));
538
+ } else if (key.return) {
539
+ if (remotes && cursorIndex < remotes.length) {
540
+ onSelect(remotes[cursorIndex].name);
541
+ } else {
470
542
  setPhase("input-url");
471
- } else if (phase === "input-url") {
472
- setPhase("list");
473
- } else if (phase === "list") {
474
- onBack?.();
475
543
  }
476
544
  }
477
545
  });
@@ -480,7 +548,8 @@ function RemoteSelect({ onSelect, onBack }) {
480
548
  }
481
549
  if (error2) {
482
550
  return /* @__PURE__ */ jsxs4(Text4, { color: "red", children: [
483
- "\u2716 \u83B7\u53D6\u8FDC\u7A0B\u4ED3\u5E93\u5931\u8D25: ",
551
+ "\u2716 ",
552
+ "\u83B7\u53D6\u8FDC\u7A0B\u4ED3\u5E93\u5931\u8D25: ",
484
553
  error2
485
554
  ] });
486
555
  }
@@ -511,7 +580,10 @@ function RemoteSelect({ onSelect, onBack }) {
511
580
  }
512
581
  )
513
582
  ] }),
514
- /* @__PURE__ */ jsx4(Text4, { color: "gray", dimColor: true, children: " \u652F\u6301 HTTPS / SSH \u5730\u5740" })
583
+ /* @__PURE__ */ jsxs4(Text4, { color: "gray", dimColor: true, children: [
584
+ " ",
585
+ "\u652F\u6301 HTTPS / SSH \u5730\u5740"
586
+ ] })
515
587
  ] });
516
588
  }
517
589
  if (phase === "input-name") {
@@ -555,99 +627,187 @@ function RemoteSelect({ onSelect, onBack }) {
555
627
  ] })
556
628
  ] });
557
629
  }
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: [
630
+ const maxNameLen = Math.max(...(remotes || []).map((r) => r.name.length), 0);
631
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
569
632
  /* @__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
- )
633
+ /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginTop: 1, children: [
634
+ (remotes || []).map((r, i) => {
635
+ const isCursor = i === cursorIndex;
636
+ return /* @__PURE__ */ jsxs4(Box4, { children: [
637
+ /* @__PURE__ */ jsx4(Text4, { color: isCursor ? "cyan" : "gray", children: isCursor ? "\u203A" : " " }),
638
+ /* @__PURE__ */ jsx4(Text4, { children: " " }),
639
+ /* @__PURE__ */ jsx4(Text4, { color: isCursor ? "cyan" : "white", bold: isCursor, children: r.name.padEnd(maxNameLen + 2) }),
640
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", dimColor: true, children: r.fetchUrl })
641
+ ] }, r.name);
642
+ }),
643
+ /* @__PURE__ */ jsxs4(Box4, { children: [
644
+ /* @__PURE__ */ jsx4(Text4, { color: cursorIndex === (remotes?.length || 0) ? "cyan" : "gray", children: cursorIndex === (remotes?.length || 0) ? "\u203A" : " " }),
645
+ /* @__PURE__ */ jsx4(Text4, { children: " " }),
646
+ /* @__PURE__ */ jsx4(Text4, { color: "green", dimColor: cursorIndex !== (remotes?.length || 0), children: "+ \u6DFB\u52A0\u8FDC\u7A0B\u4ED3\u5E93..." })
647
+ ] })
648
+ ] })
583
649
  ] });
584
650
  }
585
651
 
586
652
  // src/components/branch-select.tsx
587
653
  import { useState as useState4, useMemo } from "react";
588
654
  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";
655
+ import { Spinner as Spinner3, TextInput as TextInput2 } from "@inkjs/ui";
590
656
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
591
657
  function BranchSelect({ remote: remote2, onSelect, onBack }) {
592
658
  const { data: branches, loading, error: error2 } = useBranches(remote2);
593
659
  const [filter, setFilter] = useState4("");
594
- useInput4((_input, key) => {
595
- if (key.escape) onBack?.();
596
- });
597
- const filteredOptions = useMemo(() => {
660
+ const [cursorIndex, setCursorIndex] = useState4(0);
661
+ const filteredBranches = useMemo(() => {
598
662
  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 }));
663
+ return filter ? branches.filter((b) => b.toLowerCase().includes(filter.toLowerCase())) : branches;
601
664
  }, [branches, filter]);
665
+ const visibleCount = 10;
666
+ const startIdx = Math.max(0, Math.min(cursorIndex - Math.floor(visibleCount / 2), filteredBranches.length - visibleCount));
667
+ const visibleBranches = filteredBranches.slice(startIdx, startIdx + visibleCount);
668
+ useInput4((input, key) => {
669
+ if (key.escape) {
670
+ onBack?.();
671
+ } else if (key.upArrow) {
672
+ setCursorIndex((prev) => Math.max(0, prev - 1));
673
+ } else if (key.downArrow) {
674
+ setCursorIndex((prev) => Math.min(filteredBranches.length - 1, prev + 1));
675
+ } else if (key.return) {
676
+ if (filteredBranches.length > 0) {
677
+ onSelect(filteredBranches[cursorIndex]);
678
+ }
679
+ }
680
+ });
602
681
  if (loading) {
603
682
  return /* @__PURE__ */ jsx5(Spinner3, { label: `\u83B7\u53D6 ${remote2} \u7684\u5206\u652F\u5217\u8868...` });
604
683
  }
605
684
  if (error2) {
606
685
  return /* @__PURE__ */ jsxs5(Text5, { color: "red", children: [
607
- "\u2716 \u83B7\u53D6\u5206\u652F\u5217\u8868\u5931\u8D25: ",
686
+ "\u2716 ",
687
+ "\u83B7\u53D6\u5206\u652F\u5217\u8868\u5931\u8D25: ",
608
688
  error2
609
689
  ] });
610
690
  }
611
691
  if (!branches || branches.length === 0) {
612
- return /* @__PURE__ */ jsx5(Text5, { color: "red", children: "\u2716 \u672A\u627E\u5230\u8FDC\u7A0B\u5206\u652F" });
692
+ return /* @__PURE__ */ jsxs5(Text5, { color: "red", children: [
693
+ "\u2716 ",
694
+ "\u672A\u627E\u5230\u8FDC\u7A0B\u5206\u652F"
695
+ ] });
613
696
  }
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: "/ " }),
697
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
698
+ /* @__PURE__ */ jsx5(SectionHeader, { title: "\u9009\u62E9\u5206\u652F", subtitle: `${remote2} \xB7 ${branches.length} \u4E2A\u5206\u652F` }),
699
+ /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, children: [
700
+ /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: "/ " }),
618
701
  /* @__PURE__ */ jsx5(
619
702
  TextInput2,
620
703
  {
621
704
  placeholder: "\u8F93\u5165\u5173\u952E\u5B57\u8FC7\u6EE4...",
622
- onChange: setFilter
705
+ onChange: (val) => {
706
+ setFilter(val);
707
+ setCursorIndex(0);
708
+ }
623
709
  }
624
710
  ),
625
711
  filter && /* @__PURE__ */ jsxs5(Text5, { color: "gray", dimColor: true, children: [
626
712
  " \xB7 \u5339\u914D ",
627
- filteredOptions.length
713
+ filteredBranches.length
628
714
  ] })
629
715
  ] }),
630
- filteredOptions.length > 0 ? /* @__PURE__ */ jsx5(Select2, { options: filteredOptions, onChange: onSelect }) : /* @__PURE__ */ jsx5(Text5, { color: "yellow", children: "\u25B2 \u65E0\u5339\u914D\u5206\u652F" })
716
+ filteredBranches.length > 0 ? /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginTop: 1, children: [
717
+ startIdx > 0 && /* @__PURE__ */ jsxs5(Text5, { color: "gray", dimColor: true, children: [
718
+ " ",
719
+ "\u2191 ",
720
+ startIdx,
721
+ " more"
722
+ ] }),
723
+ visibleBranches.map((b, i) => {
724
+ const actualIdx = startIdx + i;
725
+ const isCursor = actualIdx === cursorIndex;
726
+ return /* @__PURE__ */ jsxs5(Box5, { children: [
727
+ /* @__PURE__ */ jsx5(Text5, { color: isCursor ? "cyan" : "gray", children: isCursor ? "\u203A" : " " }),
728
+ /* @__PURE__ */ jsx5(Text5, { children: " " }),
729
+ /* @__PURE__ */ jsx5(Text5, { color: isCursor ? "cyan" : "white", bold: isCursor, children: b })
730
+ ] }, b);
731
+ }),
732
+ startIdx + visibleCount < filteredBranches.length && /* @__PURE__ */ jsxs5(Text5, { color: "gray", dimColor: true, children: [
733
+ " ",
734
+ "\u2193 ",
735
+ filteredBranches.length - startIdx - visibleCount,
736
+ " more"
737
+ ] })
738
+ ] }) : /* @__PURE__ */ jsxs5(Text5, { color: "yellow", children: [
739
+ "\u25B2 ",
740
+ "\u65E0\u5339\u914D\u5206\u652F"
741
+ ] })
631
742
  ] });
632
743
  }
633
744
 
634
745
  // src/components/commit-list.tsx
635
- import { useState as useState5, useMemo as useMemo2, useRef } from "react";
746
+ import { useState as useState5, useMemo as useMemo2, useRef as useRef2, useEffect as useEffect3 } from "react";
636
747
  import { Box as Box6, Text as Text6, useInput as useInput5 } from "ink";
637
748
  import { Spinner as Spinner4 } from "@inkjs/ui";
638
749
  import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
750
+ function StatPanel({ stat, loading, count: count2 }) {
751
+ const STAT_HEIGHT = 8;
752
+ const [scrollOffset, setScrollOffset] = useState5(0);
753
+ const lines = useMemo2(() => stat ? stat.split("\n") : [], [stat]);
754
+ const canScroll = lines.length > STAT_HEIGHT;
755
+ useEffect3(() => {
756
+ setScrollOffset(0);
757
+ }, [stat]);
758
+ useInput5((input) => {
759
+ if (!canScroll) return;
760
+ if (input === "j") {
761
+ setScrollOffset((prev) => Math.min(prev + 1, lines.length - STAT_HEIGHT));
762
+ } else if (input === "k") {
763
+ setScrollOffset((prev) => Math.max(0, prev - 1));
764
+ }
765
+ });
766
+ const visibleLines = canScroll ? lines.slice(scrollOffset, scrollOffset + STAT_HEIGHT) : lines;
767
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
768
+ /* @__PURE__ */ jsxs6(Box6, { justifyContent: "space-between", children: [
769
+ /* @__PURE__ */ jsxs6(Text6, { bold: true, color: "cyan", children: [
770
+ "\u25C6 ",
771
+ "\u5DF2\u9009 ",
772
+ count2,
773
+ " \u4E2A commit \xB7 diff --stat"
774
+ ] }),
775
+ canScroll && /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
776
+ scrollOffset + 1,
777
+ "-",
778
+ Math.min(scrollOffset + STAT_HEIGHT, lines.length),
779
+ "/",
780
+ lines.length,
781
+ " [j/k]"
782
+ ] })
783
+ ] }),
784
+ /* @__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)" }) })
785
+ ] });
786
+ }
639
787
  function CommitList({ remote: remote2, branch: branch2, onSelect, onBack }) {
640
- const { data: commits2, loading, error: error2 } = useCommits(remote2, branch2, 30);
788
+ const { data: commits2, loading, loadingMore, error: error2, hasMore, loadMore } = useCommits(remote2, branch2, 100);
641
789
  const [selectedIndex, setSelectedIndex] = useState5(0);
642
790
  const [selectedHashes, setSelectedHashes] = useState5(/* @__PURE__ */ new Set());
643
791
  const [shiftMode, setShiftMode] = useState5(false);
644
- const anchorIndexRef = useRef(null);
792
+ const anchorIndexRef = useRef2(null);
645
793
  const selectedKey = useMemo2(() => Array.from(selectedHashes).sort().join(","), [selectedHashes]);
646
794
  const selectedArray = useMemo2(() => Array.from(selectedHashes), [selectedKey]);
647
795
  const { stat, loading: statLoading } = useCommitStat(selectedArray);
796
+ const syncedCount = useMemo2(() => {
797
+ if (!commits2) return 0;
798
+ return commits2.filter((c) => c.synced).length;
799
+ }, [commits2]);
800
+ useEffect3(() => {
801
+ if (!commits2 || !hasMore || loadingMore) return;
802
+ if (selectedIndex >= commits2.length - 5) {
803
+ loadMore();
804
+ }
805
+ }, [selectedIndex, commits2?.length, hasMore, loadingMore, loadMore]);
648
806
  const toggleCurrent = () => {
649
807
  if (!commits2 || commits2.length === 0) return;
650
- const hash = commits2[selectedIndex].hash;
808
+ const commit = commits2[selectedIndex];
809
+ if (commit.synced) return;
810
+ const hash = commit.hash;
651
811
  setSelectedHashes((prev) => {
652
812
  const next = new Set(prev);
653
813
  if (next.has(hash)) {
@@ -667,19 +827,22 @@ function CommitList({ remote: remote2, branch: branch2, onSelect, onBack }) {
667
827
  setSelectedHashes((prev) => {
668
828
  const next = new Set(prev);
669
829
  for (let i = start; i <= end; i++) {
670
- next.add(commits2[i].hash);
830
+ if (!commits2[i].synced) {
831
+ next.add(commits2[i].hash);
832
+ }
671
833
  }
672
834
  return next;
673
835
  });
674
836
  };
675
837
  const toggleAll = () => {
676
838
  if (!commits2 || commits2.length === 0) return;
839
+ const unsyncedCommits = commits2.filter((c) => !c.synced);
677
840
  setSelectedHashes((prev) => {
678
- if (prev.size === commits2.length) {
841
+ if (prev.size === unsyncedCommits.length) {
679
842
  anchorIndexRef.current = null;
680
843
  return /* @__PURE__ */ new Set();
681
844
  }
682
- return new Set(commits2.map((c) => c.hash));
845
+ return new Set(unsyncedCommits.map((c) => c.hash));
683
846
  });
684
847
  };
685
848
  const invertSelection = () => {
@@ -687,7 +850,7 @@ function CommitList({ remote: remote2, branch: branch2, onSelect, onBack }) {
687
850
  setSelectedHashes((prev) => {
688
851
  const next = /* @__PURE__ */ new Set();
689
852
  for (const c of commits2) {
690
- if (!prev.has(c.hash)) next.add(c.hash);
853
+ if (!c.synced && !prev.has(c.hash)) next.add(c.hash);
691
854
  }
692
855
  return next;
693
856
  });
@@ -697,7 +860,9 @@ function CommitList({ remote: remote2, branch: branch2, onSelect, onBack }) {
697
860
  setSelectedHashes((prev) => {
698
861
  const next = new Set(prev);
699
862
  for (let i = 0; i <= selectedIndex; i++) {
700
- next.add(commits2[i].hash);
863
+ if (!commits2[i].synced) {
864
+ next.add(commits2[i].hash);
865
+ }
701
866
  }
702
867
  return next;
703
868
  });
@@ -754,16 +919,21 @@ function CommitList({ remote: remote2, branch: branch2, onSelect, onBack }) {
754
919
  }
755
920
  if (error2) {
756
921
  return /* @__PURE__ */ jsxs6(Text6, { color: "red", children: [
757
- "\u2716 \u83B7\u53D6 commit \u5217\u8868\u5931\u8D25: ",
922
+ "\u2716 ",
923
+ "\u83B7\u53D6 commit \u5217\u8868\u5931\u8D25: ",
758
924
  error2
759
925
  ] });
760
926
  }
761
927
  if (!commits2 || commits2.length === 0) {
762
- return /* @__PURE__ */ jsx6(Text6, { color: "yellow", children: "\u25B2 \u8BE5\u5206\u652F\u6CA1\u6709 commit" });
928
+ return /* @__PURE__ */ jsxs6(Text6, { color: "yellow", children: [
929
+ "\u25B2 ",
930
+ "\u8BE5\u5206\u652F\u6CA1\u6709 commit"
931
+ ] });
763
932
  }
764
933
  const visibleCount = 10;
765
934
  const startIdx = Math.max(0, Math.min(selectedIndex - Math.floor(visibleCount / 2), commits2.length - visibleCount));
766
935
  const visibleCommits = commits2.slice(startIdx, startIdx + visibleCount);
936
+ const unsyncedTotal = commits2.length - syncedCount;
767
937
  return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", gap: 1, children: [
768
938
  /* @__PURE__ */ jsx6(SectionHeader, { title: "\u9009\u62E9\u8981\u540C\u6B65\u7684 commit" }),
769
939
  /* @__PURE__ */ jsxs6(Box6, { gap: 2, children: [
@@ -774,17 +944,28 @@ function CommitList({ remote: remote2, branch: branch2, onSelect, onBack }) {
774
944
  ] }),
775
945
  /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
776
946
  commits2.length,
777
- " commits"
947
+ " commits",
948
+ hasMore ? "+" : ""
949
+ ] }),
950
+ syncedCount > 0 && /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
951
+ syncedCount,
952
+ " \u5DF2\u540C\u6B65"
953
+ ] }),
954
+ /* @__PURE__ */ jsxs6(Text6, { color: "green", children: [
955
+ unsyncedTotal,
956
+ " \u5F85\u540C\u6B65"
778
957
  ] }),
779
958
  /* @__PURE__ */ jsxs6(Text6, { color: selectedHashes.size > 0 ? "cyan" : "gray", bold: selectedHashes.size > 0, children: [
780
959
  "\u5DF2\u9009 ",
781
960
  selectedHashes.size
782
961
  ] }),
783
- shiftMode && /* @__PURE__ */ jsx6(Text6, { color: "yellow", bold: true, children: "SHIFT" })
962
+ shiftMode && /* @__PURE__ */ jsx6(Text6, { color: "yellow", bold: true, children: "SHIFT" }),
963
+ loadingMore && /* @__PURE__ */ jsx6(Text6, { color: "gray", dimColor: true, children: "\u52A0\u8F7D\u4E2D..." })
784
964
  ] }),
785
965
  /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
786
966
  startIdx > 0 && /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
787
- " \u2191 ",
967
+ " ",
968
+ "\u2191 ",
788
969
  startIdx,
789
970
  " more"
790
971
  ] }),
@@ -793,30 +974,60 @@ function CommitList({ remote: remote2, branch: branch2, onSelect, onBack }) {
793
974
  const isSelected = selectedHashes.has(c.hash);
794
975
  const isCursor = actualIdx === selectedIndex;
795
976
  const isAnchor = actualIdx === anchorIndexRef.current;
977
+ const isSynced = !!c.synced;
796
978
  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
- ] }),
979
+ /* @__PURE__ */ jsxs6(
980
+ Text6,
981
+ {
982
+ backgroundColor: isCursor ? isSynced ? "gray" : "blue" : void 0,
983
+ color: isSynced ? "gray" : isSelected ? "green" : "white",
984
+ dimColor: isSynced,
985
+ children: [
986
+ isCursor ? "\u25B8 " : " ",
987
+ isSynced ? "\u2713" : isAnchor ? "\u2693" : isSelected ? "\u25CF" : "\u25CB",
988
+ " "
989
+ ]
990
+ }
991
+ ),
992
+ /* @__PURE__ */ jsx6(
993
+ Text6,
994
+ {
995
+ backgroundColor: isCursor ? isSynced ? "gray" : "blue" : void 0,
996
+ color: isSynced ? "gray" : "yellow",
997
+ dimColor: isSynced,
998
+ children: c.shortHash
999
+ }
1000
+ ),
1001
+ /* @__PURE__ */ jsxs6(
1002
+ Text6,
1003
+ {
1004
+ backgroundColor: isCursor ? isSynced ? "gray" : "blue" : void 0,
1005
+ color: isSynced ? "gray" : isSelected ? "green" : "white",
1006
+ dimColor: isSynced,
1007
+ children: [
1008
+ " ",
1009
+ c.message
1010
+ ]
1011
+ }
1012
+ ),
807
1013
  /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
808
1014
  " ",
809
1015
  c.author,
810
1016
  " \xB7 ",
811
1017
  c.date
812
- ] })
1018
+ ] }),
1019
+ isSynced && /* @__PURE__ */ jsx6(Text6, { color: "gray", dimColor: true, children: " [\u5DF2\u540C\u6B65]" })
813
1020
  ] }, c.hash);
814
1021
  }),
815
- startIdx + visibleCount < commits2.length && /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
816
- " \u2193 ",
1022
+ startIdx + visibleCount < commits2.length ? /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
1023
+ " ",
1024
+ "\u2193 ",
817
1025
  commits2.length - startIdx - visibleCount,
818
1026
  " more"
819
- ] })
1027
+ ] }) : hasMore ? /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
1028
+ " ",
1029
+ "\u2193 \u6EDA\u52A8\u52A0\u8F7D\u66F4\u591A..."
1030
+ ] }) : null
820
1031
  ] }),
821
1032
  /* @__PURE__ */ jsx6(KeyHints, { hints: [
822
1033
  { key: "\u2191\u2193", label: "\u5BFC\u822A" },
@@ -828,7 +1039,7 @@ function CommitList({ remote: remote2, branch: branch2, onSelect, onBack }) {
828
1039
  { key: "Enter", label: "\u786E\u8BA4" },
829
1040
  { key: "Esc", label: "\u8FD4\u56DE" }
830
1041
  ] }),
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)" }) })
1042
+ selectedHashes.size > 0 && /* @__PURE__ */ jsx6(StatPanel, { stat, loading: statLoading, count: selectedHashes.size })
832
1043
  ] });
833
1044
  }
834
1045
 
@@ -890,7 +1101,7 @@ function ConfirmPanel({ commits: commits2, selectedHashes, hasMerge, useMainline
890
1101
  }
891
1102
 
892
1103
  // src/components/result-panel.tsx
893
- import { useState as useState6, useEffect as useEffect3 } from "react";
1104
+ import { useState as useState6, useEffect as useEffect4 } from "react";
894
1105
  import { Box as Box8, Text as Text8 } from "ink";
895
1106
  import { Spinner as Spinner5 } from "@inkjs/ui";
896
1107
  import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
@@ -907,7 +1118,7 @@ function ResultPanel({ selectedHashes, useMainline, stashed, onStashRestored, on
907
1118
  if (ok) onStashRestored();
908
1119
  return ok;
909
1120
  };
910
- useEffect3(() => {
1121
+ useEffect4(() => {
911
1122
  async function run() {
912
1123
  const res = await cherryPick(selectedHashes, useMainline);
913
1124
  setResult(res);
@@ -994,10 +1205,10 @@ function App({ initialRemote, initialBranch }) {
994
1205
  const [useMainline, setUseMainline] = useState7(false);
995
1206
  const [stashed, setStashed] = useState7(false);
996
1207
  const [guardTimestamp, setGuardTimestamp] = useState7();
997
- const stashedRef = useRef2(false);
998
- const stashRestoredRef = useRef2(false);
999
- const mountedRef = useRef2(true);
1000
- const debounceTimer = useRef2(null);
1208
+ const stashedRef = useRef3(false);
1209
+ const stashRestoredRef = useRef3(false);
1210
+ const mountedRef = useRef3(true);
1211
+ const debounceTimer = useRef3(null);
1001
1212
  const setStep = useCallback2((newStep) => {
1002
1213
  setInputReady(false);
1003
1214
  setStepRaw(newStep);
@@ -1024,7 +1235,7 @@ function App({ initialRemote, initialBranch }) {
1024
1235
  stashRestoredRef.current = true;
1025
1236
  removeStashGuard();
1026
1237
  }, []);
1027
- useEffect4(() => {
1238
+ useEffect5(() => {
1028
1239
  mountedRef.current = true;
1029
1240
  async function check() {
1030
1241
  const guard = await checkStashGuard();
@@ -1350,7 +1561,7 @@ var cli = meow(
1350
1561
  -r, --remote <name> \u6307\u5B9A\u8FDC\u7A0B\u4ED3\u5E93\u540D\u79F0
1351
1562
  -b, --branch <name> \u6307\u5B9A\u8FDC\u7A0B\u5206\u652F\u540D\u79F0
1352
1563
  -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
1564
+ -n, --count <number> \u663E\u793A commit \u6570\u91CF\uFF08\u9ED8\u8BA4 100\uFF09
1354
1565
  -m, --mainline \u5BF9 merge commit \u4F7F\u7528 -m 1
1355
1566
  -y, --yes \u8DF3\u8FC7\u786E\u8BA4\u76F4\u63A5\u6267\u884C
1356
1567
  --no-stash \u8DF3\u8FC7 stash \u63D0\u793A
@@ -1386,7 +1597,7 @@ var cli = meow(
1386
1597
  remote: { type: "string", shortFlag: "r" },
1387
1598
  branch: { type: "string", shortFlag: "b" },
1388
1599
  commits: { type: "string", shortFlag: "c" },
1389
- count: { type: "number", shortFlag: "n", default: 30 },
1600
+ count: { type: "number", shortFlag: "n", default: 100 },
1390
1601
  mainline: { type: "boolean", shortFlag: "m", default: false },
1391
1602
  yes: { type: "boolean", shortFlag: "y", default: false },
1392
1603
  noStash: { type: "boolean", default: false },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "git-sync-tui",
3
3
  "type": "module",
4
- "version": "0.1.4",
4
+ "version": "0.1.5",
5
5
  "packageManager": "pnpm@10.32.1",
6
6
  "description": "Interactive TUI tool for cross-repo git commit synchronization (cherry-pick --no-commit)",
7
7
  "author": "KiWi233333",