git-sync-tui 0.1.6 → 0.1.7
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 +92 -23
- package/README.zh-CN.md +91 -22
- package/dist/cli.js +174 -31
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,14 +15,15 @@
|
|
|
15
15
|
|
|
16
16
|
<p align="center">
|
|
17
17
|
Cherry-pick commits from remote branches with an intuitive terminal UI.<br>
|
|
18
|
-
|
|
18
|
+
Multi-select commits, preview diff stats, handle conflicts interactively, and sync safely with backup & stash protection.
|
|
19
19
|
</p>
|
|
20
20
|
|
|
21
21
|
<p align="center">
|
|
22
22
|
<a href="#-features">Features</a> ·
|
|
23
23
|
<a href="#-quick-start">Quick Start</a> ·
|
|
24
24
|
<a href="#-installation">Installation</a> ·
|
|
25
|
-
<a href="#-workflow">Workflow</a>
|
|
25
|
+
<a href="#-workflow">Workflow</a> ·
|
|
26
|
+
<a href="#%EF%B8%8F-cli-options">CLI Options</a>
|
|
26
27
|
</p>
|
|
27
28
|
|
|
28
29
|
<p align="center">
|
|
@@ -35,11 +36,18 @@
|
|
|
35
36
|
|
|
36
37
|
## ✨ Features
|
|
37
38
|
|
|
38
|
-
- 🎯 **Multi-select commits** —
|
|
39
|
+
- 🎯 **Multi-select commits** — Select non-consecutive commits with Space, range-select with Shift+↑↓, toggle all with `a`, invert with `i`
|
|
39
40
|
- 🔍 **Branch search** — Fuzzy filter branches by keyword
|
|
40
|
-
- 👀 **Diff preview** —
|
|
41
|
-
- ⚡ **
|
|
42
|
-
-
|
|
41
|
+
- 👀 **Diff preview** — Scrollable `--stat` summary panel with `j`/`k` navigation
|
|
42
|
+
- ⚡ **Dual mode** — `--no-commit` stages changes for review, or commit individually preserving original messages
|
|
43
|
+
- 🔀 **One-by-one cherry-pick** — Executes commits sequentially, pausing on conflicts for interactive resolution
|
|
44
|
+
- ⚠️ **Conflict handling** — Shows conflicted files, resolve in another terminal, then continue/abort/quit
|
|
45
|
+
- 🛡️ **Safe backup** — Creates a backup branch before execution; full rollback on abort
|
|
46
|
+
- 📦 **Auto stash** — Detects uncommitted changes, offers to stash, auto-restores after sync
|
|
47
|
+
- 🔄 **Stash recovery** — Detects interrupted sessions and offers to recover stashed changes
|
|
48
|
+
- 🌿 **Branch check** — Auto-creates target branch from main/master if not on it
|
|
49
|
+
- ✅ **Synced markers** — Marks already-synced commits as `[synced]` in the commit list
|
|
50
|
+
- 🖥️ **CLI mode** — Non-interactive mode with `-r -b -c` flags for scripting
|
|
43
51
|
- 🌐 **Universal** — Works in any git repository, any language
|
|
44
52
|
|
|
45
53
|
## 🚀 Quick Start
|
|
@@ -64,35 +72,95 @@ npm install -g git-sync-tui
|
|
|
64
72
|
## 🔄 Workflow
|
|
65
73
|
|
|
66
74
|
```
|
|
67
|
-
Select Remote → Select Branch → Multi-select Commits
|
|
68
|
-
|
|
69
|
-
|
|
75
|
+
Check workspace → Select Remote → Select Branch → Branch Check → Multi-select Commits
|
|
76
|
+
↓ ↓
|
|
77
|
+
Auto stash Preview diff stats
|
|
78
|
+
(if needed) ↓
|
|
79
|
+
Confirm & choose mode
|
|
80
|
+
↓
|
|
81
|
+
Cherry-pick one-by-one (with backup)
|
|
82
|
+
↓
|
|
83
|
+
Handle conflicts / Done
|
|
84
|
+
↓
|
|
85
|
+
Restore stash & exit
|
|
70
86
|
```
|
|
71
87
|
|
|
72
88
|
## ⌨️ Keyboard Shortcuts
|
|
73
89
|
|
|
90
|
+
### Commit Selection
|
|
91
|
+
|
|
74
92
|
| Key | Action |
|
|
75
93
|
|-----|--------|
|
|
76
|
-
| `↑` `↓` | Navigate
|
|
94
|
+
| `↑` `↓` | Navigate commits |
|
|
77
95
|
| `Space` | Toggle commit selection |
|
|
96
|
+
| `Shift`+`↑`/`↓` | Range select |
|
|
97
|
+
| `a` | Select all / Deselect all |
|
|
98
|
+
| `i` | Invert selection |
|
|
99
|
+
| `r` | Select from top to cursor |
|
|
100
|
+
| `j` / `k` | Scroll diff stat preview |
|
|
78
101
|
| `Enter` | Confirm selection |
|
|
79
|
-
| `
|
|
80
|
-
| `/` | Search (in branch list) |
|
|
102
|
+
| `Esc` | Go back |
|
|
81
103
|
|
|
82
|
-
|
|
104
|
+
### Confirm Panel
|
|
105
|
+
|
|
106
|
+
| Key | Action |
|
|
107
|
+
|-----|--------|
|
|
108
|
+
| `y` | Confirm execution |
|
|
109
|
+
| `n` | Cancel |
|
|
110
|
+
| `c` | Toggle commit mode (--no-commit / individual) |
|
|
111
|
+
| `m` | Toggle `-m 1` for merge commits |
|
|
112
|
+
| `Esc` | Go back |
|
|
83
113
|
|
|
84
|
-
|
|
114
|
+
### Conflict Handling
|
|
85
115
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
116
|
+
| Key | Action |
|
|
117
|
+
|-----|--------|
|
|
118
|
+
| `c` | Continue (after resolving conflicts) |
|
|
119
|
+
| `a` | Abort (rollback all changes) |
|
|
120
|
+
| `q` | Quit (keep current state) |
|
|
89
121
|
|
|
90
|
-
|
|
91
|
-
git commit -m "sync: cherry-picked commits from feature-branch"
|
|
122
|
+
## ⚙️ CLI Options
|
|
92
123
|
|
|
93
|
-
# Or discard all changes
|
|
94
|
-
git reset HEAD
|
|
95
124
|
```
|
|
125
|
+
Usage
|
|
126
|
+
$ git-sync-tui [options]
|
|
127
|
+
|
|
128
|
+
Options
|
|
129
|
+
-r, --remote <name> Remote name
|
|
130
|
+
-b, --branch <name> Remote branch name
|
|
131
|
+
-c, --commits <hashes> Commit hashes (comma-separated)
|
|
132
|
+
-n, --count <number> Number of commits to show (default: 100)
|
|
133
|
+
-m, --mainline Use -m 1 for merge commits
|
|
134
|
+
-y, --yes Skip confirmation
|
|
135
|
+
--no-stash Skip stash prompt
|
|
136
|
+
--list List remote branch commits and exit
|
|
137
|
+
|
|
138
|
+
Modes
|
|
139
|
+
No arguments Interactive TUI mode
|
|
140
|
+
-r -b --list List commits (plain text)
|
|
141
|
+
-r -b -c CLI mode, confirm before execution
|
|
142
|
+
-r -b -c --yes CLI mode, execute directly
|
|
143
|
+
-r or -r -b only TUI mode, skip completed steps
|
|
144
|
+
|
|
145
|
+
Examples
|
|
146
|
+
$ git-sync-tui # TUI mode
|
|
147
|
+
$ git-sync-tui -r upstream -b main --list # List commits
|
|
148
|
+
$ git-sync-tui -r upstream -b main -c abc1234 --yes # Execute directly
|
|
149
|
+
$ git-sync-tui -r upstream -b main -c abc1234,def5678 # Confirm then execute
|
|
150
|
+
$ git-sync-tui -r upstream # TUI mode, skip remote select
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## 📋 After Sync
|
|
154
|
+
|
|
155
|
+
**--no-commit mode** — Changes are staged in your working tree (not committed):
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
git diff --cached # Review staged changes
|
|
159
|
+
git commit -m "sync: cherry-picked commits from feature-branch" # Commit
|
|
160
|
+
git reset HEAD # Or discard all changes
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Individual commit mode** — Original commit messages are preserved. Check with `git log`.
|
|
96
164
|
|
|
97
165
|
## 💡 Use Cases
|
|
98
166
|
|
|
@@ -107,8 +175,8 @@ git reset HEAD
|
|
|
107
175
|
```bash
|
|
108
176
|
git clone https://github.com/KiWi233333/git-sync-tui.git
|
|
109
177
|
cd git-sync-tui
|
|
110
|
-
|
|
111
|
-
|
|
178
|
+
pnpm install
|
|
179
|
+
pnpm start
|
|
112
180
|
```
|
|
113
181
|
|
|
114
182
|
## 🏗️ Tech Stack
|
|
@@ -116,6 +184,7 @@ npm start
|
|
|
116
184
|
- [Ink](https://github.com/vadimdemedes/ink) — React for interactive CLI apps
|
|
117
185
|
- [@inkjs/ui](https://github.com/inkjs/ui) — UI components for Ink
|
|
118
186
|
- [simple-git](https://github.com/steveukx/git-js) — Git commands interface
|
|
187
|
+
- [meow](https://github.com/sindresorhus/meow) — CLI argument parsing
|
|
119
188
|
|
|
120
189
|
## 🤝 Contributing
|
|
121
190
|
|
package/README.zh-CN.md
CHANGED
|
@@ -15,14 +15,15 @@
|
|
|
15
15
|
|
|
16
16
|
<p align="center">
|
|
17
17
|
通过直观的终端界面从远程分支 cherry-pick 提交。<br>
|
|
18
|
-
|
|
18
|
+
多选提交、预览 diff 统计、交互式处理冲突,支持备份分支和 stash 保护机制。
|
|
19
19
|
</p>
|
|
20
20
|
|
|
21
21
|
<p align="center">
|
|
22
22
|
<a href="#-功能特性">功能</a> ·
|
|
23
23
|
<a href="#-快速开始">快速开始</a> ·
|
|
24
24
|
<a href="#-安装">安装</a> ·
|
|
25
|
-
<a href="#-工作流程">工作流程</a>
|
|
25
|
+
<a href="#-工作流程">工作流程</a> ·
|
|
26
|
+
<a href="#%EF%B8%8F-命令行选项">命令行选项</a>
|
|
26
27
|
</p>
|
|
27
28
|
|
|
28
29
|
<p align="center">
|
|
@@ -35,11 +36,18 @@
|
|
|
35
36
|
|
|
36
37
|
## ✨ 功能特性
|
|
37
38
|
|
|
38
|
-
- 🎯 **多选提交** — 使用 Space
|
|
39
|
+
- 🎯 **多选提交** — 使用 Space 选择不连续的提交,Shift+↑↓ 连选,`a` 全选,`i` 反选
|
|
39
40
|
- 🔍 **分支搜索** — 按关键词模糊过滤分支
|
|
40
|
-
- 👀 **差异预览** —
|
|
41
|
-
- ⚡
|
|
42
|
-
-
|
|
41
|
+
- 👀 **差异预览** — 可滚动的 `--stat` 摘要面板,支持 `j`/`k` 上下滚动
|
|
42
|
+
- ⚡ **双模式** — `--no-commit` 仅暂存变更供审查,或逐个提交保留原始 commit 信息
|
|
43
|
+
- 🔀 **逐个 cherry-pick** — 按顺序执行提交,遇到冲突时暂停等待交互处理
|
|
44
|
+
- ⚠️ **冲突处理** — 显示冲突文件列表,在另一终端解决后继续/放弃/退出
|
|
45
|
+
- 🛡️ **安全备份** — 执行前自动创建备份分支,放弃时完整回滚
|
|
46
|
+
- 📦 **自动 stash** — 检测未提交变更,提示 stash 保存,同步后自动恢复
|
|
47
|
+
- 🔄 **Stash 恢复** — 检测上次中断的会话,提供恢复 stash 的选项
|
|
48
|
+
- 🌿 **分支检查** — 若当前不在目标分支,自动从 main/master 创建并切换
|
|
49
|
+
- ✅ **已同步标记** — 在 commit 列表中标记已同步的提交为 `[已同步]`
|
|
50
|
+
- 🖥️ **CLI 模式** — 支持 `-r -b -c` 参数的非交互模式,适用于脚本
|
|
43
51
|
- 🌐 **通用性** — 适用于任何 Git 仓库,不限语言
|
|
44
52
|
|
|
45
53
|
## 🚀 快速开始
|
|
@@ -64,35 +72,95 @@ npm install -g git-sync-tui
|
|
|
64
72
|
## 🔄 工作流程
|
|
65
73
|
|
|
66
74
|
```
|
|
67
|
-
选择远程仓库 → 选择分支 →
|
|
68
|
-
|
|
69
|
-
|
|
75
|
+
检查工作区 → 选择远程仓库 → 选择分支 → 分支检查 → 多选提交
|
|
76
|
+
↓ ↓
|
|
77
|
+
自动 stash 预览 diff 统计
|
|
78
|
+
(如需要) ↓
|
|
79
|
+
确认并选择模式
|
|
80
|
+
↓
|
|
81
|
+
逐个 cherry-pick(带备份)
|
|
82
|
+
↓
|
|
83
|
+
处理冲突 / 完成
|
|
84
|
+
↓
|
|
85
|
+
恢复 stash 并退出
|
|
70
86
|
```
|
|
71
87
|
|
|
72
88
|
## ⌨️ 快捷键
|
|
73
89
|
|
|
90
|
+
### 提交选择
|
|
91
|
+
|
|
74
92
|
| 按键 | 操作 |
|
|
75
93
|
|-----|------|
|
|
76
94
|
| `↑` `↓` | 上下导航 |
|
|
77
95
|
| `Space` | 切换提交选择 |
|
|
96
|
+
| `Shift`+`↑`/`↓` | 连续选择 |
|
|
97
|
+
| `a` | 全选 / 取消全选 |
|
|
98
|
+
| `i` | 反选 |
|
|
99
|
+
| `r` | 从开头选至光标 |
|
|
100
|
+
| `j` / `k` | 滚动 diff stat 预览 |
|
|
78
101
|
| `Enter` | 确认选择 |
|
|
79
|
-
| `
|
|
80
|
-
| `/` | 搜索(在分支列表中) |
|
|
102
|
+
| `Esc` | 返回上一步 |
|
|
81
103
|
|
|
82
|
-
|
|
104
|
+
### 确认面板
|
|
105
|
+
|
|
106
|
+
| 按键 | 操作 |
|
|
107
|
+
|-----|------|
|
|
108
|
+
| `y` | 确认执行 |
|
|
109
|
+
| `n` | 取消 |
|
|
110
|
+
| `c` | 切换提交模式(--no-commit / 逐个提交) |
|
|
111
|
+
| `m` | 切换 `-m 1`(merge commit 时) |
|
|
112
|
+
| `Esc` | 返回 |
|
|
83
113
|
|
|
84
|
-
|
|
114
|
+
### 冲突处理
|
|
85
115
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
116
|
+
| 按键 | 操作 |
|
|
117
|
+
|-----|------|
|
|
118
|
+
| `c` | 继续(冲突已解决) |
|
|
119
|
+
| `a` | 放弃(回滚全部变更) |
|
|
120
|
+
| `q` | 退出(保留当前状态) |
|
|
89
121
|
|
|
90
|
-
|
|
91
|
-
git commit -m "sync: 从 feature-branch cherry-pick 提交"
|
|
122
|
+
## ⚙️ 命令行选项
|
|
92
123
|
|
|
93
|
-
# 或放弃所有变更
|
|
94
|
-
git reset HEAD
|
|
95
124
|
```
|
|
125
|
+
用法
|
|
126
|
+
$ git-sync-tui [options]
|
|
127
|
+
|
|
128
|
+
选项
|
|
129
|
+
-r, --remote <name> 指定远程仓库名称
|
|
130
|
+
-b, --branch <name> 指定远程分支名称
|
|
131
|
+
-c, --commits <hashes> 指定 commit hash(逗号分隔)
|
|
132
|
+
-n, --count <number> 显示 commit 数量(默认 100)
|
|
133
|
+
-m, --mainline 对 merge commit 使用 -m 1
|
|
134
|
+
-y, --yes 跳过确认直接执行
|
|
135
|
+
--no-stash 跳过 stash 提示
|
|
136
|
+
--list 列出远程分支的 commit 后退出
|
|
137
|
+
|
|
138
|
+
模式
|
|
139
|
+
无参数 交互式 TUI 模式
|
|
140
|
+
-r -b --list 列出 commit(纯文本)
|
|
141
|
+
-r -b -c CLI 模式,确认后执行
|
|
142
|
+
-r -b -c --yes CLI 模式,直接执行
|
|
143
|
+
仅 -r 或 -r -b TUI 模式,跳过已指定步骤
|
|
144
|
+
|
|
145
|
+
示例
|
|
146
|
+
$ git-sync-tui # TUI 模式
|
|
147
|
+
$ git-sync-tui -r upstream -b main --list # 列出 commits
|
|
148
|
+
$ git-sync-tui -r upstream -b main -c abc1234 --yes # 直接执行
|
|
149
|
+
$ git-sync-tui -r upstream -b main -c abc1234,def5678 # 确认后执行
|
|
150
|
+
$ git-sync-tui -r upstream # TUI 模式,跳过选择仓库
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## 📋 同步后操作
|
|
154
|
+
|
|
155
|
+
**--no-commit 模式** — 变更已暂存在工作区(未提交):
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
git diff --cached # 查看暂存的变更
|
|
159
|
+
git commit -m "sync: 从 feature-branch cherry-pick 提交" # 提交
|
|
160
|
+
git reset HEAD # 或放弃所有变更
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**逐个提交模式** — 保留原始 commit 信息,可通过 `git log` 查看。
|
|
96
164
|
|
|
97
165
|
## 💡 使用场景
|
|
98
166
|
|
|
@@ -107,8 +175,8 @@ git reset HEAD
|
|
|
107
175
|
```bash
|
|
108
176
|
git clone https://github.com/KiWi233333/git-sync-tui.git
|
|
109
177
|
cd git-sync-tui
|
|
110
|
-
|
|
111
|
-
|
|
178
|
+
pnpm install
|
|
179
|
+
pnpm start
|
|
112
180
|
```
|
|
113
181
|
|
|
114
182
|
## 🏗️ 技术栈
|
|
@@ -116,6 +184,7 @@ npm start
|
|
|
116
184
|
- [Ink](https://github.com/vadimdemedes/ink) — 用于构建交互式 CLI 应用的 React 框架
|
|
117
185
|
- [@inkjs/ui](https://github.com/inkjs/ui) — Ink 的 UI 组件库
|
|
118
186
|
- [simple-git](https://github.com/steveukx/git-js) — Git 命令接口
|
|
187
|
+
- [meow](https://github.com/sindresorhus/meow) — CLI 参数解析
|
|
119
188
|
|
|
120
189
|
## 🤝 贡献
|
|
121
190
|
|
package/dist/cli.js
CHANGED
|
@@ -11,8 +11,8 @@ import { render } from "ink";
|
|
|
11
11
|
import meow from "meow";
|
|
12
12
|
|
|
13
13
|
// src/app.tsx
|
|
14
|
-
import { useState as
|
|
15
|
-
import { Box as
|
|
14
|
+
import { useState as useState9, useEffect as useEffect7, useRef as useRef5, useCallback as useCallback3 } from "react";
|
|
15
|
+
import { Box as Box11, useApp } from "ink";
|
|
16
16
|
import { Spinner as Spinner7 } from "@inkjs/ui";
|
|
17
17
|
|
|
18
18
|
// src/components/ui.tsx
|
|
@@ -1463,6 +1463,10 @@ function ResultPanel({ selectedHashes, useMainline, noCommit, stashed, onStashRe
|
|
|
1463
1463
|
setPhase("aborted");
|
|
1464
1464
|
}, [tryRestoreStash]);
|
|
1465
1465
|
useInput8((input, key) => {
|
|
1466
|
+
if (phase === "done" || phase === "aborted") {
|
|
1467
|
+
onDone();
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1466
1470
|
if (phase === "conflict") {
|
|
1467
1471
|
if (input === "c" || input === "C") {
|
|
1468
1472
|
handleContinue();
|
|
@@ -1571,7 +1575,8 @@ function ResultPanel({ selectedHashes, useMainline, noCommit, stashed, onStashRe
|
|
|
1571
1575
|
stashed && stashRestored === true && /* @__PURE__ */ jsxs9(Text9, { color: "green", children: [
|
|
1572
1576
|
"\u2714 ",
|
|
1573
1577
|
"\u5DF2\u6062\u590D\u5DE5\u4F5C\u533A\u53D8\u66F4 (stash pop)"
|
|
1574
|
-
] })
|
|
1578
|
+
] }),
|
|
1579
|
+
/* @__PURE__ */ jsx9(Text9, { color: "gray", dimColor: true, children: "\u6309\u4EFB\u610F\u952E\u9000\u51FA" })
|
|
1575
1580
|
] });
|
|
1576
1581
|
}
|
|
1577
1582
|
const total = orderedHashes.current.length;
|
|
@@ -1608,13 +1613,139 @@ function ResultPanel({ selectedHashes, useMainline, noCommit, stashed, onStashRe
|
|
|
1608
1613
|
" git reset HEAD ",
|
|
1609
1614
|
/* @__PURE__ */ jsx9(Text9, { color: "gray", dimColor: true, children: "# \u6216\u653E\u5F03" })
|
|
1610
1615
|
] })
|
|
1611
|
-
] }) : /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", children: /* @__PURE__ */ jsx9(Text9, { color: "gray", dimColor: true, children: " \u5DF2\u4FDD\u7559\u539F\u59CB commit \u4FE1\u606F\uFF0C\u53EF\u901A\u8FC7 git log \u67E5\u770B" }) })
|
|
1616
|
+
] }) : /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", children: /* @__PURE__ */ jsx9(Text9, { color: "gray", dimColor: true, children: " \u5DF2\u4FDD\u7559\u539F\u59CB commit \u4FE1\u606F\uFF0C\u53EF\u901A\u8FC7 git log \u67E5\u770B" }) }),
|
|
1617
|
+
/* @__PURE__ */ jsx9(Text9, { color: "gray", dimColor: true, children: "\u6309\u4EFB\u610F\u952E\u9000\u51FA" })
|
|
1612
1618
|
] });
|
|
1613
1619
|
}
|
|
1614
1620
|
|
|
1621
|
+
// src/components/update-banner.tsx
|
|
1622
|
+
import { useState as useState8, useEffect as useEffect6 } from "react";
|
|
1623
|
+
import { Box as Box10, Text as Text10 } from "ink";
|
|
1624
|
+
|
|
1625
|
+
// src/utils/update-check.ts
|
|
1626
|
+
import https from "https";
|
|
1627
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync } from "fs";
|
|
1628
|
+
import { join as join2 } from "path";
|
|
1629
|
+
import { homedir } from "os";
|
|
1630
|
+
var PKG_NAME = "git-sync-tui";
|
|
1631
|
+
var CHECK_INTERVAL = 24 * 60 * 60 * 1e3;
|
|
1632
|
+
var REQUEST_TIMEOUT = 3e3;
|
|
1633
|
+
function getCachePath() {
|
|
1634
|
+
const dir = join2(homedir(), ".config", "git-sync-tui");
|
|
1635
|
+
return join2(dir, "update-check.json");
|
|
1636
|
+
}
|
|
1637
|
+
function readCache() {
|
|
1638
|
+
try {
|
|
1639
|
+
const raw = readFileSync2(getCachePath(), "utf-8");
|
|
1640
|
+
return JSON.parse(raw);
|
|
1641
|
+
} catch {
|
|
1642
|
+
return null;
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
function writeCache(data) {
|
|
1646
|
+
try {
|
|
1647
|
+
const dir = join2(homedir(), ".config", "git-sync-tui");
|
|
1648
|
+
mkdirSync(dir, { recursive: true });
|
|
1649
|
+
writeFileSync2(getCachePath(), JSON.stringify(data));
|
|
1650
|
+
} catch {
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
function fetchLatestVersion() {
|
|
1654
|
+
return new Promise((resolve) => {
|
|
1655
|
+
const req = https.get(
|
|
1656
|
+
`https://registry.npmjs.org/${PKG_NAME}/latest`,
|
|
1657
|
+
{ timeout: REQUEST_TIMEOUT, headers: { Accept: "application/json" } },
|
|
1658
|
+
(res) => {
|
|
1659
|
+
if (res.statusCode !== 200) {
|
|
1660
|
+
resolve(null);
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
let data = "";
|
|
1664
|
+
res.on("data", (chunk) => {
|
|
1665
|
+
data += chunk.toString();
|
|
1666
|
+
});
|
|
1667
|
+
res.on("end", () => {
|
|
1668
|
+
try {
|
|
1669
|
+
const json = JSON.parse(data);
|
|
1670
|
+
resolve(json.version || null);
|
|
1671
|
+
} catch {
|
|
1672
|
+
resolve(null);
|
|
1673
|
+
}
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
);
|
|
1677
|
+
req.on("error", () => resolve(null));
|
|
1678
|
+
req.on("timeout", () => {
|
|
1679
|
+
req.destroy();
|
|
1680
|
+
resolve(null);
|
|
1681
|
+
});
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1684
|
+
function compareVersions(current, latest) {
|
|
1685
|
+
const parse = (v) => v.replace(/^v/, "").split(".").map(Number);
|
|
1686
|
+
const c = parse(current);
|
|
1687
|
+
const l = parse(latest);
|
|
1688
|
+
for (let i = 0; i < 3; i++) {
|
|
1689
|
+
if ((l[i] || 0) > (c[i] || 0)) return true;
|
|
1690
|
+
if ((l[i] || 0) < (c[i] || 0)) return false;
|
|
1691
|
+
}
|
|
1692
|
+
return false;
|
|
1693
|
+
}
|
|
1694
|
+
async function checkForUpdate(currentVersion) {
|
|
1695
|
+
const noUpdate = { hasUpdate: false, current: currentVersion, latest: currentVersion };
|
|
1696
|
+
const cache = readCache();
|
|
1697
|
+
if (cache && Date.now() - cache.checkedAt < CHECK_INTERVAL) {
|
|
1698
|
+
return {
|
|
1699
|
+
hasUpdate: compareVersions(currentVersion, cache.latest),
|
|
1700
|
+
current: currentVersion,
|
|
1701
|
+
latest: cache.latest
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
const latest = await fetchLatestVersion();
|
|
1705
|
+
if (!latest) return noUpdate;
|
|
1706
|
+
writeCache({ latest, checkedAt: Date.now() });
|
|
1707
|
+
return {
|
|
1708
|
+
hasUpdate: compareVersions(currentVersion, latest),
|
|
1709
|
+
current: currentVersion,
|
|
1710
|
+
latest
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
// src/components/update-banner.tsx
|
|
1715
|
+
import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
1716
|
+
function UpdateBanner({ currentVersion }) {
|
|
1717
|
+
const [info, setInfo] = useState8(null);
|
|
1718
|
+
useEffect6(() => {
|
|
1719
|
+
let cancelled = false;
|
|
1720
|
+
checkForUpdate(currentVersion).then((result) => {
|
|
1721
|
+
if (!cancelled && result.hasUpdate) {
|
|
1722
|
+
setInfo(result);
|
|
1723
|
+
}
|
|
1724
|
+
});
|
|
1725
|
+
return () => {
|
|
1726
|
+
cancelled = true;
|
|
1727
|
+
};
|
|
1728
|
+
}, [currentVersion]);
|
|
1729
|
+
if (!info) return null;
|
|
1730
|
+
return /* @__PURE__ */ jsx10(Box10, { marginTop: 1, children: /* @__PURE__ */ jsxs10(Text10, { color: "yellow", children: [
|
|
1731
|
+
"\u{1F4A1} ",
|
|
1732
|
+
"\u65B0\u7248\u672C\u53EF\u7528 ",
|
|
1733
|
+
/* @__PURE__ */ jsx10(Text10, { bold: true, color: "green", children: info.latest }),
|
|
1734
|
+
/* @__PURE__ */ jsxs10(Text10, { color: "gray", children: [
|
|
1735
|
+
" (\u5F53\u524D ",
|
|
1736
|
+
info.current,
|
|
1737
|
+
")"
|
|
1738
|
+
] }),
|
|
1739
|
+
/* @__PURE__ */ jsx10(Text10, { color: "cyan", children: " \u2192 npm i -g git-sync-tui" })
|
|
1740
|
+
] }) });
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1615
1743
|
// src/app.tsx
|
|
1616
1744
|
import { execSync } from "child_process";
|
|
1617
|
-
import {
|
|
1745
|
+
import { createRequire } from "module";
|
|
1746
|
+
import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
1747
|
+
var require2 = createRequire(import.meta.url);
|
|
1748
|
+
var { version: APP_VERSION } = require2("../package.json");
|
|
1618
1749
|
var STEP_NUMBER = {
|
|
1619
1750
|
checking: 0,
|
|
1620
1751
|
"stash-recovery": 0,
|
|
@@ -1630,17 +1761,17 @@ var STEP_DEBOUNCE = 100;
|
|
|
1630
1761
|
function App({ initialRemote, initialBranch }) {
|
|
1631
1762
|
const { exit } = useApp();
|
|
1632
1763
|
const entryStep = initialRemote && initialBranch ? "branch-check" : initialRemote ? "branch" : "remote";
|
|
1633
|
-
const [step, setStepRaw] =
|
|
1634
|
-
const [inputReady, setInputReady] =
|
|
1635
|
-
const [remote2, setRemote] =
|
|
1636
|
-
const [branch2, setBranch] =
|
|
1637
|
-
const [selectedHashes, setSelectedHashes] =
|
|
1638
|
-
const [commits2, setCommits] =
|
|
1639
|
-
const [hasMerge, setHasMerge] =
|
|
1640
|
-
const [useMainline, setUseMainline] =
|
|
1641
|
-
const [noCommit, setNoCommit] =
|
|
1642
|
-
const [stashed, setStashed] =
|
|
1643
|
-
const [guardTimestamp, setGuardTimestamp] =
|
|
1764
|
+
const [step, setStepRaw] = useState9("checking");
|
|
1765
|
+
const [inputReady, setInputReady] = useState9(true);
|
|
1766
|
+
const [remote2, setRemote] = useState9(initialRemote || "");
|
|
1767
|
+
const [branch2, setBranch] = useState9(initialBranch || "");
|
|
1768
|
+
const [selectedHashes, setSelectedHashes] = useState9([]);
|
|
1769
|
+
const [commits2, setCommits] = useState9([]);
|
|
1770
|
+
const [hasMerge, setHasMerge] = useState9(false);
|
|
1771
|
+
const [useMainline, setUseMainline] = useState9(false);
|
|
1772
|
+
const [noCommit, setNoCommit] = useState9(false);
|
|
1773
|
+
const [stashed, setStashed] = useState9(false);
|
|
1774
|
+
const [guardTimestamp, setGuardTimestamp] = useState9();
|
|
1644
1775
|
const stashedRef = useRef5(false);
|
|
1645
1776
|
const stashRestoredRef = useRef5(false);
|
|
1646
1777
|
const mountedRef = useRef5(true);
|
|
@@ -1671,7 +1802,7 @@ function App({ initialRemote, initialBranch }) {
|
|
|
1671
1802
|
stashRestoredRef.current = true;
|
|
1672
1803
|
removeStashGuard();
|
|
1673
1804
|
}, []);
|
|
1674
|
-
|
|
1805
|
+
useEffect7(() => {
|
|
1675
1806
|
mountedRef.current = true;
|
|
1676
1807
|
async function check() {
|
|
1677
1808
|
const guard = await checkStashGuard();
|
|
@@ -1741,10 +1872,10 @@ function App({ initialRemote, initialBranch }) {
|
|
|
1741
1872
|
exit();
|
|
1742
1873
|
}
|
|
1743
1874
|
}, [setStep, restoreStashSync, exit]);
|
|
1744
|
-
return /* @__PURE__ */
|
|
1745
|
-
/* @__PURE__ */
|
|
1746
|
-
step === "checking" && /* @__PURE__ */
|
|
1747
|
-
step === "stash-recovery" && inputReady && /* @__PURE__ */
|
|
1875
|
+
return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", children: [
|
|
1876
|
+
/* @__PURE__ */ jsx11(AppHeader, { step: STEP_NUMBER[step], stashed, noCommit }),
|
|
1877
|
+
step === "checking" && /* @__PURE__ */ jsx11(Spinner7, { label: "\u68C0\u67E5\u5DE5\u4F5C\u533A\u72B6\u6001..." }),
|
|
1878
|
+
step === "stash-recovery" && inputReady && /* @__PURE__ */ jsx11(
|
|
1748
1879
|
StashRecovery,
|
|
1749
1880
|
{
|
|
1750
1881
|
timestamp: guardTimestamp,
|
|
@@ -1752,14 +1883,14 @@ function App({ initialRemote, initialBranch }) {
|
|
|
1752
1883
|
onSkip: skipStashRecover
|
|
1753
1884
|
}
|
|
1754
1885
|
),
|
|
1755
|
-
step === "stash-prompt" && inputReady && /* @__PURE__ */
|
|
1886
|
+
step === "stash-prompt" && inputReady && /* @__PURE__ */ jsx11(
|
|
1756
1887
|
StashPrompt,
|
|
1757
1888
|
{
|
|
1758
1889
|
onConfirm: doStash,
|
|
1759
1890
|
onSkip: () => setStep(entryStep)
|
|
1760
1891
|
}
|
|
1761
1892
|
),
|
|
1762
|
-
step === "remote" && inputReady && /* @__PURE__ */
|
|
1893
|
+
step === "remote" && inputReady && /* @__PURE__ */ jsx11(
|
|
1763
1894
|
RemoteSelect,
|
|
1764
1895
|
{
|
|
1765
1896
|
onSelect: (r) => {
|
|
@@ -1769,7 +1900,7 @@ function App({ initialRemote, initialBranch }) {
|
|
|
1769
1900
|
onBack: () => goBack("remote")
|
|
1770
1901
|
}
|
|
1771
1902
|
),
|
|
1772
|
-
step === "branch" && inputReady && /* @__PURE__ */
|
|
1903
|
+
step === "branch" && inputReady && /* @__PURE__ */ jsx11(
|
|
1773
1904
|
BranchSelect,
|
|
1774
1905
|
{
|
|
1775
1906
|
remote: remote2,
|
|
@@ -1780,7 +1911,7 @@ function App({ initialRemote, initialBranch }) {
|
|
|
1780
1911
|
onBack: () => goBack("branch")
|
|
1781
1912
|
}
|
|
1782
1913
|
),
|
|
1783
|
-
step === "branch-check" && inputReady && /* @__PURE__ */
|
|
1914
|
+
step === "branch-check" && inputReady && /* @__PURE__ */ jsx11(
|
|
1784
1915
|
BranchCheck,
|
|
1785
1916
|
{
|
|
1786
1917
|
targetBranch: branch2,
|
|
@@ -1788,7 +1919,7 @@ function App({ initialRemote, initialBranch }) {
|
|
|
1788
1919
|
onBack: () => goBack("branch-check")
|
|
1789
1920
|
}
|
|
1790
1921
|
),
|
|
1791
|
-
step === "commits" && inputReady && /* @__PURE__ */
|
|
1922
|
+
step === "commits" && inputReady && /* @__PURE__ */ jsx11(
|
|
1792
1923
|
CommitList,
|
|
1793
1924
|
{
|
|
1794
1925
|
remote: remote2,
|
|
@@ -1801,7 +1932,7 @@ function App({ initialRemote, initialBranch }) {
|
|
|
1801
1932
|
onBack: () => goBack("commits")
|
|
1802
1933
|
}
|
|
1803
1934
|
),
|
|
1804
|
-
step === "confirm" && inputReady && /* @__PURE__ */
|
|
1935
|
+
step === "confirm" && inputReady && /* @__PURE__ */ jsx11(
|
|
1805
1936
|
ConfirmPanel,
|
|
1806
1937
|
{
|
|
1807
1938
|
commits: commits2,
|
|
@@ -1815,7 +1946,7 @@ function App({ initialRemote, initialBranch }) {
|
|
|
1815
1946
|
onCancel: () => goBack("confirm")
|
|
1816
1947
|
}
|
|
1817
1948
|
),
|
|
1818
|
-
step === "result" && /* @__PURE__ */
|
|
1949
|
+
step === "result" && /* @__PURE__ */ jsx11(
|
|
1819
1950
|
ResultPanel,
|
|
1820
1951
|
{
|
|
1821
1952
|
selectedHashes,
|
|
@@ -1828,12 +1959,16 @@ function App({ initialRemote, initialBranch }) {
|
|
|
1828
1959
|
exit();
|
|
1829
1960
|
}
|
|
1830
1961
|
}
|
|
1831
|
-
)
|
|
1962
|
+
),
|
|
1963
|
+
/* @__PURE__ */ jsx11(UpdateBanner, { currentVersion: APP_VERSION })
|
|
1832
1964
|
] });
|
|
1833
1965
|
}
|
|
1834
1966
|
|
|
1835
1967
|
// src/cli-runner.ts
|
|
1836
1968
|
import { createInterface } from "readline";
|
|
1969
|
+
import { createRequire as createRequire2 } from "module";
|
|
1970
|
+
var require3 = createRequire2(import.meta.url);
|
|
1971
|
+
var { version: APP_VERSION2 } = require3("../package.json");
|
|
1837
1972
|
function log(msg) {
|
|
1838
1973
|
process.stdout.write(msg + "\n");
|
|
1839
1974
|
}
|
|
@@ -1988,16 +2123,24 @@ ${stat}`);
|
|
|
1988
2123
|
process.exit(1);
|
|
1989
2124
|
}
|
|
1990
2125
|
}
|
|
2126
|
+
async function printUpdateNotice() {
|
|
2127
|
+
const info = await checkForUpdate(APP_VERSION2);
|
|
2128
|
+
if (info.hasUpdate) {
|
|
2129
|
+
log(`
|
|
2130
|
+
\u{1F4A1} \u65B0\u7248\u672C\u53EF\u7528 ${info.latest} (\u5F53\u524D ${info.current}) \u2192 npm i -g git-sync-tui`);
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
1991
2133
|
async function runCli(opts) {
|
|
1992
2134
|
if (opts.list) {
|
|
1993
2135
|
await runList(opts);
|
|
1994
2136
|
} else {
|
|
1995
2137
|
await runExec(opts);
|
|
1996
2138
|
}
|
|
2139
|
+
await printUpdateNotice();
|
|
1997
2140
|
}
|
|
1998
2141
|
|
|
1999
2142
|
// src/cli.tsx
|
|
2000
|
-
import { jsx as
|
|
2143
|
+
import { jsx as jsx12 } from "react/jsx-runtime";
|
|
2001
2144
|
var cli = meow(
|
|
2002
2145
|
`
|
|
2003
2146
|
\u7528\u6CD5
|
|
@@ -2071,5 +2214,5 @@ if (isCliMode) {
|
|
|
2071
2214
|
process.exit(1);
|
|
2072
2215
|
});
|
|
2073
2216
|
} else {
|
|
2074
|
-
render(/* @__PURE__ */
|
|
2217
|
+
render(/* @__PURE__ */ jsx12(App, { initialRemote: remote, initialBranch: branch }));
|
|
2075
2218
|
}
|
package/package.json
CHANGED