git-sync-tui 0.1.6 → 0.1.8
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 +214 -73
- 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
|
@@ -1,18 +1,12 @@
|
|
|
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
|
-
});
|
|
8
2
|
|
|
9
3
|
// src/cli.tsx
|
|
10
4
|
import { render } from "ink";
|
|
11
5
|
import meow from "meow";
|
|
12
6
|
|
|
13
7
|
// src/app.tsx
|
|
14
|
-
import { useState as
|
|
15
|
-
import { Box as
|
|
8
|
+
import { useState as useState9, useEffect as useEffect7, useRef as useRef5, useCallback as useCallback4 } from "react";
|
|
9
|
+
import { Box as Box11, useApp } from "ink";
|
|
16
10
|
import { Spinner as Spinner7 } from "@inkjs/ui";
|
|
17
11
|
|
|
18
12
|
// src/components/ui.tsx
|
|
@@ -140,11 +134,13 @@ import { Spinner } from "@inkjs/ui";
|
|
|
140
134
|
// src/utils/git.ts
|
|
141
135
|
import simpleGit from "simple-git";
|
|
142
136
|
import { existsSync, writeFileSync, unlinkSync, readFileSync } from "fs";
|
|
137
|
+
import { execSync } from "child_process";
|
|
143
138
|
import { join } from "path";
|
|
144
139
|
var gitInstance = null;
|
|
145
140
|
function getGit(cwd) {
|
|
146
|
-
if (
|
|
147
|
-
|
|
141
|
+
if (cwd) return simpleGit(cwd);
|
|
142
|
+
if (!gitInstance) {
|
|
143
|
+
gitInstance = simpleGit();
|
|
148
144
|
}
|
|
149
145
|
return gitInstance;
|
|
150
146
|
}
|
|
@@ -227,13 +223,14 @@ async function getMultiCommitStat(hashes) {
|
|
|
227
223
|
if (hashes.length === 0) return "";
|
|
228
224
|
const git = getGit();
|
|
229
225
|
try {
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
${result.trim()}`
|
|
235
|
-
|
|
236
|
-
|
|
226
|
+
const results = await Promise.all(
|
|
227
|
+
hashes.map(async (hash) => {
|
|
228
|
+
const result = await git.raw(["diff-tree", "--stat", "--no-commit-id", "-r", hash]);
|
|
229
|
+
return result.trim() ? `${hash.substring(0, 7)}:
|
|
230
|
+
${result.trim()}` : "";
|
|
231
|
+
})
|
|
232
|
+
);
|
|
233
|
+
return results.filter(Boolean).join("\n\n");
|
|
237
234
|
} catch {
|
|
238
235
|
return "(\u65E0\u6CD5\u83B7\u53D6 stat \u4FE1\u606F)";
|
|
239
236
|
}
|
|
@@ -454,8 +451,7 @@ async function removeStashGuard() {
|
|
|
454
451
|
}
|
|
455
452
|
function removeStashGuardSync() {
|
|
456
453
|
try {
|
|
457
|
-
const
|
|
458
|
-
const gitDir = String(execSync2("git rev-parse --git-dir", { encoding: "utf-8" })).trim();
|
|
454
|
+
const gitDir = String(execSync("git rev-parse --git-dir", { encoding: "utf-8" })).trim();
|
|
459
455
|
const guardPath = join(gitDir, STASH_GUARD_FILE);
|
|
460
456
|
if (existsSync(guardPath)) {
|
|
461
457
|
unlinkSync(guardPath);
|
|
@@ -542,10 +538,12 @@ function useAsync(fn, deps = []) {
|
|
|
542
538
|
loading: true,
|
|
543
539
|
error: null
|
|
544
540
|
});
|
|
541
|
+
const fnRef = useRef(fn);
|
|
542
|
+
fnRef.current = fn;
|
|
545
543
|
const load = useCallback(async () => {
|
|
546
544
|
setState({ data: null, loading: true, error: null });
|
|
547
545
|
try {
|
|
548
|
-
const data = await
|
|
546
|
+
const data = await fnRef.current();
|
|
549
547
|
setState({ data, loading: false, error: null });
|
|
550
548
|
} catch (err) {
|
|
551
549
|
setState({ data: null, loading: false, error: err.message });
|
|
@@ -612,20 +610,23 @@ function useCommits(remote2, branch2, pageSize = 100) {
|
|
|
612
610
|
function useCommitStat(hashes) {
|
|
613
611
|
const [stat, setStat] = useState2("");
|
|
614
612
|
const [loading, setLoading] = useState2(false);
|
|
613
|
+
const hashKey = hashes.join(",");
|
|
614
|
+
const stableHashes = useRef(hashes);
|
|
615
|
+
stableHashes.current = hashes;
|
|
615
616
|
useEffect2(() => {
|
|
616
|
-
if (
|
|
617
|
+
if (stableHashes.current.length === 0) {
|
|
617
618
|
setStat("");
|
|
618
619
|
return;
|
|
619
620
|
}
|
|
620
621
|
setLoading(true);
|
|
621
|
-
getMultiCommitStat(
|
|
622
|
+
getMultiCommitStat(stableHashes.current).then((s) => {
|
|
622
623
|
setStat(s);
|
|
623
624
|
setLoading(false);
|
|
624
625
|
}).catch(() => {
|
|
625
626
|
setStat("(\u83B7\u53D6\u5931\u8D25)");
|
|
626
627
|
setLoading(false);
|
|
627
628
|
});
|
|
628
|
-
}, [
|
|
629
|
+
}, [hashKey]);
|
|
629
630
|
return { stat, loading };
|
|
630
631
|
}
|
|
631
632
|
|
|
@@ -1180,6 +1181,8 @@ function BranchCheck({ targetBranch, onContinue, onBack }) {
|
|
|
1180
1181
|
const [error2, setError] = useState6(null);
|
|
1181
1182
|
const [matched, setMatched] = useState6(false);
|
|
1182
1183
|
const autoCreated = useRef3(false);
|
|
1184
|
+
const onContinueRef = useRef3(onContinue);
|
|
1185
|
+
onContinueRef.current = onContinue;
|
|
1183
1186
|
useEffect4(() => {
|
|
1184
1187
|
getCurrentBranch().then((branch2) => {
|
|
1185
1188
|
setCurrentBranch(branch2);
|
|
@@ -1189,7 +1192,7 @@ function BranchCheck({ targetBranch, onContinue, onBack }) {
|
|
|
1189
1192
|
});
|
|
1190
1193
|
}, [targetBranch]);
|
|
1191
1194
|
useEffect4(() => {
|
|
1192
|
-
if (matched)
|
|
1195
|
+
if (matched) onContinueRef.current();
|
|
1193
1196
|
}, [matched]);
|
|
1194
1197
|
useEffect4(() => {
|
|
1195
1198
|
if (currentBranch === null || matched || autoCreated.current) return;
|
|
@@ -1197,7 +1200,7 @@ function BranchCheck({ targetBranch, onContinue, onBack }) {
|
|
|
1197
1200
|
autoCreated.current = true;
|
|
1198
1201
|
setCreating(true);
|
|
1199
1202
|
createBranchFrom(targetBranch, currentBranch).then(() => {
|
|
1200
|
-
|
|
1203
|
+
onContinueRef.current();
|
|
1201
1204
|
}).catch((err) => {
|
|
1202
1205
|
setCreating(false);
|
|
1203
1206
|
setError(err.message);
|
|
@@ -1333,7 +1336,7 @@ function ConfirmPanel({ commits: commits2, selectedHashes, hasMerge, useMainline
|
|
|
1333
1336
|
}
|
|
1334
1337
|
|
|
1335
1338
|
// src/components/result-panel.tsx
|
|
1336
|
-
import { useState as useState7, useEffect as useEffect5, useCallback as
|
|
1339
|
+
import { useState as useState7, useEffect as useEffect5, useCallback as useCallback3, useRef as useRef4 } from "react";
|
|
1337
1340
|
import { Box as Box9, Text as Text9, useInput as useInput8 } from "ink";
|
|
1338
1341
|
import { Spinner as Spinner6 } from "@inkjs/ui";
|
|
1339
1342
|
import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
@@ -1349,7 +1352,7 @@ function ResultPanel({ selectedHashes, useMainline, noCommit, stashed, onStashRe
|
|
|
1349
1352
|
const remainingRef = useRef4([]);
|
|
1350
1353
|
const backupBranchRef = useRef4("");
|
|
1351
1354
|
const orderedHashes = useRef4([...selectedHashes].reverse());
|
|
1352
|
-
const tryRestoreStash =
|
|
1355
|
+
const tryRestoreStash = useCallback3(async () => {
|
|
1353
1356
|
if (!stashed) return true;
|
|
1354
1357
|
setPhase("restoring");
|
|
1355
1358
|
const ok = await stashPop();
|
|
@@ -1357,7 +1360,7 @@ function ResultPanel({ selectedHashes, useMainline, noCommit, stashed, onStashRe
|
|
|
1357
1360
|
if (ok) onStashRestored();
|
|
1358
1361
|
return ok;
|
|
1359
1362
|
}, [stashed, onStashRestored]);
|
|
1360
|
-
const finishAll =
|
|
1363
|
+
const finishAll = useCallback3(async () => {
|
|
1361
1364
|
if (noCommit) {
|
|
1362
1365
|
const stat = await getStagedStat();
|
|
1363
1366
|
setStagedStat(stat);
|
|
@@ -1368,7 +1371,7 @@ function ResultPanel({ selectedHashes, useMainline, noCommit, stashed, onStashRe
|
|
|
1368
1371
|
await tryRestoreStash();
|
|
1369
1372
|
setPhase("done");
|
|
1370
1373
|
}, [noCommit, tryRestoreStash]);
|
|
1371
|
-
const executeFrom =
|
|
1374
|
+
const executeFrom = useCallback3(async (startIndex) => {
|
|
1372
1375
|
const hashes = orderedHashes.current;
|
|
1373
1376
|
for (let i = startIndex; i < hashes.length; i++) {
|
|
1374
1377
|
setCurrentIndex(i);
|
|
@@ -1389,7 +1392,7 @@ function ResultPanel({ selectedHashes, useMainline, noCommit, stashed, onStashRe
|
|
|
1389
1392
|
executeFrom(0);
|
|
1390
1393
|
});
|
|
1391
1394
|
}, []);
|
|
1392
|
-
const continueRemaining =
|
|
1395
|
+
const continueRemaining = useCallback3(async () => {
|
|
1393
1396
|
const remaining = remainingRef.current;
|
|
1394
1397
|
if (remaining.length === 0) {
|
|
1395
1398
|
await finishAll();
|
|
@@ -1409,7 +1412,7 @@ function ResultPanel({ selectedHashes, useMainline, noCommit, stashed, onStashRe
|
|
|
1409
1412
|
}
|
|
1410
1413
|
await finishAll();
|
|
1411
1414
|
}, [useMainline, noCommit, finishAll]);
|
|
1412
|
-
const handleContinue =
|
|
1415
|
+
const handleContinue = useCallback3(async () => {
|
|
1413
1416
|
const conflicts = await getConflictFiles();
|
|
1414
1417
|
if (conflicts.length > 0) {
|
|
1415
1418
|
setConflictFiles(conflicts);
|
|
@@ -1442,14 +1445,14 @@ function ResultPanel({ selectedHashes, useMainline, noCommit, stashed, onStashRe
|
|
|
1442
1445
|
await continueRemaining();
|
|
1443
1446
|
}
|
|
1444
1447
|
}, [noCommit, continueRemaining]);
|
|
1445
|
-
const handleSkip =
|
|
1448
|
+
const handleSkip = useCallback3(async () => {
|
|
1446
1449
|
setPhase("continuing");
|
|
1447
1450
|
setErrorMsg("");
|
|
1448
1451
|
await skipCherryPick();
|
|
1449
1452
|
setSkippedCount((c) => c + 1);
|
|
1450
1453
|
await continueRemaining();
|
|
1451
1454
|
}, [continueRemaining]);
|
|
1452
|
-
const handleAbort =
|
|
1455
|
+
const handleAbort = useCallback3(async () => {
|
|
1453
1456
|
setPhase("aborting");
|
|
1454
1457
|
await abortCherryPick();
|
|
1455
1458
|
if (backupBranchRef.current) {
|
|
@@ -1463,6 +1466,10 @@ function ResultPanel({ selectedHashes, useMainline, noCommit, stashed, onStashRe
|
|
|
1463
1466
|
setPhase("aborted");
|
|
1464
1467
|
}, [tryRestoreStash]);
|
|
1465
1468
|
useInput8((input, key) => {
|
|
1469
|
+
if (phase === "done" || phase === "aborted") {
|
|
1470
|
+
onDone();
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1466
1473
|
if (phase === "conflict") {
|
|
1467
1474
|
if (input === "c" || input === "C") {
|
|
1468
1475
|
handleContinue();
|
|
@@ -1571,7 +1578,8 @@ function ResultPanel({ selectedHashes, useMainline, noCommit, stashed, onStashRe
|
|
|
1571
1578
|
stashed && stashRestored === true && /* @__PURE__ */ jsxs9(Text9, { color: "green", children: [
|
|
1572
1579
|
"\u2714 ",
|
|
1573
1580
|
"\u5DF2\u6062\u590D\u5DE5\u4F5C\u533A\u53D8\u66F4 (stash pop)"
|
|
1574
|
-
] })
|
|
1581
|
+
] }),
|
|
1582
|
+
/* @__PURE__ */ jsx9(Text9, { color: "gray", dimColor: true, children: "\u6309\u4EFB\u610F\u952E\u9000\u51FA" })
|
|
1575
1583
|
] });
|
|
1576
1584
|
}
|
|
1577
1585
|
const total = orderedHashes.current.length;
|
|
@@ -1608,13 +1616,137 @@ function ResultPanel({ selectedHashes, useMainline, noCommit, stashed, onStashRe
|
|
|
1608
1616
|
" git reset HEAD ",
|
|
1609
1617
|
/* @__PURE__ */ jsx9(Text9, { color: "gray", dimColor: true, children: "# \u6216\u653E\u5F03" })
|
|
1610
1618
|
] })
|
|
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" }) })
|
|
1619
|
+
] }) : /* @__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" }) }),
|
|
1620
|
+
/* @__PURE__ */ jsx9(Text9, { color: "gray", dimColor: true, children: "\u6309\u4EFB\u610F\u952E\u9000\u51FA" })
|
|
1612
1621
|
] });
|
|
1613
1622
|
}
|
|
1614
1623
|
|
|
1615
|
-
// src/
|
|
1616
|
-
import {
|
|
1624
|
+
// src/components/update-banner.tsx
|
|
1625
|
+
import { useState as useState8, useEffect as useEffect6 } from "react";
|
|
1626
|
+
import { Box as Box10, Text as Text10 } from "ink";
|
|
1627
|
+
|
|
1628
|
+
// src/utils/update-check.ts
|
|
1629
|
+
import https from "https";
|
|
1630
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync } from "fs";
|
|
1631
|
+
import { join as join2 } from "path";
|
|
1632
|
+
import { homedir } from "os";
|
|
1633
|
+
var PKG_NAME = "git-sync-tui";
|
|
1634
|
+
var CHECK_INTERVAL = 24 * 60 * 60 * 1e3;
|
|
1635
|
+
var REQUEST_TIMEOUT = 3e3;
|
|
1636
|
+
function getCachePath() {
|
|
1637
|
+
const dir = join2(homedir(), ".config", "git-sync-tui");
|
|
1638
|
+
return join2(dir, "update-check.json");
|
|
1639
|
+
}
|
|
1640
|
+
function readCache() {
|
|
1641
|
+
try {
|
|
1642
|
+
const raw = readFileSync2(getCachePath(), "utf-8");
|
|
1643
|
+
return JSON.parse(raw);
|
|
1644
|
+
} catch {
|
|
1645
|
+
return null;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
function writeCache(data) {
|
|
1649
|
+
try {
|
|
1650
|
+
const dir = join2(homedir(), ".config", "git-sync-tui");
|
|
1651
|
+
mkdirSync(dir, { recursive: true });
|
|
1652
|
+
writeFileSync2(getCachePath(), JSON.stringify(data));
|
|
1653
|
+
} catch {
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
function fetchLatestVersion() {
|
|
1657
|
+
return new Promise((resolve) => {
|
|
1658
|
+
const req = https.get(
|
|
1659
|
+
`https://registry.npmjs.org/${PKG_NAME}/latest`,
|
|
1660
|
+
{ timeout: REQUEST_TIMEOUT, headers: { Accept: "application/json" } },
|
|
1661
|
+
(res) => {
|
|
1662
|
+
if (res.statusCode !== 200) {
|
|
1663
|
+
resolve(null);
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
let data = "";
|
|
1667
|
+
res.on("data", (chunk) => {
|
|
1668
|
+
data += chunk.toString();
|
|
1669
|
+
});
|
|
1670
|
+
res.on("end", () => {
|
|
1671
|
+
try {
|
|
1672
|
+
const json = JSON.parse(data);
|
|
1673
|
+
resolve(json.version || null);
|
|
1674
|
+
} catch {
|
|
1675
|
+
resolve(null);
|
|
1676
|
+
}
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
);
|
|
1680
|
+
req.on("error", () => resolve(null));
|
|
1681
|
+
req.on("timeout", () => {
|
|
1682
|
+
req.destroy();
|
|
1683
|
+
resolve(null);
|
|
1684
|
+
});
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
function compareVersions(current, latest) {
|
|
1688
|
+
const parse = (v) => v.replace(/^v/, "").split(".").map(Number);
|
|
1689
|
+
const c = parse(current);
|
|
1690
|
+
const l = parse(latest);
|
|
1691
|
+
for (let i = 0; i < 3; i++) {
|
|
1692
|
+
if ((l[i] || 0) > (c[i] || 0)) return true;
|
|
1693
|
+
if ((l[i] || 0) < (c[i] || 0)) return false;
|
|
1694
|
+
}
|
|
1695
|
+
return false;
|
|
1696
|
+
}
|
|
1697
|
+
async function checkForUpdate(currentVersion) {
|
|
1698
|
+
const noUpdate = { hasUpdate: false, current: currentVersion, latest: currentVersion };
|
|
1699
|
+
const cache = readCache();
|
|
1700
|
+
if (cache && Date.now() - cache.checkedAt < CHECK_INTERVAL) {
|
|
1701
|
+
return {
|
|
1702
|
+
hasUpdate: compareVersions(currentVersion, cache.latest),
|
|
1703
|
+
current: currentVersion,
|
|
1704
|
+
latest: cache.latest
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
const latest = await fetchLatestVersion();
|
|
1708
|
+
if (!latest) return noUpdate;
|
|
1709
|
+
writeCache({ latest, checkedAt: Date.now() });
|
|
1710
|
+
return {
|
|
1711
|
+
hasUpdate: compareVersions(currentVersion, latest),
|
|
1712
|
+
current: currentVersion,
|
|
1713
|
+
latest
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
// src/components/update-banner.tsx
|
|
1617
1718
|
import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
1719
|
+
function UpdateBanner({ currentVersion }) {
|
|
1720
|
+
const [info, setInfo] = useState8(null);
|
|
1721
|
+
useEffect6(() => {
|
|
1722
|
+
let cancelled = false;
|
|
1723
|
+
checkForUpdate(currentVersion).then((result) => {
|
|
1724
|
+
if (!cancelled && result.hasUpdate) {
|
|
1725
|
+
setInfo(result);
|
|
1726
|
+
}
|
|
1727
|
+
});
|
|
1728
|
+
return () => {
|
|
1729
|
+
cancelled = true;
|
|
1730
|
+
};
|
|
1731
|
+
}, [currentVersion]);
|
|
1732
|
+
if (!info) return null;
|
|
1733
|
+
return /* @__PURE__ */ jsx10(Box10, { marginTop: 1, children: /* @__PURE__ */ jsxs10(Text10, { color: "yellow", children: [
|
|
1734
|
+
"\u{1F4A1} ",
|
|
1735
|
+
"\u65B0\u7248\u672C\u53EF\u7528 ",
|
|
1736
|
+
/* @__PURE__ */ jsx10(Text10, { bold: true, color: "green", children: info.latest }),
|
|
1737
|
+
/* @__PURE__ */ jsxs10(Text10, { color: "gray", children: [
|
|
1738
|
+
" (\u5F53\u524D ",
|
|
1739
|
+
info.current,
|
|
1740
|
+
")"
|
|
1741
|
+
] }),
|
|
1742
|
+
/* @__PURE__ */ jsx10(Text10, { color: "cyan", children: " \u2192 npm i -g git-sync-tui" })
|
|
1743
|
+
] }) });
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
// src/app.tsx
|
|
1747
|
+
import { execSync as execSync2 } from "child_process";
|
|
1748
|
+
import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
1749
|
+
var APP_VERSION = "0.1.8";
|
|
1618
1750
|
var STEP_NUMBER = {
|
|
1619
1751
|
checking: 0,
|
|
1620
1752
|
"stash-recovery": 0,
|
|
@@ -1630,22 +1762,22 @@ var STEP_DEBOUNCE = 100;
|
|
|
1630
1762
|
function App({ initialRemote, initialBranch }) {
|
|
1631
1763
|
const { exit } = useApp();
|
|
1632
1764
|
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] =
|
|
1765
|
+
const [step, setStepRaw] = useState9("checking");
|
|
1766
|
+
const [inputReady, setInputReady] = useState9(true);
|
|
1767
|
+
const [remote2, setRemote] = useState9(initialRemote || "");
|
|
1768
|
+
const [branch2, setBranch] = useState9(initialBranch || "");
|
|
1769
|
+
const [selectedHashes, setSelectedHashes] = useState9([]);
|
|
1770
|
+
const [commits2, setCommits] = useState9([]);
|
|
1771
|
+
const [hasMerge, setHasMerge] = useState9(false);
|
|
1772
|
+
const [useMainline, setUseMainline] = useState9(false);
|
|
1773
|
+
const [noCommit, setNoCommit] = useState9(false);
|
|
1774
|
+
const [stashed, setStashed] = useState9(false);
|
|
1775
|
+
const [guardTimestamp, setGuardTimestamp] = useState9();
|
|
1644
1776
|
const stashedRef = useRef5(false);
|
|
1645
1777
|
const stashRestoredRef = useRef5(false);
|
|
1646
1778
|
const mountedRef = useRef5(true);
|
|
1647
1779
|
const debounceTimer = useRef5(null);
|
|
1648
|
-
const setStep =
|
|
1780
|
+
const setStep = useCallback4((newStep) => {
|
|
1649
1781
|
setInputReady(false);
|
|
1650
1782
|
setStepRaw(newStep);
|
|
1651
1783
|
if (debounceTimer.current) clearTimeout(debounceTimer.current);
|
|
@@ -1653,10 +1785,10 @@ function App({ initialRemote, initialBranch }) {
|
|
|
1653
1785
|
if (mountedRef.current) setInputReady(true);
|
|
1654
1786
|
}, STEP_DEBOUNCE);
|
|
1655
1787
|
}, []);
|
|
1656
|
-
const restoreStashSync =
|
|
1788
|
+
const restoreStashSync = useCallback4(() => {
|
|
1657
1789
|
if (stashedRef.current && !stashRestoredRef.current) {
|
|
1658
1790
|
try {
|
|
1659
|
-
|
|
1791
|
+
execSync2("git stash pop", { stdio: "ignore" });
|
|
1660
1792
|
stashRestoredRef.current = true;
|
|
1661
1793
|
removeStashGuardSync();
|
|
1662
1794
|
} catch {
|
|
@@ -1667,11 +1799,11 @@ function App({ initialRemote, initialBranch }) {
|
|
|
1667
1799
|
}
|
|
1668
1800
|
}
|
|
1669
1801
|
}, []);
|
|
1670
|
-
const markStashRestored =
|
|
1802
|
+
const markStashRestored = useCallback4(() => {
|
|
1671
1803
|
stashRestoredRef.current = true;
|
|
1672
1804
|
removeStashGuard();
|
|
1673
1805
|
}, []);
|
|
1674
|
-
|
|
1806
|
+
useEffect7(() => {
|
|
1675
1807
|
mountedRef.current = true;
|
|
1676
1808
|
async function check() {
|
|
1677
1809
|
const guard = await checkStashGuard();
|
|
@@ -1726,7 +1858,7 @@ function App({ initialRemote, initialBranch }) {
|
|
|
1726
1858
|
const clean = await isWorkingDirClean();
|
|
1727
1859
|
if (mountedRef.current) setStep(clean ? entryStep : "stash-prompt");
|
|
1728
1860
|
};
|
|
1729
|
-
const goBack =
|
|
1861
|
+
const goBack = useCallback4((fromStep) => {
|
|
1730
1862
|
const backMap = {
|
|
1731
1863
|
branch: "remote",
|
|
1732
1864
|
"branch-check": "branch",
|
|
@@ -1741,10 +1873,10 @@ function App({ initialRemote, initialBranch }) {
|
|
|
1741
1873
|
exit();
|
|
1742
1874
|
}
|
|
1743
1875
|
}, [setStep, restoreStashSync, exit]);
|
|
1744
|
-
return /* @__PURE__ */
|
|
1745
|
-
/* @__PURE__ */
|
|
1746
|
-
step === "checking" && /* @__PURE__ */
|
|
1747
|
-
step === "stash-recovery" && inputReady && /* @__PURE__ */
|
|
1876
|
+
return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", children: [
|
|
1877
|
+
/* @__PURE__ */ jsx11(AppHeader, { step: STEP_NUMBER[step], stashed, noCommit }),
|
|
1878
|
+
step === "checking" && /* @__PURE__ */ jsx11(Spinner7, { label: "\u68C0\u67E5\u5DE5\u4F5C\u533A\u72B6\u6001..." }),
|
|
1879
|
+
step === "stash-recovery" && inputReady && /* @__PURE__ */ jsx11(
|
|
1748
1880
|
StashRecovery,
|
|
1749
1881
|
{
|
|
1750
1882
|
timestamp: guardTimestamp,
|
|
@@ -1752,14 +1884,14 @@ function App({ initialRemote, initialBranch }) {
|
|
|
1752
1884
|
onSkip: skipStashRecover
|
|
1753
1885
|
}
|
|
1754
1886
|
),
|
|
1755
|
-
step === "stash-prompt" && inputReady && /* @__PURE__ */
|
|
1887
|
+
step === "stash-prompt" && inputReady && /* @__PURE__ */ jsx11(
|
|
1756
1888
|
StashPrompt,
|
|
1757
1889
|
{
|
|
1758
1890
|
onConfirm: doStash,
|
|
1759
1891
|
onSkip: () => setStep(entryStep)
|
|
1760
1892
|
}
|
|
1761
1893
|
),
|
|
1762
|
-
step === "remote" && inputReady && /* @__PURE__ */
|
|
1894
|
+
step === "remote" && inputReady && /* @__PURE__ */ jsx11(
|
|
1763
1895
|
RemoteSelect,
|
|
1764
1896
|
{
|
|
1765
1897
|
onSelect: (r) => {
|
|
@@ -1769,7 +1901,7 @@ function App({ initialRemote, initialBranch }) {
|
|
|
1769
1901
|
onBack: () => goBack("remote")
|
|
1770
1902
|
}
|
|
1771
1903
|
),
|
|
1772
|
-
step === "branch" && inputReady && /* @__PURE__ */
|
|
1904
|
+
step === "branch" && inputReady && /* @__PURE__ */ jsx11(
|
|
1773
1905
|
BranchSelect,
|
|
1774
1906
|
{
|
|
1775
1907
|
remote: remote2,
|
|
@@ -1780,7 +1912,7 @@ function App({ initialRemote, initialBranch }) {
|
|
|
1780
1912
|
onBack: () => goBack("branch")
|
|
1781
1913
|
}
|
|
1782
1914
|
),
|
|
1783
|
-
step === "branch-check" && inputReady && /* @__PURE__ */
|
|
1915
|
+
step === "branch-check" && inputReady && /* @__PURE__ */ jsx11(
|
|
1784
1916
|
BranchCheck,
|
|
1785
1917
|
{
|
|
1786
1918
|
targetBranch: branch2,
|
|
@@ -1788,20 +1920,22 @@ function App({ initialRemote, initialBranch }) {
|
|
|
1788
1920
|
onBack: () => goBack("branch-check")
|
|
1789
1921
|
}
|
|
1790
1922
|
),
|
|
1791
|
-
step === "commits" && inputReady && /* @__PURE__ */
|
|
1923
|
+
step === "commits" && inputReady && /* @__PURE__ */ jsx11(
|
|
1792
1924
|
CommitList,
|
|
1793
1925
|
{
|
|
1794
1926
|
remote: remote2,
|
|
1795
1927
|
branch: branch2,
|
|
1796
|
-
onSelect: (hashes, loadedCommits) => {
|
|
1928
|
+
onSelect: async (hashes, loadedCommits) => {
|
|
1797
1929
|
setSelectedHashes(hashes);
|
|
1798
1930
|
setCommits(loadedCommits);
|
|
1931
|
+
const merge = await hasMergeCommits(hashes);
|
|
1932
|
+
setHasMerge(merge);
|
|
1799
1933
|
setStep("confirm");
|
|
1800
1934
|
},
|
|
1801
1935
|
onBack: () => goBack("commits")
|
|
1802
1936
|
}
|
|
1803
1937
|
),
|
|
1804
|
-
step === "confirm" && inputReady && /* @__PURE__ */
|
|
1938
|
+
step === "confirm" && inputReady && /* @__PURE__ */ jsx11(
|
|
1805
1939
|
ConfirmPanel,
|
|
1806
1940
|
{
|
|
1807
1941
|
commits: commits2,
|
|
@@ -1815,7 +1949,7 @@ function App({ initialRemote, initialBranch }) {
|
|
|
1815
1949
|
onCancel: () => goBack("confirm")
|
|
1816
1950
|
}
|
|
1817
1951
|
),
|
|
1818
|
-
step === "result" && /* @__PURE__ */
|
|
1952
|
+
step === "result" && /* @__PURE__ */ jsx11(
|
|
1819
1953
|
ResultPanel,
|
|
1820
1954
|
{
|
|
1821
1955
|
selectedHashes,
|
|
@@ -1828,21 +1962,20 @@ function App({ initialRemote, initialBranch }) {
|
|
|
1828
1962
|
exit();
|
|
1829
1963
|
}
|
|
1830
1964
|
}
|
|
1831
|
-
)
|
|
1965
|
+
),
|
|
1966
|
+
/* @__PURE__ */ jsx11(UpdateBanner, { currentVersion: APP_VERSION })
|
|
1832
1967
|
] });
|
|
1833
1968
|
}
|
|
1834
1969
|
|
|
1835
1970
|
// src/cli-runner.ts
|
|
1836
1971
|
import { createInterface } from "readline";
|
|
1972
|
+
var APP_VERSION2 = "0.1.8";
|
|
1837
1973
|
function log(msg) {
|
|
1838
1974
|
process.stdout.write(msg + "\n");
|
|
1839
1975
|
}
|
|
1840
1976
|
function error(msg) {
|
|
1841
1977
|
process.stderr.write(msg + "\n");
|
|
1842
1978
|
}
|
|
1843
|
-
function padEnd(str, len) {
|
|
1844
|
-
return str.length >= len ? str : str + " ".repeat(len - str.length);
|
|
1845
|
-
}
|
|
1846
1979
|
async function confirm(message) {
|
|
1847
1980
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1848
1981
|
return new Promise((resolve) => {
|
|
@@ -1904,7 +2037,7 @@ async function validateBranch(remote2, branch2) {
|
|
|
1904
2037
|
log(`\u2714 \u5206\u652F '${remote2}/${branch2}'`);
|
|
1905
2038
|
}
|
|
1906
2039
|
function formatCommitLine(c) {
|
|
1907
|
-
return ` ${c.shortHash} ${
|
|
2040
|
+
return ` ${c.shortHash} ${c.message.slice(0, 60).padEnd(62)} ${c.author.padEnd(16)} ${c.date}`;
|
|
1908
2041
|
}
|
|
1909
2042
|
async function runList(opts) {
|
|
1910
2043
|
await validateRemote(opts.remote);
|
|
@@ -1988,16 +2121,24 @@ ${stat}`);
|
|
|
1988
2121
|
process.exit(1);
|
|
1989
2122
|
}
|
|
1990
2123
|
}
|
|
2124
|
+
async function printUpdateNotice() {
|
|
2125
|
+
const info = await checkForUpdate(APP_VERSION2);
|
|
2126
|
+
if (info.hasUpdate) {
|
|
2127
|
+
log(`
|
|
2128
|
+
\u{1F4A1} \u65B0\u7248\u672C\u53EF\u7528 ${info.latest} (\u5F53\u524D ${info.current}) \u2192 npm i -g git-sync-tui`);
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
1991
2131
|
async function runCli(opts) {
|
|
1992
2132
|
if (opts.list) {
|
|
1993
2133
|
await runList(opts);
|
|
1994
2134
|
} else {
|
|
1995
2135
|
await runExec(opts);
|
|
1996
2136
|
}
|
|
2137
|
+
await printUpdateNotice();
|
|
1997
2138
|
}
|
|
1998
2139
|
|
|
1999
2140
|
// src/cli.tsx
|
|
2000
|
-
import { jsx as
|
|
2141
|
+
import { jsx as jsx12 } from "react/jsx-runtime";
|
|
2001
2142
|
var cli = meow(
|
|
2002
2143
|
`
|
|
2003
2144
|
\u7528\u6CD5
|
|
@@ -2071,5 +2212,5 @@ if (isCliMode) {
|
|
|
2071
2212
|
process.exit(1);
|
|
2072
2213
|
});
|
|
2073
2214
|
} else {
|
|
2074
|
-
render(/* @__PURE__ */
|
|
2215
|
+
render(/* @__PURE__ */ jsx12(App, { initialRemote: remote, initialBranch: branch }));
|
|
2075
2216
|
}
|
package/package.json
CHANGED