git-sync-tui 0.1.0 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +92 -34
  2. package/README.zh-CN.md +126 -0
  3. package/dist/cli.js +887 -196
  4. package/package.json +17 -3
package/dist/cli.js CHANGED
@@ -1,22 +1,142 @@
1
1
  #!/usr/bin/env node
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined") return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
2
8
 
3
9
  // src/cli.tsx
4
10
  import { render } from "ink";
5
11
  import meow from "meow";
6
12
 
7
13
  // src/app.tsx
8
- import { useState as useState5 } from "react";
9
- import { Box as Box6, Text as Text6 } from "ink";
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";
10
17
 
11
- // src/components/remote-select.tsx
18
+ // src/components/ui.tsx
19
+ import React from "react";
12
20
  import { Box, Text } from "ink";
13
- import { Select, Spinner } from "@inkjs/ui";
21
+ import { jsx, jsxs } from "react/jsx-runtime";
22
+ var STEP_LABELS = ["Remote", "Branch", "Commits", "Confirm", "Sync"];
23
+ function StepProgress({ current }) {
24
+ return /* @__PURE__ */ jsx(Box, { children: STEP_LABELS.map((label, i) => {
25
+ const step = i + 1;
26
+ const isActive = step === current;
27
+ const isDone = step < current;
28
+ const isLast = i === STEP_LABELS.length - 1;
29
+ return /* @__PURE__ */ jsxs(React.Fragment, { children: [
30
+ /* @__PURE__ */ jsxs(Text, { color: isActive ? "cyan" : isDone ? "green" : "gray", dimColor: !isActive && !isDone, children: [
31
+ isDone ? "\u25CF" : isActive ? "\u25C6" : "\u25CB",
32
+ " ",
33
+ isActive ? /* @__PURE__ */ jsx(Text, { bold: true, children: label }) : label
34
+ ] }),
35
+ !isLast && /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: " \u2500 " })
36
+ ] }, label);
37
+ }) });
38
+ }
39
+ function SectionHeader({ title, subtitle }) {
40
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
41
+ /* @__PURE__ */ jsxs(Text, { bold: true, color: "cyan", children: [
42
+ "\u25B8 ",
43
+ title
44
+ ] }),
45
+ subtitle && /* @__PURE__ */ jsxs(Text, { color: "gray", dimColor: true, children: [
46
+ " ",
47
+ subtitle
48
+ ] })
49
+ ] });
50
+ }
51
+ function KeyHints({ hints }) {
52
+ 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
+ " ",
58
+ label
59
+ ] })
60
+ ] }, key)) });
61
+ }
62
+ function InlineKeys({ hints }) {
63
+ return /* @__PURE__ */ jsx(Box, { gap: 1, children: hints.map(({ key, label }, i) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
64
+ /* @__PURE__ */ jsxs(Text, { color: "green", children: [
65
+ "[",
66
+ key,
67
+ "]"
68
+ ] }),
69
+ /* @__PURE__ */ jsxs(Text, { children: [
70
+ " ",
71
+ label
72
+ ] }),
73
+ i < hints.length - 1 && /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: " / " })
74
+ ] }, key)) });
75
+ }
76
+ var PANEL_CONFIG = {
77
+ info: { icon: "\u25C6", color: "cyan", borderColor: "cyan" },
78
+ warn: { icon: "\u25B2", color: "yellow", borderColor: "yellow" },
79
+ error: { icon: "\u2716", color: "red", borderColor: "red" },
80
+ success: { icon: "\u2714", color: "green", borderColor: "green" }
81
+ };
82
+ function StatusPanel({ type, title, children }) {
83
+ const { icon, color, borderColor } = PANEL_CONFIG[type];
84
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor, paddingX: 1, children: [
85
+ /* @__PURE__ */ jsxs(Text, { bold: true, color, children: [
86
+ icon,
87
+ " ",
88
+ title
89
+ ] }),
90
+ children
91
+ ] });
92
+ }
93
+ function AppHeader({ step, stashed }) {
94
+ 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" })
100
+ ] }),
101
+ /* @__PURE__ */ jsxs(Box, { children: [
102
+ /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: " " }),
103
+ /* @__PURE__ */ jsx(StepProgress, { current: step })
104
+ ] })
105
+ ] });
106
+ }
14
107
 
15
- // src/hooks/use-git.ts
16
- import { useState, useEffect, useCallback } from "react";
108
+ // src/components/stash-prompt.tsx
109
+ import { Box as Box2, Text as Text2, useInput } from "ink";
110
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
111
+ function StashPrompt({ onConfirm, onSkip }) {
112
+ useInput((input) => {
113
+ if (input === "y" || input === "Y") {
114
+ onConfirm();
115
+ } else if (input === "n" || input === "N") {
116
+ onSkip();
117
+ }
118
+ });
119
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", gap: 1, children: [
120
+ /* @__PURE__ */ jsx2(StatusPanel, { type: "warn", title: "\u5DE5\u4F5C\u533A\u6709\u672A\u63D0\u4EA4\u7684\u53D8\u66F4", children: /* @__PURE__ */ jsx2(Text2, { color: "gray", children: " Cherry-pick \u64CD\u4F5C\u53EF\u80FD\u4F1A\u4E0E\u672A\u63D0\u4EA4\u7684\u5185\u5BB9\u51B2\u7A81" }) }),
121
+ /* @__PURE__ */ jsxs2(Box2, { children: [
122
+ /* @__PURE__ */ jsx2(Text2, { bold: true, children: "\u81EA\u52A8 stash \u4FDD\u5B58\u5F53\u524D\u53D8\u66F4? " }),
123
+ /* @__PURE__ */ jsx2(InlineKeys, { hints: [
124
+ { key: "y", label: "\u662F" },
125
+ { key: "n", label: "\u5426\uFF0C\u7EE7\u7EED" }
126
+ ] })
127
+ ] })
128
+ ] });
129
+ }
130
+
131
+ // src/components/stash-recovery.tsx
132
+ import { useState, useEffect } from "react";
133
+ import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
134
+ import { Spinner } from "@inkjs/ui";
17
135
 
18
136
  // src/utils/git.ts
19
137
  import simpleGit from "simple-git";
138
+ import { existsSync, writeFileSync, unlinkSync, readFileSync } from "fs";
139
+ import { join } from "path";
20
140
  var gitInstance = null;
21
141
  function getGit(cwd) {
22
142
  if (!gitInstance || cwd) {
@@ -32,6 +152,10 @@ async function getRemotes() {
32
152
  fetchUrl: r.refs.fetch
33
153
  }));
34
154
  }
155
+ async function addRemote(name, url) {
156
+ const git = getGit();
157
+ await git.addRemote(name, url);
158
+ }
35
159
  async function getRemoteBranches(remote) {
36
160
  const git = getGit();
37
161
  try {
@@ -80,12 +204,29 @@ ${result.trim()}`);
80
204
  return "(\u65E0\u6CD5\u83B7\u53D6 stat \u4FE1\u606F)";
81
205
  }
82
206
  }
83
- async function cherryPick(hashes) {
207
+ async function hasMergeCommits(hashes) {
208
+ const git = getGit();
209
+ try {
210
+ for (const hash of hashes) {
211
+ const result = await git.raw(["rev-list", "--merges", "-n", "1", hash]);
212
+ if (result.trim()) return true;
213
+ }
214
+ return false;
215
+ } catch {
216
+ return false;
217
+ }
218
+ }
219
+ async function cherryPick(hashes, useMainline = false) {
84
220
  const git = getGit();
85
221
  try {
86
222
  const orderedHashes = [...hashes].reverse();
87
223
  for (const hash of orderedHashes) {
88
- await git.raw(["cherry-pick", "--no-commit", hash]);
224
+ const args = ["cherry-pick", "--no-commit"];
225
+ if (useMainline) {
226
+ args.push("-m", "1");
227
+ }
228
+ args.push(hash);
229
+ await git.raw(args);
89
230
  }
90
231
  return { success: true };
91
232
  } catch (err) {
@@ -111,10 +252,138 @@ async function getStagedStat() {
111
252
  return "";
112
253
  }
113
254
  }
255
+ async function isWorkingDirClean() {
256
+ const git = getGit();
257
+ const status = await git.status();
258
+ return status.isClean();
259
+ }
260
+ async function stash() {
261
+ const git = getGit();
262
+ try {
263
+ await git.stash(["push", "--include-untracked", "-m", "Auto-stash by git-sync-tui"]);
264
+ return true;
265
+ } catch {
266
+ return false;
267
+ }
268
+ }
269
+ async function stashPop() {
270
+ const git = getGit();
271
+ try {
272
+ await git.stash(["pop"]);
273
+ return true;
274
+ } catch {
275
+ return false;
276
+ }
277
+ }
278
+ var STASH_GUARD_FILE = "git-sync-tui-stash-guard";
279
+ async function getGitDir() {
280
+ const git = getGit();
281
+ const dir = await git.revparse(["--git-dir"]);
282
+ return dir.trim();
283
+ }
284
+ async function writeStashGuard() {
285
+ try {
286
+ const gitDir = await getGitDir();
287
+ writeFileSync(join(gitDir, STASH_GUARD_FILE), (/* @__PURE__ */ new Date()).toISOString(), "utf-8");
288
+ } catch {
289
+ }
290
+ }
291
+ async function removeStashGuard() {
292
+ try {
293
+ const gitDir = await getGitDir();
294
+ const guardPath = join(gitDir, STASH_GUARD_FILE);
295
+ if (existsSync(guardPath)) {
296
+ unlinkSync(guardPath);
297
+ }
298
+ } catch {
299
+ }
300
+ }
301
+ function removeStashGuardSync() {
302
+ try {
303
+ const { execSync: execSync2 } = __require("child_process");
304
+ const gitDir = String(execSync2("git rev-parse --git-dir", { encoding: "utf-8" })).trim();
305
+ const guardPath = join(gitDir, STASH_GUARD_FILE);
306
+ if (existsSync(guardPath)) {
307
+ unlinkSync(guardPath);
308
+ }
309
+ } catch {
310
+ }
311
+ }
312
+ async function checkStashGuard() {
313
+ try {
314
+ const gitDir = await getGitDir();
315
+ const guardPath = join(gitDir, STASH_GUARD_FILE);
316
+ if (existsSync(guardPath)) {
317
+ const timestamp = readFileSync(guardPath, "utf-8").trim();
318
+ return { exists: true, timestamp };
319
+ }
320
+ } catch {
321
+ }
322
+ return { exists: false };
323
+ }
324
+ async function findStashEntry() {
325
+ const git = getGit();
326
+ try {
327
+ const result = await git.stash(["list"]);
328
+ const lines = result.trim().split("\n");
329
+ for (const line of lines) {
330
+ if (line.includes("Auto-stash by git-sync-tui")) {
331
+ return line;
332
+ }
333
+ }
334
+ } catch {
335
+ }
336
+ return null;
337
+ }
338
+
339
+ // src/components/stash-recovery.tsx
340
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
341
+ function StashRecovery({ timestamp, onRecover, onSkip }) {
342
+ const [stashEntry, setStashEntry] = useState(void 0);
343
+ useEffect(() => {
344
+ findStashEntry().then(setStashEntry);
345
+ }, []);
346
+ useInput2((input) => {
347
+ if (input === "y" || input === "Y") {
348
+ onRecover();
349
+ } else if (input === "n" || input === "N") {
350
+ onSkip();
351
+ }
352
+ });
353
+ if (stashEntry === void 0) {
354
+ return /* @__PURE__ */ jsx3(Spinner, { label: "\u68C0\u67E5 stash \u8BB0\u5F55..." });
355
+ }
356
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", gap: 1, children: [
357
+ /* @__PURE__ */ jsxs3(StatusPanel, { type: "warn", title: "\u68C0\u6D4B\u5230\u4E0A\u6B21\u8FD0\u884C\u4E2D\u65AD", children: [
358
+ timestamp && /* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
359
+ " \u4E2D\u65AD\u65F6\u95F4: ",
360
+ timestamp
361
+ ] }),
362
+ stashEntry && /* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
363
+ " Stash: ",
364
+ stashEntry
365
+ ] }),
366
+ !stashEntry && /* @__PURE__ */ jsx3(Text3, { color: "red", children: " \u672A\u627E\u5230\u5BF9\u5E94\u7684 stash \u6761\u76EE\uFF08\u53EF\u80FD\u5DF2\u624B\u52A8\u6062\u590D\uFF09" })
367
+ ] }),
368
+ /* @__PURE__ */ jsxs3(Box3, { children: [
369
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: stashEntry ? "\u6062\u590D stash? " : "\u6E05\u9664\u4E2D\u65AD\u6807\u8BB0? " }),
370
+ /* @__PURE__ */ jsx3(InlineKeys, { hints: [
371
+ { key: "y", label: "\u662F" },
372
+ { key: "n", label: "\u8DF3\u8FC7" }
373
+ ] })
374
+ ] })
375
+ ] });
376
+ }
377
+
378
+ // src/components/remote-select.tsx
379
+ import { useState as useState3 } from "react";
380
+ import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
381
+ import { Select, Spinner as Spinner2, TextInput } from "@inkjs/ui";
114
382
 
115
383
  // src/hooks/use-git.ts
384
+ import { useState as useState2, useEffect as useEffect2, useCallback } from "react";
116
385
  function useAsync(fn, deps = []) {
117
- const [state, setState] = useState({
386
+ const [state, setState] = useState2({
118
387
  data: null,
119
388
  loading: true,
120
389
  error: null
@@ -128,7 +397,7 @@ function useAsync(fn, deps = []) {
128
397
  setState({ data: null, loading: false, error: err.message });
129
398
  }
130
399
  }, deps);
131
- useEffect(() => {
400
+ useEffect2(() => {
132
401
  load();
133
402
  }, [load]);
134
403
  return { ...state, reload: load };
@@ -149,9 +418,9 @@ function useCommits(remote, branch, count = 30) {
149
418
  );
150
419
  }
151
420
  function useCommitStat(hashes) {
152
- const [stat, setStat] = useState("");
153
- const [loading, setLoading] = useState(false);
154
- useEffect(() => {
421
+ const [stat, setStat] = useState2("");
422
+ const [loading, setLoading] = useState2(false);
423
+ useEffect2(() => {
155
424
  if (hashes.length === 0) {
156
425
  setStat("");
157
426
  return;
@@ -169,310 +438,729 @@ function useCommitStat(hashes) {
169
438
  }
170
439
 
171
440
  // src/components/remote-select.tsx
172
- import { jsx, jsxs } from "react/jsx-runtime";
173
- function RemoteSelect({ onSelect }) {
174
- const { data: remotes, loading, error } = useRemotes();
441
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
442
+ function extractRemoteName(url) {
443
+ const trimmed = url.trim().replace(/\/+$/, "").replace(/\.git$/, "");
444
+ const lastSegment = trimmed.split(/[/\\:]+/).filter(Boolean).pop() || "";
445
+ return lastSegment;
446
+ }
447
+ function RemoteSelect({ onSelect, onBack }) {
448
+ const { data: remotes, loading, error, reload } = useRemotes();
449
+ const [phase, setPhase] = useState3("list");
450
+ const [customUrl, setCustomUrl] = useState3("");
451
+ const [addError, setAddError] = useState3(null);
452
+ useInput3((_input, key) => {
453
+ if (key.escape) {
454
+ if (phase === "input-name") {
455
+ setPhase("input-url");
456
+ } else if (phase === "input-url") {
457
+ setPhase("list");
458
+ } else if (phase === "list") {
459
+ onBack?.();
460
+ }
461
+ }
462
+ });
175
463
  if (loading) {
176
- return /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Spinner, { label: "\u6B63\u5728\u83B7\u53D6\u8FDC\u7A0B\u4ED3\u5E93\u5217\u8868..." }) });
464
+ return /* @__PURE__ */ jsx4(Spinner2, { label: "\u83B7\u53D6\u8FDC\u7A0B\u4ED3\u5E93..." });
177
465
  }
178
466
  if (error) {
179
- return /* @__PURE__ */ jsxs(Text, { color: "red", children: [
180
- "\u83B7\u53D6\u8FDC\u7A0B\u4ED3\u5E93\u5931\u8D25: ",
467
+ return /* @__PURE__ */ jsxs4(Text4, { color: "red", children: [
468
+ "\u2716 \u83B7\u53D6\u8FDC\u7A0B\u4ED3\u5E93\u5931\u8D25: ",
181
469
  error
182
470
  ] });
183
471
  }
184
- if (!remotes || remotes.length === 0) {
185
- return /* @__PURE__ */ jsx(Text, { color: "red", children: "\u672A\u627E\u5230\u4EFB\u4F55\u8FDC\u7A0B\u4ED3\u5E93\uFF0C\u8BF7\u5148 git remote add" });
472
+ if (phase === "adding") {
473
+ return /* @__PURE__ */ jsx4(Spinner2, { label: "\u6DFB\u52A0\u8FDC\u7A0B\u4ED3\u5E93..." });
186
474
  }
187
- const options = remotes.map((r) => ({
188
- label: `${r.name} ${r.fetchUrl}`,
189
- value: r.name
190
- }));
191
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", gap: 1, children: [
192
- /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "[1/5] \u9009\u62E9\u8FDC\u7A0B\u4ED3\u5E93" }),
193
- /* @__PURE__ */ jsx(Select, { options, onChange: onSelect })
475
+ if (phase === "input-url") {
476
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", gap: 1, children: [
477
+ /* @__PURE__ */ jsx4(SectionHeader, { title: "\u6DFB\u52A0\u8FDC\u7A0B\u4ED3\u5E93" }),
478
+ addError && /* @__PURE__ */ jsxs4(Text4, { color: "red", children: [
479
+ "\u2716 ",
480
+ addError
481
+ ] }),
482
+ /* @__PURE__ */ jsxs4(Box4, { children: [
483
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", children: "URL \u25B8 " }),
484
+ /* @__PURE__ */ jsx4(
485
+ TextInput,
486
+ {
487
+ placeholder: "https://github.com/user/repo.git",
488
+ onSubmit: (url) => {
489
+ if (!url.trim()) {
490
+ setAddError("\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A");
491
+ return;
492
+ }
493
+ setCustomUrl(url.trim());
494
+ setPhase("input-name");
495
+ }
496
+ }
497
+ )
498
+ ] }),
499
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", dimColor: true, children: " \u652F\u6301 HTTPS / SSH \u5730\u5740" })
500
+ ] });
501
+ }
502
+ if (phase === "input-name") {
503
+ const defaultName = extractRemoteName(customUrl);
504
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", gap: 1, children: [
505
+ /* @__PURE__ */ jsx4(SectionHeader, { title: "\u6DFB\u52A0\u8FDC\u7A0B\u4ED3\u5E93", subtitle: customUrl }),
506
+ addError && /* @__PURE__ */ jsxs4(Text4, { color: "red", children: [
507
+ "\u2716 ",
508
+ addError
509
+ ] }),
510
+ /* @__PURE__ */ jsxs4(Box4, { children: [
511
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", children: "\u540D\u79F0 \u25B8 " }),
512
+ /* @__PURE__ */ jsx4(
513
+ TextInput,
514
+ {
515
+ placeholder: defaultName || "upstream",
516
+ defaultValue: defaultName,
517
+ onSubmit: async (name) => {
518
+ const remoteName = name.trim();
519
+ if (!remoteName) {
520
+ setAddError("\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A");
521
+ return;
522
+ }
523
+ if (remotes?.some((r) => r.name === remoteName)) {
524
+ setAddError(`\u8FDC\u7A0B "${remoteName}" \u5DF2\u5B58\u5728`);
525
+ return;
526
+ }
527
+ setAddError(null);
528
+ setPhase("adding");
529
+ try {
530
+ await addRemote(remoteName, customUrl);
531
+ reload();
532
+ onSelect(remoteName);
533
+ } catch (err) {
534
+ setAddError(err.message);
535
+ setPhase("input-name");
536
+ }
537
+ }
538
+ }
539
+ )
540
+ ] })
541
+ ] });
542
+ }
543
+ const options = [
544
+ ...(remotes || []).map((r) => ({
545
+ label: `${r.name} ${r.fetchUrl}`,
546
+ value: r.name
547
+ })),
548
+ {
549
+ label: "+ \u6DFB\u52A0\u8FDC\u7A0B\u4ED3\u5E93...",
550
+ value: "__add_custom__"
551
+ }
552
+ ];
553
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", gap: 1, children: [
554
+ /* @__PURE__ */ jsx4(SectionHeader, { title: "\u9009\u62E9\u8FDC\u7A0B\u4ED3\u5E93" }),
555
+ /* @__PURE__ */ jsx4(
556
+ Select,
557
+ {
558
+ options,
559
+ onChange: (value) => {
560
+ if (value === "__add_custom__") {
561
+ setPhase("input-url");
562
+ } else {
563
+ onSelect(value);
564
+ }
565
+ }
566
+ }
567
+ )
194
568
  ] });
195
569
  }
196
570
 
197
571
  // src/components/branch-select.tsx
198
- import { useState as useState2, useMemo } from "react";
199
- import { Box as Box2, Text as Text2 } from "ink";
200
- import { Select as Select2, Spinner as Spinner2, TextInput } from "@inkjs/ui";
201
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
202
- function BranchSelect({ remote, onSelect }) {
572
+ import { useState as useState4, useMemo } from "react";
573
+ import { Box as Box5, Text as Text5, useInput as useInput4 } from "ink";
574
+ import { Select as Select2, Spinner as Spinner3, TextInput as TextInput2 } from "@inkjs/ui";
575
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
576
+ function BranchSelect({ remote, onSelect, onBack }) {
203
577
  const { data: branches, loading, error } = useBranches(remote);
204
- const [filter, setFilter] = useState2("");
578
+ const [filter, setFilter] = useState4("");
579
+ useInput4((_input, key) => {
580
+ if (key.escape) onBack?.();
581
+ });
205
582
  const filteredOptions = useMemo(() => {
206
583
  if (!branches) return [];
207
584
  const filtered = filter ? branches.filter((b) => b.toLowerCase().includes(filter.toLowerCase())) : branches;
208
585
  return filtered.map((b) => ({ label: b, value: b }));
209
586
  }, [branches, filter]);
210
587
  if (loading) {
211
- return /* @__PURE__ */ jsx2(Box2, { children: /* @__PURE__ */ jsx2(Spinner2, { label: `\u6B63\u5728\u83B7\u53D6 ${remote} \u7684\u5206\u652F\u5217\u8868...` }) });
588
+ return /* @__PURE__ */ jsx5(Spinner3, { label: `\u83B7\u53D6 ${remote} \u7684\u5206\u652F\u5217\u8868...` });
212
589
  }
213
590
  if (error) {
214
- return /* @__PURE__ */ jsxs2(Text2, { color: "red", children: [
215
- "\u83B7\u53D6\u5206\u652F\u5217\u8868\u5931\u8D25: ",
591
+ return /* @__PURE__ */ jsxs5(Text5, { color: "red", children: [
592
+ "\u2716 \u83B7\u53D6\u5206\u652F\u5217\u8868\u5931\u8D25: ",
216
593
  error
217
594
  ] });
218
595
  }
219
596
  if (!branches || branches.length === 0) {
220
- return /* @__PURE__ */ jsx2(Text2, { color: "red", children: "\u672A\u627E\u5230\u8FDC\u7A0B\u5206\u652F" });
597
+ return /* @__PURE__ */ jsx5(Text5, { color: "red", children: "\u2716 \u672A\u627E\u5230\u8FDC\u7A0B\u5206\u652F" });
221
598
  }
222
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", gap: 1, children: [
223
- /* @__PURE__ */ jsxs2(Text2, { bold: true, color: "cyan", children: [
224
- "[2/5] \u9009\u62E9\u5206\u652F (",
225
- remote,
226
- ")"
227
- ] }),
228
- /* @__PURE__ */ jsxs2(Box2, { children: [
229
- /* @__PURE__ */ jsx2(Text2, { color: "gray", children: "\u641C\u7D22: " }),
230
- /* @__PURE__ */ jsx2(
231
- TextInput,
599
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", gap: 1, children: [
600
+ /* @__PURE__ */ jsx5(SectionHeader, { title: `\u9009\u62E9\u5206\u652F`, subtitle: `${remote} \xB7 ${branches.length} \u4E2A\u5206\u652F` }),
601
+ /* @__PURE__ */ jsxs5(Box5, { children: [
602
+ /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "/ " }),
603
+ /* @__PURE__ */ jsx5(
604
+ TextInput2,
232
605
  {
233
- placeholder: "\u8F93\u5165\u5173\u952E\u5B57\u8FC7\u6EE4\u5206\u652F...",
606
+ placeholder: "\u8F93\u5165\u5173\u952E\u5B57\u8FC7\u6EE4...",
234
607
  onChange: setFilter
235
608
  }
236
- )
609
+ ),
610
+ filter && /* @__PURE__ */ jsxs5(Text5, { color: "gray", dimColor: true, children: [
611
+ " \xB7 \u5339\u914D ",
612
+ filteredOptions.length
613
+ ] })
237
614
  ] }),
238
- /* @__PURE__ */ jsxs2(Text2, { color: "gray", dimColor: true, children: [
239
- "\u5171 ",
240
- branches.length,
241
- " \u4E2A\u5206\u652F",
242
- filter ? `\uFF0C\u5339\u914D ${filteredOptions.length} \u4E2A` : ""
243
- ] }),
244
- filteredOptions.length > 0 ? /* @__PURE__ */ jsx2(Select2, { options: filteredOptions, onChange: onSelect }) : /* @__PURE__ */ jsx2(Text2, { color: "yellow", children: "\u65E0\u5339\u914D\u5206\u652F" })
615
+ filteredOptions.length > 0 ? /* @__PURE__ */ jsx5(Select2, { options: filteredOptions, onChange: onSelect }) : /* @__PURE__ */ jsx5(Text5, { color: "yellow", children: "\u25B2 \u65E0\u5339\u914D\u5206\u652F" })
245
616
  ] });
246
617
  }
247
618
 
248
619
  // src/components/commit-list.tsx
249
- import { useState as useState3 } from "react";
250
- import { Box as Box3, Text as Text3 } from "ink";
251
- import { MultiSelect, Spinner as Spinner3 } from "@inkjs/ui";
252
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
253
- function CommitList({ remote, branch, onSelect }) {
620
+ import { useState as useState5, useMemo as useMemo2, useRef } from "react";
621
+ import { Box as Box6, Text as Text6, useInput as useInput5 } from "ink";
622
+ import { Spinner as Spinner4 } from "@inkjs/ui";
623
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
624
+ function CommitList({ remote, branch, onSelect, onBack }) {
254
625
  const { data: commits, loading, error } = useCommits(remote, branch, 30);
255
- const [selectedHashes, setSelectedHashes] = useState3([]);
256
- const { stat, loading: statLoading } = useCommitStat(selectedHashes);
626
+ const [selectedIndex, setSelectedIndex] = useState5(0);
627
+ const [selectedHashes, setSelectedHashes] = useState5(/* @__PURE__ */ new Set());
628
+ const [shiftMode, setShiftMode] = useState5(false);
629
+ const anchorIndexRef = useRef(null);
630
+ const selectedKey = useMemo2(() => Array.from(selectedHashes).sort().join(","), [selectedHashes]);
631
+ const selectedArray = useMemo2(() => Array.from(selectedHashes), [selectedKey]);
632
+ const { stat, loading: statLoading } = useCommitStat(selectedArray);
633
+ const toggleCurrent = () => {
634
+ if (!commits || commits.length === 0) return;
635
+ const hash = commits[selectedIndex].hash;
636
+ setSelectedHashes((prev) => {
637
+ const next = new Set(prev);
638
+ if (next.has(hash)) {
639
+ next.delete(hash);
640
+ anchorIndexRef.current = null;
641
+ } else {
642
+ next.add(hash);
643
+ anchorIndexRef.current = selectedIndex;
644
+ }
645
+ return next;
646
+ });
647
+ };
648
+ const selectRange = (anchor, current) => {
649
+ if (!commits) return;
650
+ const start = Math.min(anchor, current);
651
+ const end = Math.max(anchor, current);
652
+ setSelectedHashes((prev) => {
653
+ const next = new Set(prev);
654
+ for (let i = start; i <= end; i++) {
655
+ next.add(commits[i].hash);
656
+ }
657
+ return next;
658
+ });
659
+ };
660
+ const toggleAll = () => {
661
+ if (!commits || commits.length === 0) return;
662
+ setSelectedHashes((prev) => {
663
+ if (prev.size === commits.length) {
664
+ anchorIndexRef.current = null;
665
+ return /* @__PURE__ */ new Set();
666
+ }
667
+ return new Set(commits.map((c) => c.hash));
668
+ });
669
+ };
670
+ const invertSelection = () => {
671
+ if (!commits || commits.length === 0) return;
672
+ setSelectedHashes((prev) => {
673
+ const next = /* @__PURE__ */ new Set();
674
+ for (const c of commits) {
675
+ if (!prev.has(c.hash)) next.add(c.hash);
676
+ }
677
+ return next;
678
+ });
679
+ };
680
+ const selectToCurrent = () => {
681
+ if (!commits || commits.length === 0) return;
682
+ setSelectedHashes((prev) => {
683
+ const next = new Set(prev);
684
+ for (let i = 0; i <= selectedIndex; i++) {
685
+ next.add(commits[i].hash);
686
+ }
687
+ return next;
688
+ });
689
+ };
690
+ useInput5((input, key) => {
691
+ if (!commits || commits.length === 0) return;
692
+ if (key.shift) {
693
+ if (!shiftMode) {
694
+ setShiftMode(true);
695
+ if (anchorIndexRef.current === null) {
696
+ anchorIndexRef.current = selectedIndex;
697
+ }
698
+ }
699
+ if (key.upArrow) {
700
+ const newIndex = Math.max(0, selectedIndex - 1);
701
+ setSelectedIndex(newIndex);
702
+ selectRange(anchorIndexRef.current, newIndex);
703
+ } else if (key.downArrow) {
704
+ const newIndex = Math.min(commits.length - 1, selectedIndex + 1);
705
+ setSelectedIndex(newIndex);
706
+ selectRange(anchorIndexRef.current, newIndex);
707
+ } else if (input === " ") {
708
+ if (anchorIndexRef.current !== null) {
709
+ selectRange(anchorIndexRef.current, selectedIndex);
710
+ } else {
711
+ toggleCurrent();
712
+ }
713
+ }
714
+ return;
715
+ }
716
+ if (shiftMode) setShiftMode(false);
717
+ if (key.upArrow) {
718
+ setSelectedIndex((prev) => Math.max(0, prev - 1));
719
+ } else if (key.downArrow) {
720
+ setSelectedIndex((prev) => Math.min(commits.length - 1, prev + 1));
721
+ } else if (input === " ") {
722
+ toggleCurrent();
723
+ } else if (input === "a" || input === "A") {
724
+ toggleAll();
725
+ } else if (input === "i" || input === "I") {
726
+ invertSelection();
727
+ } else if (input === "r" || input === "R") {
728
+ selectToCurrent();
729
+ } else if (key.escape) {
730
+ onBack?.();
731
+ } else if (key.return) {
732
+ if (selectedHashes.size > 0) {
733
+ onSelect(Array.from(selectedHashes), commits);
734
+ }
735
+ }
736
+ });
257
737
  if (loading) {
258
- return /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsx3(Spinner3, { label: `\u6B63\u5728\u83B7\u53D6 ${remote}/${branch} \u7684 commit \u5217\u8868...` }) });
738
+ return /* @__PURE__ */ jsx6(Spinner4, { label: `\u83B7\u53D6 ${remote}/${branch} \u7684 commit \u5217\u8868...` });
259
739
  }
260
740
  if (error) {
261
- return /* @__PURE__ */ jsxs3(Text3, { color: "red", children: [
262
- "\u83B7\u53D6 commit \u5217\u8868\u5931\u8D25: ",
741
+ return /* @__PURE__ */ jsxs6(Text6, { color: "red", children: [
742
+ "\u2716 \u83B7\u53D6 commit \u5217\u8868\u5931\u8D25: ",
263
743
  error
264
744
  ] });
265
745
  }
266
746
  if (!commits || commits.length === 0) {
267
- return /* @__PURE__ */ jsx3(Text3, { color: "yellow", children: "\u8BE5\u5206\u652F\u6CA1\u6709 commit" });
747
+ return /* @__PURE__ */ jsx6(Text6, { color: "yellow", children: "\u25B2 \u8BE5\u5206\u652F\u6CA1\u6709 commit" });
268
748
  }
269
- const options = commits.map((c) => ({
270
- label: `${c.shortHash} ${c.message} (${c.author}, ${c.date})`,
271
- value: c.hash
272
- }));
273
- return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", gap: 1, children: [
274
- /* @__PURE__ */ jsx3(Text3, { bold: true, color: "cyan", children: "[3/5] \u9009\u62E9\u8981\u540C\u6B65\u7684 commit (Space \u9009\u62E9, Enter \u786E\u8BA4)" }),
275
- /* @__PURE__ */ jsxs3(Text3, { color: "gray", dimColor: true, children: [
276
- remote,
277
- "/",
278
- branch,
279
- " \u6700\u8FD1 ",
280
- commits.length,
281
- " \u4E2A commit"
282
- ] }),
283
- /* @__PURE__ */ jsx3(
284
- MultiSelect,
285
- {
286
- options,
287
- onChange: setSelectedHashes,
288
- onSubmit: (hashes) => {
289
- if (hashes.length > 0) {
290
- onSelect(hashes, commits);
291
- }
292
- }
293
- }
294
- ),
295
- selectedHashes.length > 0 && /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: [
296
- /* @__PURE__ */ jsxs3(Text3, { bold: true, color: "yellow", children: [
749
+ const visibleCount = 10;
750
+ const startIdx = Math.max(0, Math.min(selectedIndex - Math.floor(visibleCount / 2), commits.length - visibleCount));
751
+ const visibleCommits = commits.slice(startIdx, startIdx + visibleCount);
752
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", gap: 1, children: [
753
+ /* @__PURE__ */ jsx6(SectionHeader, { title: "\u9009\u62E9\u8981\u540C\u6B65\u7684 commit" }),
754
+ /* @__PURE__ */ jsxs6(Box6, { gap: 2, children: [
755
+ /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
756
+ remote,
757
+ "/",
758
+ branch
759
+ ] }),
760
+ /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
761
+ commits.length,
762
+ " commits"
763
+ ] }),
764
+ /* @__PURE__ */ jsxs6(Text6, { color: selectedHashes.size > 0 ? "cyan" : "gray", bold: selectedHashes.size > 0, children: [
297
765
  "\u5DF2\u9009 ",
298
- selectedHashes.length,
299
- " \u4E2A commit \u2014 diff --stat \u9884\u89C8:"
766
+ selectedHashes.size
300
767
  ] }),
301
- statLoading ? /* @__PURE__ */ jsx3(Spinner3, { label: "\u52A0\u8F7D\u4E2D..." }) : /* @__PURE__ */ jsx3(Text3, { color: "gray", children: stat || "(\u65E0\u53D8\u66F4)" })
302
- ] })
768
+ shiftMode && /* @__PURE__ */ jsx6(Text6, { color: "yellow", bold: true, children: "SHIFT" })
769
+ ] }),
770
+ /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
771
+ startIdx > 0 && /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
772
+ " \u2191 ",
773
+ startIdx,
774
+ " more"
775
+ ] }),
776
+ visibleCommits.map((c, i) => {
777
+ const actualIdx = startIdx + i;
778
+ const isSelected = selectedHashes.has(c.hash);
779
+ const isCursor = actualIdx === selectedIndex;
780
+ const isAnchor = actualIdx === anchorIndexRef.current;
781
+ return /* @__PURE__ */ jsxs6(Box6, { children: [
782
+ /* @__PURE__ */ jsxs6(Text6, { backgroundColor: isCursor ? "blue" : void 0, color: isSelected ? "green" : "white", children: [
783
+ isCursor ? "\u25B8 " : " ",
784
+ isAnchor ? "\u2693" : isSelected ? "\u25CF" : "\u25CB",
785
+ " "
786
+ ] }),
787
+ /* @__PURE__ */ jsx6(Text6, { backgroundColor: isCursor ? "blue" : void 0, color: "yellow", children: c.shortHash }),
788
+ /* @__PURE__ */ jsxs6(Text6, { backgroundColor: isCursor ? "blue" : void 0, color: isSelected ? "green" : "white", children: [
789
+ " ",
790
+ c.message
791
+ ] }),
792
+ /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
793
+ " ",
794
+ c.author,
795
+ " \xB7 ",
796
+ c.date
797
+ ] })
798
+ ] }, c.hash);
799
+ }),
800
+ startIdx + visibleCount < commits.length && /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
801
+ " \u2193 ",
802
+ commits.length - startIdx - visibleCount,
803
+ " more"
804
+ ] })
805
+ ] }),
806
+ /* @__PURE__ */ jsx6(KeyHints, { hints: [
807
+ { key: "\u2191\u2193", label: "\u5BFC\u822A" },
808
+ { key: "Space", label: "\u9009\u62E9" },
809
+ { key: "a", label: "\u5168\u9009" },
810
+ { key: "i", label: "\u53CD\u9009" },
811
+ { key: "r", label: "\u9009\u81F3\u5F00\u5934" },
812
+ { key: "Shift+\u2191\u2193", label: "\u8FDE\u9009" },
813
+ { key: "Enter", label: "\u786E\u8BA4" },
814
+ { key: "Esc", label: "\u8FD4\u56DE" }
815
+ ] }),
816
+ 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)" }) })
303
817
  ] });
304
818
  }
305
819
 
306
820
  // src/components/confirm-panel.tsx
307
- import { Box as Box4, Text as Text4, useInput } from "ink";
308
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
309
- function ConfirmPanel({ commits, selectedHashes, onConfirm, onCancel }) {
310
- useInput((input) => {
311
- if (input === "y" || input === "Y") {
821
+ import { Box as Box7, Text as Text7, useInput as useInput6 } from "ink";
822
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
823
+ function ConfirmPanel({ commits, selectedHashes, hasMerge, useMainline, onToggleMainline, onConfirm, onCancel }) {
824
+ useInput6((input, key) => {
825
+ if (key.escape) {
826
+ onCancel();
827
+ } else if (input === "y" || input === "Y") {
312
828
  onConfirm();
313
829
  } else if (input === "n" || input === "N" || input === "q") {
314
830
  onCancel();
831
+ } else if (hasMerge && (input === "m" || input === "M")) {
832
+ onToggleMainline();
315
833
  }
316
834
  });
317
835
  const selectedCommits = selectedHashes.map((hash) => commits.find((c) => c.hash === hash)).filter(Boolean);
318
- return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", gap: 1, children: [
319
- /* @__PURE__ */ jsx4(Text4, { bold: true, color: "cyan", children: "[4/5] \u786E\u8BA4\u6267\u884C" }),
320
- /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
321
- /* @__PURE__ */ jsxs4(Text4, { bold: true, children: [
322
- "\u5C06 cherry-pick --no-commit \u4EE5\u4E0B ",
323
- selectedCommits.length,
324
- " \u4E2A commit:"
836
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", gap: 1, children: [
837
+ /* @__PURE__ */ jsx7(SectionHeader, { title: "\u786E\u8BA4\u6267\u884C" }),
838
+ /* @__PURE__ */ jsx7(StatusPanel, { type: "info", title: `cherry-pick --no-commit \xB7 ${selectedCommits.length} \u4E2A commit`, children: selectedCommits.map((c) => /* @__PURE__ */ jsxs7(Box7, { children: [
839
+ /* @__PURE__ */ jsxs7(Text7, { color: "yellow", children: [
840
+ " ",
841
+ c.shortHash
325
842
  ] }),
326
- selectedCommits.map((c) => /* @__PURE__ */ jsxs4(Text4, { children: [
327
- /* @__PURE__ */ jsxs4(Text4, { color: "green", children: [
328
- " ",
329
- c.shortHash
330
- ] }),
331
- /* @__PURE__ */ jsxs4(Text4, { children: [
332
- " ",
333
- c.message
334
- ] }),
335
- /* @__PURE__ */ jsxs4(Text4, { color: "gray", dimColor: true, children: [
336
- " (",
337
- c.author,
338
- ")"
339
- ] })
340
- ] }, c.hash))
843
+ /* @__PURE__ */ jsxs7(Text7, { children: [
844
+ " ",
845
+ c.message
846
+ ] }),
847
+ /* @__PURE__ */ jsxs7(Text7, { color: "gray", dimColor: true, children: [
848
+ " ",
849
+ c.author
850
+ ] })
851
+ ] }, c.hash)) }),
852
+ hasMerge && /* @__PURE__ */ jsxs7(StatusPanel, { type: "warn", title: "\u68C0\u6D4B\u5230 Merge Commit", children: [
853
+ /* @__PURE__ */ jsx7(Text7, { color: "gray", children: " Cherry-pick \u5408\u5E76\u63D0\u4EA4\u9700\u8981\u6307\u5B9A\u7236\u8282\u70B9 (-m 1)" }),
854
+ /* @__PURE__ */ jsxs7(Box7, { children: [
855
+ /* @__PURE__ */ jsx7(Text7, { children: " " }),
856
+ /* @__PURE__ */ jsx7(Text7, { color: "cyan", children: "[m]" }),
857
+ /* @__PURE__ */ jsx7(Text7, { children: " \u5207\u6362 -m 1: " }),
858
+ useMainline ? /* @__PURE__ */ jsx7(Text7, { color: "green", bold: true, children: "\u5DF2\u542F\u7528" }) : /* @__PURE__ */ jsx7(Text7, { color: "gray", children: "\u672A\u542F\u7528" })
859
+ ] })
341
860
  ] }),
342
- /* @__PURE__ */ jsxs4(Box4, { children: [
343
- /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: "\u26A0 " }),
344
- /* @__PURE__ */ jsx4(Text4, { children: "\u4F7F\u7528 --no-commit \u6A21\u5F0F\uFF0C\u6539\u52A8\u5C06\u6682\u5B58\u5230\u5DE5\u4F5C\u533A\uFF0C\u9700\u624B\u52A8 commit" })
861
+ /* @__PURE__ */ jsxs7(Box7, { children: [
862
+ /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: "\u25B2 " }),
863
+ /* @__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" })
345
864
  ] }),
346
- /* @__PURE__ */ jsxs4(Box4, { children: [
347
- /* @__PURE__ */ jsx4(Text4, { bold: true, children: "\u786E\u8BA4\u6267\u884C? " }),
348
- /* @__PURE__ */ jsx4(Text4, { color: "green", children: "[y]" }),
349
- /* @__PURE__ */ jsx4(Text4, { children: " \u786E\u8BA4 / " }),
350
- /* @__PURE__ */ jsx4(Text4, { color: "red", children: "[n]" }),
351
- /* @__PURE__ */ jsx4(Text4, { children: " \u53D6\u6D88" })
865
+ /* @__PURE__ */ jsxs7(Box7, { children: [
866
+ /* @__PURE__ */ jsx7(Text7, { bold: true, children: "\u786E\u8BA4\u6267\u884C? " }),
867
+ /* @__PURE__ */ jsx7(InlineKeys, { hints: [
868
+ { key: "y", label: "\u786E\u8BA4" },
869
+ { key: "n", label: "\u53D6\u6D88" },
870
+ ...hasMerge ? [{ key: "m", label: "\u5207\u6362 -m 1" }] : [],
871
+ { key: "Esc", label: "\u8FD4\u56DE" }
872
+ ] })
352
873
  ] })
353
874
  ] });
354
875
  }
355
876
 
356
877
  // src/components/result-panel.tsx
357
- import { useState as useState4, useEffect as useEffect3 } from "react";
358
- import { Box as Box5, Text as Text5 } from "ink";
359
- import { Spinner as Spinner4 } from "@inkjs/ui";
360
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
361
- function ResultPanel({ selectedHashes, onDone }) {
362
- const [phase, setPhase] = useState4("executing");
363
- const [result, setResult] = useState4(null);
364
- const [stagedStat, setStagedStat] = useState4("");
878
+ import { useState as useState6, useEffect as useEffect3 } from "react";
879
+ import { Box as Box8, Text as Text8 } from "ink";
880
+ import { Spinner as Spinner5 } from "@inkjs/ui";
881
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
882
+ function ResultPanel({ selectedHashes, useMainline, stashed, onStashRestored, onDone }) {
883
+ const [phase, setPhase] = useState6("executing");
884
+ const [result, setResult] = useState6(null);
885
+ const [stagedStat, setStagedStat] = useState6("");
886
+ const [stashRestored, setStashRestored] = useState6(null);
887
+ const tryRestoreStash = async () => {
888
+ if (!stashed) return true;
889
+ setPhase("restoring");
890
+ const ok = await stashPop();
891
+ setStashRestored(ok);
892
+ if (ok) onStashRestored();
893
+ return ok;
894
+ };
365
895
  useEffect3(() => {
366
896
  async function run() {
367
- const res = await cherryPick(selectedHashes);
897
+ const res = await cherryPick(selectedHashes, useMainline);
368
898
  setResult(res);
369
899
  if (res.success) {
370
900
  const stat = await getStagedStat();
371
901
  setStagedStat(stat);
902
+ await tryRestoreStash();
372
903
  setPhase("done");
373
904
  } else {
905
+ await tryRestoreStash();
374
906
  setPhase("error");
375
907
  }
376
908
  }
377
909
  run();
378
910
  }, []);
379
911
  if (phase === "executing") {
380
- return /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(Spinner4, { label: `\u6B63\u5728\u6267\u884C cherry-pick --no-commit (${selectedHashes.length} \u4E2A commit)...` }) });
912
+ return /* @__PURE__ */ jsx8(Spinner5, { label: `cherry-pick --no-commit (${selectedHashes.length} \u4E2A commit)...` });
913
+ }
914
+ if (phase === "restoring") {
915
+ return /* @__PURE__ */ jsx8(Spinner5, { label: "\u6062\u590D\u5DE5\u4F5C\u533A (git stash pop)..." });
381
916
  }
382
917
  if (phase === "error" && result) {
383
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", gap: 1, children: [
384
- /* @__PURE__ */ jsx5(Text5, { bold: true, color: "red", children: "[5/5] Cherry-pick \u9047\u5230\u51B2\u7A81" }),
385
- result.conflictFiles && result.conflictFiles.length > 0 && /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", borderStyle: "single", borderColor: "red", paddingX: 1, children: [
386
- /* @__PURE__ */ jsx5(Text5, { bold: true, children: "\u51B2\u7A81\u6587\u4EF6:" }),
387
- result.conflictFiles.map((f) => /* @__PURE__ */ jsxs5(Text5, { color: "red", children: [
388
- " ",
389
- f
390
- ] }, f))
918
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", gap: 1, children: [
919
+ /* @__PURE__ */ jsx8(SectionHeader, { title: "Cherry-pick \u9047\u5230\u51B2\u7A81" }),
920
+ 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: [
921
+ " ",
922
+ f
923
+ ] }, f)) }),
924
+ /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
925
+ /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: "\u25B8 \u624B\u52A8\u89E3\u51B3\u51B2\u7A81\u540E\u6267\u884C git add \u548C git commit" }),
926
+ /* @__PURE__ */ jsx8(Text8, { color: "gray", dimColor: true, children: "\u25B8 \u6216\u6267\u884C git cherry-pick --abort \u653E\u5F03\u64CD\u4F5C" })
391
927
  ] }),
392
- /* @__PURE__ */ jsx5(Text5, { color: "yellow", children: "\u8BF7\u624B\u52A8\u89E3\u51B3\u51B2\u7A81\u540E\u6267\u884C git add \u548C git commit" }),
393
- /* @__PURE__ */ jsx5(Text5, { color: "gray", dimColor: true, children: "\u6216\u6267\u884C git cherry-pick --abort \u653E\u5F03\u64CD\u4F5C" })
928
+ stashed && stashRestored === false && /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: "\u25B2 stash \u6062\u590D\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8 git stash pop" }),
929
+ stashed && stashRestored === true && /* @__PURE__ */ jsx8(Text8, { color: "green", children: "\u2714 \u5DF2\u6062\u590D\u5DE5\u4F5C\u533A\u53D8\u66F4 (stash pop)" })
394
930
  ] });
395
931
  }
396
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", gap: 1, children: [
397
- /* @__PURE__ */ jsx5(Text5, { bold: true, color: "green", children: "[5/5] \u540C\u6B65\u5B8C\u6210!" }),
398
- /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1, children: [
399
- /* @__PURE__ */ jsx5(Text5, { bold: true, children: "\u6682\u5B58\u533A\u53D8\u66F4\u6982\u89C8 (git diff --cached --stat):" }),
400
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: stagedStat || "(\u65E0\u53D8\u66F4)" })
401
- ] }),
402
- /* @__PURE__ */ jsx5(Text5, { color: "yellow", children: "\u6539\u52A8\u5DF2\u6682\u5B58\u5230\u5DE5\u4F5C\u533A (--no-commit \u6A21\u5F0F)" }),
403
- /* @__PURE__ */ jsx5(Text5, { children: "\u8BF7\u5BA1\u67E5\u540E\u624B\u52A8\u6267\u884C:" }),
404
- /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: " git diff --cached # \u67E5\u770B\u8BE6\u7EC6 diff" }),
405
- /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: ' git commit -m "\u540C\u6B65 commit" # \u63D0\u4EA4' }),
406
- /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: " git reset HEAD # \u6216\u653E\u5F03\u6240\u6709\u6539\u52A8" })
932
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", gap: 1, children: [
933
+ /* @__PURE__ */ jsx8(SectionHeader, { title: "\u540C\u6B65\u5B8C\u6210" }),
934
+ /* @__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)" }) }),
935
+ 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" })),
936
+ /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
937
+ /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: "\u25B2 \u6539\u52A8\u5DF2\u6682\u5B58\u5230\u5DE5\u4F5C\u533A (--no-commit \u6A21\u5F0F)" }),
938
+ /* @__PURE__ */ jsx8(Text8, { color: "gray", dimColor: true, children: " \u5BA1\u67E5\u540E\u624B\u52A8\u6267\u884C:" }),
939
+ /* @__PURE__ */ jsxs8(Text8, { color: "cyan", children: [
940
+ " git diff --cached ",
941
+ /* @__PURE__ */ jsx8(Text8, { color: "gray", dimColor: true, children: "# \u67E5\u770B\u8BE6\u7EC6 diff" })
942
+ ] }),
943
+ /* @__PURE__ */ jsxs8(Text8, { color: "cyan", children: [
944
+ ' git commit -m "sync: ..." ',
945
+ /* @__PURE__ */ jsx8(Text8, { color: "gray", dimColor: true, children: "# \u63D0\u4EA4" })
946
+ ] }),
947
+ /* @__PURE__ */ jsxs8(Text8, { color: "cyan", children: [
948
+ " git reset HEAD ",
949
+ /* @__PURE__ */ jsx8(Text8, { color: "gray", dimColor: true, children: "# \u6216\u653E\u5F03" })
950
+ ] })
951
+ ] })
407
952
  ] });
408
953
  }
409
954
 
410
955
  // src/app.tsx
411
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
956
+ import { execSync } from "child_process";
957
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
958
+ var STEP_NUMBER = {
959
+ checking: 0,
960
+ "stash-recovery": 0,
961
+ "stash-prompt": 0,
962
+ remote: 1,
963
+ branch: 2,
964
+ commits: 3,
965
+ confirm: 4,
966
+ result: 5
967
+ };
968
+ var STEP_DEBOUNCE = 100;
412
969
  function App() {
413
- const [step, setStep] = useState5("remote");
414
- const [remote, setRemote] = useState5("");
415
- const [branch, setBranch] = useState5("");
416
- const [selectedHashes, setSelectedHashes] = useState5([]);
417
- const [commits, setCommits] = useState5([]);
418
- return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
419
- /* @__PURE__ */ jsxs6(Box6, { marginBottom: 1, children: [
420
- /* @__PURE__ */ jsx6(Text6, { bold: true, inverse: true, color: "white", children: " git-sync-tui " }),
421
- /* @__PURE__ */ jsx6(Text6, { children: " " }),
422
- /* @__PURE__ */ jsx6(Text6, { color: "gray", children: "\u4EA4\u4E92\u5F0F commit \u540C\u6B65\u5DE5\u5177 (cherry-pick --no-commit)" })
423
- ] }),
424
- step === "remote" && /* @__PURE__ */ jsx6(
970
+ const { exit } = useApp();
971
+ const [step, setStepRaw] = useState7("checking");
972
+ const [inputReady, setInputReady] = useState7(true);
973
+ const [remote, setRemote] = useState7("");
974
+ const [branch, setBranch] = useState7("");
975
+ const [selectedHashes, setSelectedHashes] = useState7([]);
976
+ const [commits, setCommits] = useState7([]);
977
+ const [hasMerge, setHasMerge] = useState7(false);
978
+ const [useMainline, setUseMainline] = useState7(false);
979
+ const [stashed, setStashed] = useState7(false);
980
+ const [guardTimestamp, setGuardTimestamp] = useState7();
981
+ const stashedRef = useRef2(false);
982
+ const stashRestoredRef = useRef2(false);
983
+ const mountedRef = useRef2(true);
984
+ const debounceTimer = useRef2(null);
985
+ const setStep = useCallback2((newStep) => {
986
+ setInputReady(false);
987
+ setStepRaw(newStep);
988
+ if (debounceTimer.current) clearTimeout(debounceTimer.current);
989
+ debounceTimer.current = setTimeout(() => {
990
+ if (mountedRef.current) setInputReady(true);
991
+ }, STEP_DEBOUNCE);
992
+ }, []);
993
+ const restoreStashSync = useCallback2(() => {
994
+ if (stashedRef.current && !stashRestoredRef.current) {
995
+ try {
996
+ execSync("git stash pop", { stdio: "ignore" });
997
+ stashRestoredRef.current = true;
998
+ removeStashGuardSync();
999
+ } catch {
1000
+ try {
1001
+ process.stderr.write("\n\u26A0 stash \u81EA\u52A8\u6062\u590D\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u6267\u884C: git stash pop\n");
1002
+ } catch {
1003
+ }
1004
+ }
1005
+ }
1006
+ }, []);
1007
+ const markStashRestored = useCallback2(() => {
1008
+ stashRestoredRef.current = true;
1009
+ removeStashGuard();
1010
+ }, []);
1011
+ useEffect4(() => {
1012
+ mountedRef.current = true;
1013
+ async function check() {
1014
+ const guard = await checkStashGuard();
1015
+ if (!mountedRef.current) return;
1016
+ if (guard.exists) {
1017
+ setGuardTimestamp(guard.timestamp);
1018
+ setStep("stash-recovery");
1019
+ return;
1020
+ }
1021
+ const clean = await isWorkingDirClean();
1022
+ if (!mountedRef.current) return;
1023
+ setStep(clean ? "remote" : "stash-prompt");
1024
+ }
1025
+ check();
1026
+ const onSignal = () => {
1027
+ restoreStashSync();
1028
+ process.exit(0);
1029
+ };
1030
+ process.on("SIGINT", onSignal);
1031
+ process.on("SIGTERM", onSignal);
1032
+ process.on("SIGHUP", onSignal);
1033
+ return () => {
1034
+ mountedRef.current = false;
1035
+ if (debounceTimer.current) clearTimeout(debounceTimer.current);
1036
+ process.off("SIGINT", onSignal);
1037
+ process.off("SIGTERM", onSignal);
1038
+ process.off("SIGHUP", onSignal);
1039
+ };
1040
+ }, [restoreStashSync, setStep]);
1041
+ const doStash = async () => {
1042
+ const ok = await stash();
1043
+ if (ok) {
1044
+ setStashed(true);
1045
+ stashedRef.current = true;
1046
+ await writeStashGuard();
1047
+ }
1048
+ if (mountedRef.current) setStep("remote");
1049
+ };
1050
+ const doStashRecover = async () => {
1051
+ const entry = await findStashEntry();
1052
+ if (entry) {
1053
+ await stashPop();
1054
+ }
1055
+ await removeStashGuard();
1056
+ if (!mountedRef.current) return;
1057
+ const clean = await isWorkingDirClean();
1058
+ if (mountedRef.current) setStep(clean ? "remote" : "stash-prompt");
1059
+ };
1060
+ const skipStashRecover = async () => {
1061
+ await removeStashGuard();
1062
+ if (!mountedRef.current) return;
1063
+ const clean = await isWorkingDirClean();
1064
+ if (mountedRef.current) setStep(clean ? "remote" : "stash-prompt");
1065
+ };
1066
+ const goBack = useCallback2((fromStep) => {
1067
+ const backMap = {
1068
+ branch: "remote",
1069
+ commits: "branch",
1070
+ confirm: "commits"
1071
+ };
1072
+ const prev = backMap[fromStep];
1073
+ if (prev) {
1074
+ setStep(prev);
1075
+ } else {
1076
+ restoreStashSync();
1077
+ exit();
1078
+ }
1079
+ }, [setStep, restoreStashSync, exit]);
1080
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
1081
+ /* @__PURE__ */ jsx9(AppHeader, { step: STEP_NUMBER[step], stashed }),
1082
+ step === "checking" && /* @__PURE__ */ jsx9(Spinner6, { label: "\u68C0\u67E5\u5DE5\u4F5C\u533A\u72B6\u6001..." }),
1083
+ step === "stash-recovery" && inputReady && /* @__PURE__ */ jsx9(
1084
+ StashRecovery,
1085
+ {
1086
+ timestamp: guardTimestamp,
1087
+ onRecover: doStashRecover,
1088
+ onSkip: skipStashRecover
1089
+ }
1090
+ ),
1091
+ step === "stash-prompt" && inputReady && /* @__PURE__ */ jsx9(
1092
+ StashPrompt,
1093
+ {
1094
+ onConfirm: doStash,
1095
+ onSkip: () => setStep("remote")
1096
+ }
1097
+ ),
1098
+ step === "remote" && inputReady && /* @__PURE__ */ jsx9(
425
1099
  RemoteSelect,
426
1100
  {
427
1101
  onSelect: (r) => {
428
1102
  setRemote(r);
429
1103
  setStep("branch");
430
- }
1104
+ },
1105
+ onBack: () => goBack("remote")
431
1106
  }
432
1107
  ),
433
- step === "branch" && /* @__PURE__ */ jsx6(
1108
+ step === "branch" && inputReady && /* @__PURE__ */ jsx9(
434
1109
  BranchSelect,
435
1110
  {
436
1111
  remote,
437
1112
  onSelect: (b) => {
438
1113
  setBranch(b);
439
1114
  setStep("commits");
440
- }
1115
+ },
1116
+ onBack: () => goBack("branch")
441
1117
  }
442
1118
  ),
443
- step === "commits" && /* @__PURE__ */ jsx6(
1119
+ step === "commits" && inputReady && /* @__PURE__ */ jsx9(
444
1120
  CommitList,
445
1121
  {
446
1122
  remote,
447
1123
  branch,
448
- onSelect: (hashes, loadedCommits) => {
1124
+ onSelect: async (hashes, loadedCommits) => {
449
1125
  setSelectedHashes(hashes);
450
1126
  setCommits(loadedCommits);
1127
+ const merge = await hasMergeCommits(hashes);
1128
+ setHasMerge(merge);
451
1129
  setStep("confirm");
452
- }
1130
+ },
1131
+ onBack: () => goBack("commits")
453
1132
  }
454
1133
  ),
455
- step === "confirm" && /* @__PURE__ */ jsx6(
1134
+ step === "confirm" && inputReady && /* @__PURE__ */ jsx9(
456
1135
  ConfirmPanel,
457
1136
  {
458
1137
  commits,
459
1138
  selectedHashes,
1139
+ hasMerge,
1140
+ useMainline,
1141
+ onToggleMainline: () => setUseMainline((v) => !v),
460
1142
  onConfirm: () => setStep("result"),
461
- onCancel: () => setStep("commits")
1143
+ onCancel: () => goBack("confirm")
462
1144
  }
463
1145
  ),
464
- step === "result" && /* @__PURE__ */ jsx6(
1146
+ step === "result" && /* @__PURE__ */ jsx9(
465
1147
  ResultPanel,
466
1148
  {
467
1149
  selectedHashes,
468
- onDone: () => process.exit(0)
1150
+ useMainline,
1151
+ stashed,
1152
+ onStashRestored: markStashRestored,
1153
+ onDone: () => {
1154
+ restoreStashSync();
1155
+ exit();
1156
+ }
469
1157
  }
470
1158
  )
471
1159
  ] });
472
1160
  }
473
1161
 
474
1162
  // src/cli.tsx
475
- import { jsx as jsx7 } from "react/jsx-runtime";
1163
+ import { jsx as jsx10 } from "react/jsx-runtime";
476
1164
  var cli = meow(
477
1165
  `
478
1166
  \u7528\u6CD5
@@ -487,13 +1175,16 @@ var cli = meow(
487
1175
  \u4F7F\u7528 cherry-pick --no-commit \u6A21\u5F0F\uFF0C\u540C\u6B65\u540E\u53EF\u5BA1\u67E5\u518D\u63D0\u4EA4\u3002
488
1176
 
489
1177
  \u5FEB\u6377\u952E
490
- Space \u9009\u62E9/\u53D6\u6D88 commit
491
- Enter \u786E\u8BA4\u9009\u62E9
492
- \u2191/\u2193 \u5BFC\u822A
493
- y/n \u786E\u8BA4/\u53D6\u6D88\u6267\u884C
1178
+ Space \u9009\u62E9/\u53D6\u6D88 commit
1179
+ Shift+\u2191/\u2193 \u8FDE\u7EED\u9009\u62E9
1180
+ a \u5168\u9009/\u53D6\u6D88\u5168\u9009
1181
+ i \u53CD\u9009
1182
+ r \u9009\u81F3\u5F00\u5934
1183
+ Enter \u786E\u8BA4\u9009\u62E9
1184
+ y/n \u786E\u8BA4/\u53D6\u6D88\u6267\u884C
494
1185
  `,
495
1186
  {
496
1187
  importMeta: import.meta
497
1188
  }
498
1189
  );
499
- render(/* @__PURE__ */ jsx7(App, {}));
1190
+ render(/* @__PURE__ */ jsx10(App, {}));