git-sync-tui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -0
- package/dist/cli.js +499 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# git-sync-tui
|
|
2
|
+
|
|
3
|
+
Interactive TUI tool for cross-repo git commit synchronization.
|
|
4
|
+
|
|
5
|
+
Cherry-pick commits from remote branches with an interactive terminal UI — select specific commits, preview changes, and sync with `--no-commit` mode for review before committing.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Multi-select commits** — pick non-consecutive commits with Space/Enter
|
|
10
|
+
- **`--no-commit` mode** — changes are staged, not committed, so you can review and edit before committing
|
|
11
|
+
- **Diff preview** — see `--stat` summary of selected commits before executing
|
|
12
|
+
- **Branch search** — filter branches by keyword
|
|
13
|
+
- **Conflict handling** — shows conflicted files when cherry-pick fails
|
|
14
|
+
- **Language agnostic** — works in any git repo (Node.js, Go, Python, Java, etc.)
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install -g git-sync-tui
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Requires Node.js >= 20.
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Run in any git repository
|
|
28
|
+
git-sync-tui
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Workflow
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
[Select remote] → [Select branch] → [Multi-select commits] → [Preview stat] → [Confirm] → [Cherry-pick --no-commit] → [Review & commit manually]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Keyboard Shortcuts
|
|
38
|
+
|
|
39
|
+
| Key | Action |
|
|
40
|
+
|-----|--------|
|
|
41
|
+
| `↑` / `↓` | Navigate |
|
|
42
|
+
| `Space` | Toggle commit selection |
|
|
43
|
+
| `Enter` | Confirm selection |
|
|
44
|
+
| `y` / `n` | Confirm / cancel execution |
|
|
45
|
+
|
|
46
|
+
### After sync
|
|
47
|
+
|
|
48
|
+
Changes are staged in your working tree (not committed). You can:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
git diff --cached # Review changes
|
|
52
|
+
git commit -m "sync: ..." # Commit when ready
|
|
53
|
+
git reset HEAD # Or discard all changes
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Development
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
git clone https://github.com/KiWi233333/git-sync-tui.git
|
|
60
|
+
cd git-sync-tui
|
|
61
|
+
npm install
|
|
62
|
+
npm start # Run with tsx
|
|
63
|
+
npm run build # Build with tsup
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
|
|
68
|
+
MIT
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.tsx
|
|
4
|
+
import { render } from "ink";
|
|
5
|
+
import meow from "meow";
|
|
6
|
+
|
|
7
|
+
// src/app.tsx
|
|
8
|
+
import { useState as useState5 } from "react";
|
|
9
|
+
import { Box as Box6, Text as Text6 } from "ink";
|
|
10
|
+
|
|
11
|
+
// src/components/remote-select.tsx
|
|
12
|
+
import { Box, Text } from "ink";
|
|
13
|
+
import { Select, Spinner } from "@inkjs/ui";
|
|
14
|
+
|
|
15
|
+
// src/hooks/use-git.ts
|
|
16
|
+
import { useState, useEffect, useCallback } from "react";
|
|
17
|
+
|
|
18
|
+
// src/utils/git.ts
|
|
19
|
+
import simpleGit from "simple-git";
|
|
20
|
+
var gitInstance = null;
|
|
21
|
+
function getGit(cwd) {
|
|
22
|
+
if (!gitInstance || cwd) {
|
|
23
|
+
gitInstance = simpleGit(cwd);
|
|
24
|
+
}
|
|
25
|
+
return gitInstance;
|
|
26
|
+
}
|
|
27
|
+
async function getRemotes() {
|
|
28
|
+
const git = getGit();
|
|
29
|
+
const remotes = await git.getRemotes(true);
|
|
30
|
+
return remotes.map((r) => ({
|
|
31
|
+
name: r.name,
|
|
32
|
+
fetchUrl: r.refs.fetch
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
async function getRemoteBranches(remote) {
|
|
36
|
+
const git = getGit();
|
|
37
|
+
try {
|
|
38
|
+
await git.fetch(remote);
|
|
39
|
+
} catch {
|
|
40
|
+
}
|
|
41
|
+
const result = await git.branch(["-r"]);
|
|
42
|
+
const prefix = `${remote}/`;
|
|
43
|
+
return result.all.filter((b) => b.startsWith(prefix) && !b.includes("HEAD")).map((b) => b.replace(prefix, "")).sort();
|
|
44
|
+
}
|
|
45
|
+
async function getCommits(remote, branch, count = 30) {
|
|
46
|
+
const git = getGit();
|
|
47
|
+
const ref = `${remote}/${branch}`;
|
|
48
|
+
const log = await git.log({
|
|
49
|
+
from: void 0,
|
|
50
|
+
to: ref,
|
|
51
|
+
maxCount: count,
|
|
52
|
+
format: {
|
|
53
|
+
hash: "%H",
|
|
54
|
+
shortHash: "%h",
|
|
55
|
+
message: "%s",
|
|
56
|
+
author: "%an",
|
|
57
|
+
date: "%ar"
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
return log.all.map((entry) => ({
|
|
61
|
+
hash: entry.hash,
|
|
62
|
+
shortHash: entry.hash.substring(0, 7),
|
|
63
|
+
message: entry.message || "",
|
|
64
|
+
author: entry.author || "",
|
|
65
|
+
date: entry.date || ""
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
async function getMultiCommitStat(hashes) {
|
|
69
|
+
if (hashes.length === 0) return "";
|
|
70
|
+
const git = getGit();
|
|
71
|
+
try {
|
|
72
|
+
const stats = [];
|
|
73
|
+
for (const hash of hashes) {
|
|
74
|
+
const result = await git.raw(["diff-tree", "--stat", "--no-commit-id", "-r", hash]);
|
|
75
|
+
if (result.trim()) stats.push(`${hash.substring(0, 7)}:
|
|
76
|
+
${result.trim()}`);
|
|
77
|
+
}
|
|
78
|
+
return stats.join("\n\n");
|
|
79
|
+
} catch {
|
|
80
|
+
return "(\u65E0\u6CD5\u83B7\u53D6 stat \u4FE1\u606F)";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function cherryPick(hashes) {
|
|
84
|
+
const git = getGit();
|
|
85
|
+
try {
|
|
86
|
+
const orderedHashes = [...hashes].reverse();
|
|
87
|
+
for (const hash of orderedHashes) {
|
|
88
|
+
await git.raw(["cherry-pick", "--no-commit", hash]);
|
|
89
|
+
}
|
|
90
|
+
return { success: true };
|
|
91
|
+
} catch (err) {
|
|
92
|
+
try {
|
|
93
|
+
const status = await git.status();
|
|
94
|
+
const conflictFiles = status.conflicted;
|
|
95
|
+
return {
|
|
96
|
+
success: false,
|
|
97
|
+
error: err.message,
|
|
98
|
+
conflictFiles: conflictFiles.length > 0 ? conflictFiles : void 0
|
|
99
|
+
};
|
|
100
|
+
} catch {
|
|
101
|
+
return { success: false, error: err.message };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async function getStagedStat() {
|
|
106
|
+
const git = getGit();
|
|
107
|
+
try {
|
|
108
|
+
const result = await git.raw(["diff", "--cached", "--stat"]);
|
|
109
|
+
return result.trim();
|
|
110
|
+
} catch {
|
|
111
|
+
return "";
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/hooks/use-git.ts
|
|
116
|
+
function useAsync(fn, deps = []) {
|
|
117
|
+
const [state, setState] = useState({
|
|
118
|
+
data: null,
|
|
119
|
+
loading: true,
|
|
120
|
+
error: null
|
|
121
|
+
});
|
|
122
|
+
const load = useCallback(async () => {
|
|
123
|
+
setState({ data: null, loading: true, error: null });
|
|
124
|
+
try {
|
|
125
|
+
const data = await fn();
|
|
126
|
+
setState({ data, loading: false, error: null });
|
|
127
|
+
} catch (err) {
|
|
128
|
+
setState({ data: null, loading: false, error: err.message });
|
|
129
|
+
}
|
|
130
|
+
}, deps);
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
load();
|
|
133
|
+
}, [load]);
|
|
134
|
+
return { ...state, reload: load };
|
|
135
|
+
}
|
|
136
|
+
function useRemotes() {
|
|
137
|
+
return useAsync(() => getRemotes(), []);
|
|
138
|
+
}
|
|
139
|
+
function useBranches(remote) {
|
|
140
|
+
return useAsync(
|
|
141
|
+
() => remote ? getRemoteBranches(remote) : Promise.resolve([]),
|
|
142
|
+
[remote]
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
function useCommits(remote, branch, count = 30) {
|
|
146
|
+
return useAsync(
|
|
147
|
+
() => remote && branch ? getCommits(remote, branch, count) : Promise.resolve([]),
|
|
148
|
+
[remote, branch, count]
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
function useCommitStat(hashes) {
|
|
152
|
+
const [stat, setStat] = useState("");
|
|
153
|
+
const [loading, setLoading] = useState(false);
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
if (hashes.length === 0) {
|
|
156
|
+
setStat("");
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
setLoading(true);
|
|
160
|
+
getMultiCommitStat(hashes).then((s) => {
|
|
161
|
+
setStat(s);
|
|
162
|
+
setLoading(false);
|
|
163
|
+
}).catch(() => {
|
|
164
|
+
setStat("(\u83B7\u53D6\u5931\u8D25)");
|
|
165
|
+
setLoading(false);
|
|
166
|
+
});
|
|
167
|
+
}, [hashes.join(",")]);
|
|
168
|
+
return { stat, loading };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/components/remote-select.tsx
|
|
172
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
173
|
+
function RemoteSelect({ onSelect }) {
|
|
174
|
+
const { data: remotes, loading, error } = useRemotes();
|
|
175
|
+
if (loading) {
|
|
176
|
+
return /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Spinner, { label: "\u6B63\u5728\u83B7\u53D6\u8FDC\u7A0B\u4ED3\u5E93\u5217\u8868..." }) });
|
|
177
|
+
}
|
|
178
|
+
if (error) {
|
|
179
|
+
return /* @__PURE__ */ jsxs(Text, { color: "red", children: [
|
|
180
|
+
"\u83B7\u53D6\u8FDC\u7A0B\u4ED3\u5E93\u5931\u8D25: ",
|
|
181
|
+
error
|
|
182
|
+
] });
|
|
183
|
+
}
|
|
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" });
|
|
186
|
+
}
|
|
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 })
|
|
194
|
+
] });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 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 }) {
|
|
203
|
+
const { data: branches, loading, error } = useBranches(remote);
|
|
204
|
+
const [filter, setFilter] = useState2("");
|
|
205
|
+
const filteredOptions = useMemo(() => {
|
|
206
|
+
if (!branches) return [];
|
|
207
|
+
const filtered = filter ? branches.filter((b) => b.toLowerCase().includes(filter.toLowerCase())) : branches;
|
|
208
|
+
return filtered.map((b) => ({ label: b, value: b }));
|
|
209
|
+
}, [branches, filter]);
|
|
210
|
+
if (loading) {
|
|
211
|
+
return /* @__PURE__ */ jsx2(Box2, { children: /* @__PURE__ */ jsx2(Spinner2, { label: `\u6B63\u5728\u83B7\u53D6 ${remote} \u7684\u5206\u652F\u5217\u8868...` }) });
|
|
212
|
+
}
|
|
213
|
+
if (error) {
|
|
214
|
+
return /* @__PURE__ */ jsxs2(Text2, { color: "red", children: [
|
|
215
|
+
"\u83B7\u53D6\u5206\u652F\u5217\u8868\u5931\u8D25: ",
|
|
216
|
+
error
|
|
217
|
+
] });
|
|
218
|
+
}
|
|
219
|
+
if (!branches || branches.length === 0) {
|
|
220
|
+
return /* @__PURE__ */ jsx2(Text2, { color: "red", children: "\u672A\u627E\u5230\u8FDC\u7A0B\u5206\u652F" });
|
|
221
|
+
}
|
|
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,
|
|
232
|
+
{
|
|
233
|
+
placeholder: "\u8F93\u5165\u5173\u952E\u5B57\u8FC7\u6EE4\u5206\u652F...",
|
|
234
|
+
onChange: setFilter
|
|
235
|
+
}
|
|
236
|
+
)
|
|
237
|
+
] }),
|
|
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" })
|
|
245
|
+
] });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 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 }) {
|
|
254
|
+
const { data: commits, loading, error } = useCommits(remote, branch, 30);
|
|
255
|
+
const [selectedHashes, setSelectedHashes] = useState3([]);
|
|
256
|
+
const { stat, loading: statLoading } = useCommitStat(selectedHashes);
|
|
257
|
+
if (loading) {
|
|
258
|
+
return /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsx3(Spinner3, { label: `\u6B63\u5728\u83B7\u53D6 ${remote}/${branch} \u7684 commit \u5217\u8868...` }) });
|
|
259
|
+
}
|
|
260
|
+
if (error) {
|
|
261
|
+
return /* @__PURE__ */ jsxs3(Text3, { color: "red", children: [
|
|
262
|
+
"\u83B7\u53D6 commit \u5217\u8868\u5931\u8D25: ",
|
|
263
|
+
error
|
|
264
|
+
] });
|
|
265
|
+
}
|
|
266
|
+
if (!commits || commits.length === 0) {
|
|
267
|
+
return /* @__PURE__ */ jsx3(Text3, { color: "yellow", children: "\u8BE5\u5206\u652F\u6CA1\u6709 commit" });
|
|
268
|
+
}
|
|
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: [
|
|
297
|
+
"\u5DF2\u9009 ",
|
|
298
|
+
selectedHashes.length,
|
|
299
|
+
" \u4E2A commit \u2014 diff --stat \u9884\u89C8:"
|
|
300
|
+
] }),
|
|
301
|
+
statLoading ? /* @__PURE__ */ jsx3(Spinner3, { label: "\u52A0\u8F7D\u4E2D..." }) : /* @__PURE__ */ jsx3(Text3, { color: "gray", children: stat || "(\u65E0\u53D8\u66F4)" })
|
|
302
|
+
] })
|
|
303
|
+
] });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// 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") {
|
|
312
|
+
onConfirm();
|
|
313
|
+
} else if (input === "n" || input === "N" || input === "q") {
|
|
314
|
+
onCancel();
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
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:"
|
|
325
|
+
] }),
|
|
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))
|
|
341
|
+
] }),
|
|
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" })
|
|
345
|
+
] }),
|
|
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" })
|
|
352
|
+
] })
|
|
353
|
+
] });
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// 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("");
|
|
365
|
+
useEffect3(() => {
|
|
366
|
+
async function run() {
|
|
367
|
+
const res = await cherryPick(selectedHashes);
|
|
368
|
+
setResult(res);
|
|
369
|
+
if (res.success) {
|
|
370
|
+
const stat = await getStagedStat();
|
|
371
|
+
setStagedStat(stat);
|
|
372
|
+
setPhase("done");
|
|
373
|
+
} else {
|
|
374
|
+
setPhase("error");
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
run();
|
|
378
|
+
}, []);
|
|
379
|
+
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)...` }) });
|
|
381
|
+
}
|
|
382
|
+
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))
|
|
391
|
+
] }),
|
|
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" })
|
|
394
|
+
] });
|
|
395
|
+
}
|
|
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" })
|
|
407
|
+
] });
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// src/app.tsx
|
|
411
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
412
|
+
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(
|
|
425
|
+
RemoteSelect,
|
|
426
|
+
{
|
|
427
|
+
onSelect: (r) => {
|
|
428
|
+
setRemote(r);
|
|
429
|
+
setStep("branch");
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
),
|
|
433
|
+
step === "branch" && /* @__PURE__ */ jsx6(
|
|
434
|
+
BranchSelect,
|
|
435
|
+
{
|
|
436
|
+
remote,
|
|
437
|
+
onSelect: (b) => {
|
|
438
|
+
setBranch(b);
|
|
439
|
+
setStep("commits");
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
),
|
|
443
|
+
step === "commits" && /* @__PURE__ */ jsx6(
|
|
444
|
+
CommitList,
|
|
445
|
+
{
|
|
446
|
+
remote,
|
|
447
|
+
branch,
|
|
448
|
+
onSelect: (hashes, loadedCommits) => {
|
|
449
|
+
setSelectedHashes(hashes);
|
|
450
|
+
setCommits(loadedCommits);
|
|
451
|
+
setStep("confirm");
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
),
|
|
455
|
+
step === "confirm" && /* @__PURE__ */ jsx6(
|
|
456
|
+
ConfirmPanel,
|
|
457
|
+
{
|
|
458
|
+
commits,
|
|
459
|
+
selectedHashes,
|
|
460
|
+
onConfirm: () => setStep("result"),
|
|
461
|
+
onCancel: () => setStep("commits")
|
|
462
|
+
}
|
|
463
|
+
),
|
|
464
|
+
step === "result" && /* @__PURE__ */ jsx6(
|
|
465
|
+
ResultPanel,
|
|
466
|
+
{
|
|
467
|
+
selectedHashes,
|
|
468
|
+
onDone: () => process.exit(0)
|
|
469
|
+
}
|
|
470
|
+
)
|
|
471
|
+
] });
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// src/cli.tsx
|
|
475
|
+
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
476
|
+
var cli = meow(
|
|
477
|
+
`
|
|
478
|
+
\u7528\u6CD5
|
|
479
|
+
$ git-sync-tui
|
|
480
|
+
|
|
481
|
+
\u9009\u9879
|
|
482
|
+
--help \u663E\u793A\u5E2E\u52A9
|
|
483
|
+
--version \u663E\u793A\u7248\u672C
|
|
484
|
+
|
|
485
|
+
\u8BF4\u660E
|
|
486
|
+
\u4EA4\u4E92\u5F0F TUI \u5DE5\u5177\uFF0C\u4ECE\u8FDC\u7A0B\u5206\u652F\u6311\u9009 commit \u540C\u6B65\u5230\u5F53\u524D\u5206\u652F\u3002
|
|
487
|
+
\u4F7F\u7528 cherry-pick --no-commit \u6A21\u5F0F\uFF0C\u540C\u6B65\u540E\u53EF\u5BA1\u67E5\u518D\u63D0\u4EA4\u3002
|
|
488
|
+
|
|
489
|
+
\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
|
|
494
|
+
`,
|
|
495
|
+
{
|
|
496
|
+
importMeta: import.meta
|
|
497
|
+
}
|
|
498
|
+
);
|
|
499
|
+
render(/* @__PURE__ */ jsx7(App, {}));
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "git-sync-tui",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Interactive TUI tool for cross-repo git commit synchronization (cherry-pick --no-commit)",
|
|
6
|
+
"author": "KiWi233333",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/KiWi233333/git-sync-tui.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/KiWi233333/git-sync-tui#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/KiWi233333/git-sync-tui/issues"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"git-sync-tui": "dist/cli.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=20"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"start": "tsx src/cli.tsx",
|
|
26
|
+
"dev": "tsx watch src/cli.tsx",
|
|
27
|
+
"build": "tsup",
|
|
28
|
+
"prepublishOnly": "npm run build"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"git",
|
|
32
|
+
"sync",
|
|
33
|
+
"cherry-pick",
|
|
34
|
+
"tui",
|
|
35
|
+
"cli",
|
|
36
|
+
"ink",
|
|
37
|
+
"interactive",
|
|
38
|
+
"no-commit"
|
|
39
|
+
],
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@inkjs/ui": "^2.0.0",
|
|
43
|
+
"ink": "^5.1.0",
|
|
44
|
+
"meow": "^13.0.0",
|
|
45
|
+
"react": "^18.3.1",
|
|
46
|
+
"simple-git": "^3.27.0"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "^25.5.0",
|
|
50
|
+
"@types/react": "^18.3.0",
|
|
51
|
+
"tsup": "^8.5.1",
|
|
52
|
+
"tsx": "^4.19.0",
|
|
53
|
+
"typescript": "^5.7.0"
|
|
54
|
+
}
|
|
55
|
+
}
|