dsh-ide-git 0.19.2 → 0.19.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/README_EN.md +1 -1
- package/dsh.plugin.json +1 -1
- package/package.json +1 -1
- package/src/client.js +467 -145
- package/src/index.js +36 -0
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ DSH 自带的 Git 面板覆盖「暂存 / 提交 / 还原 / 看历史」;`dsh-id
|
|
|
11
11
|
## 它解决什么
|
|
12
12
|
|
|
13
13
|
- **分支管理没有树**:内置面板只有一个分支下拉。这里给出 `HEAD(当前分支) / 本地 / 远程 / 标签` 的分组树,每个分支带 ahead/behind 角标、上游、worktree 占用标记(`W`)。
|
|
14
|
-
- **历史只是列表**:这里画出提交图谱(lane 拓扑 + 分支/标签 ref 徽章)
|
|
14
|
+
- **历史只是列表**:这里画出提交图谱(lane 拓扑 + 分支/标签 ref 徽章),支持无限滚动加载、框选批量选中与过滤(文本 / 哈希 / 作者 /「我」);双击分支直接浏览该分支历史(不强制签出),有合并关系时自动定位到两分支最近的合并点。
|
|
15
15
|
- **想看某个提交改了什么**:点提交直接进详情——作者、日期、完整提交信息、变更文件(含 +/− 行数),点文件看逐行 diff(红绿 + 行号 + hunk 高亮)。
|
|
16
16
|
- **想指给 AI 一段代码**:在 diff 窗里选中几行、右键,把「哪个文件哪几行」(附提交或工作区上下文)直接发进当前会话的输入框。
|
|
17
17
|
- **想比较分支**:分支右键即可与当前分支比较,结果按文件与提交数汇报。
|
|
@@ -100,7 +100,7 @@ DSH 自带的 Git 面板覆盖「暂存 / 提交 / 还原 / 看历史」;`dsh-id
|
|
|
100
100
|
| 分支树 | `HEAD / 本地 / 远程 / 标签` 分组(可折叠)、过滤框、星标当前分支、ahead/behind 角标、worktree 占用标记;本地蓝 / 远程紫 / 标签黄 / HEAD 绿 |
|
|
101
101
|
| 分支右键 | 签出、将当前分支变基到此、合并到当前分支、与当前分支比较、从该分支新建分支、重命名、删除(当前分支禁用)、更新(Fetch)、推送、发送到对话(分支名 + 仓库) |
|
|
102
102
|
| 历史筛选 | 文本或哈希、分支或标签、提交人、日期(今天 / 近 7 天 / 近 30 天 / 今年),外加排序方向(新→旧 / 旧→新)与一键清除;路径筛选回车后由 git 服务端过滤(`git log -- <path>`) |
|
|
103
|
-
| 提交列表 | 列序对齐 IDEA:**日期 → 提交人 → 图谱 → 分支标签 → 提交信息**;lane 拓扑图整格连线(相邻行像素对齐)、HEAD 空心点、refs 徽章(HEAD 绿 / 本地蓝 / 远程紫 / 标签黄,最多 3 个 + `+n`)
|
|
103
|
+
| 提交列表 | 列序对齐 IDEA:**日期 → 提交人 → 图谱 → 分支标签 → 提交信息**;lane 拓扑图整格连线(相邻行像素对齐)、HEAD 空心点、refs 徽章(HEAD 绿 / 本地蓝 / 远程紫 / 标签黄,最多 3 个 + `+n`)、加载更多/无限滚动(每页 120)、三列表框选批量选中 |
|
|
104
104
|
| 提交右键 | 提交详情、复制修订号、签出该修订(确认)、在此新建分支、新建标签、cherry-pick、还原提交、重置到此(保留 / 丢弃更改)、发送到对话(短哈希 + 提交信息 + 仓库) |
|
|
105
105
|
| 提交详情 | 完整提交信息、作者/日期、变更文件列表(+/−),点文件看逐行 diff |
|
|
106
106
|
| diff 窗 | DSH 自家 DiffBlock 渲染(红绿底 + 复制/换行工具栏)、高度可拖拽调节并按位置记忆;**选中 diff 行右键**把行级引用发进对话——`@文件` + 第 N–M 行 + 提交(短哈希/标题/当前分支)或工作区(已暂存/未提交)上下文,删除行标注旧行号,选区定位不到行时发选中片段(截断 600 字符),另有「复制选中引用」 |
|
package/README_EN.md
CHANGED
|
@@ -87,7 +87,7 @@ The panel never guesses: it measures itself with a `ResizeObserver` (width ≥ 6
|
|
|
87
87
|
- Composer status pill: repository/branch capsule in the host input-bar tool row (colored uncommitted / pullable / to-push counters), click to switch repo and branch — kept in sync with the panel both ways; single-repo workspaces switch branches only, multi-repo workspaces switch both. Off-switch in the settings (on by default). Sessions are pre-warmed on switch so the panel opens instantly.
|
|
88
88
|
- Branch tree: HEAD / Local / Remote / Tags groups, filter box, ahead/behind badges, worktree marker; local blue / remote violet / tag amber / HEAD green.
|
|
89
89
|
- Branch context menu: checkout, rebase current onto this, merge into current, compare with current, new branch from here, rename, delete, fetch, push, send to chat (branch + repo).
|
|
90
|
-
- History filters: text or hash, branch or tag, author, date (today / 7 days / 30 days / this year), an order toggle (newest / oldest first) and one-click clear; the path filter runs server-side (`git log -- <path>`) on Enter.
|
|
90
|
+
- History filters: text or hash, branch or tag, author (with a one-click "Me" entry), date (today / 7 days / 30 days / this year), an order toggle (newest / oldest first) and one-click clear; the path filter runs server-side (`git log -- <path>`) on Enter. Infinite scroll keeps loading pages at the list edge, rubber-band selection works across all three lists, and double-clicking a branch browses its history without checking it out.
|
|
91
91
|
- Commit list: IDEA column order — **date → author → graph → refs → subject**; a lane graph whose segments span whole rows (pixel-aligned across rows), a hollow dot for HEAD, refs badges (HEAD green / local blue / remote violet / tag amber, at most three plus `+n`), load more (120 per page).
|
|
92
92
|
- Commit context menu: details, copy revision, checkout revision, new branch here, new tag, cherry-pick, revert, reset here (keep / discard changes), send to chat (short hash + message + repo).
|
|
93
93
|
- Commit details: full message, author/date, changed files with +/− and per-file line diff.
|
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-external/dsh-ide-git",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.3",
|
|
4
4
|
"main": "./src/index.js",
|
|
5
5
|
"description": "IDE 级 Git 操作面板:分支树 + 提交图谱 + 变更/暂存/提交 + 提交详情。装了 dsh-better-sidebar 时挂成它的原生 tab(右侧栏 + 底部工作台);没装时直接挂 DSH 原生右侧栏,单独安装即可用。JetBrains 风格 Git 工具窗口 for DeepSeek Harness.",
|
|
6
6
|
"engines": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-ide-git",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.3",
|
|
4
4
|
"description": "DSH plugin: an IDE-grade Git tool window as a native dsh-better-sidebar tab — branch tree, commit graph, changes, commit details, JetBrains-style actions, in the right sidebar and the bottom panel. | DSH 插件:IDE 级 Git 工具窗口,以 dsh-better-sidebar 原生 Tab 挂载——分支树 / 提交图谱 / 变更与提交详情 / JetBrains 风格操作,右侧栏与底部面板双布局自适应。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
package/src/client.js
CHANGED
|
@@ -101,7 +101,8 @@ window.__ModuleLoader__.load({
|
|
|
101
101
|
'branches.head': 'HEAD(当前分支)',
|
|
102
102
|
'branches.favorites': '收藏',
|
|
103
103
|
'branches.starHint': "已收藏",
|
|
104
|
-
'branches.pickHint': '
|
|
104
|
+
'branches.pickHint': '单击选中,双击查看提交历史',
|
|
105
|
+
'branches.headHint': '双击查看当前分支的提交历史',
|
|
105
106
|
'branches.worktree': '已在另一工作区检出',
|
|
106
107
|
'action.stash': '贮藏',
|
|
107
108
|
'action.favorite': '收藏/取消收藏当前分支',
|
|
@@ -347,6 +348,7 @@ window.__ModuleLoader__.load({
|
|
|
347
348
|
'action.unavailable': '当前状态不可用',
|
|
348
349
|
'filter.branch': '分支',
|
|
349
350
|
'filter.user': '用户',
|
|
351
|
+
'history.me': '我',
|
|
350
352
|
'filter.date': '日期',
|
|
351
353
|
'filter.path': '路径',
|
|
352
354
|
'filter.all': '全部',
|
|
@@ -426,7 +428,8 @@ window.__ModuleLoader__.load({
|
|
|
426
428
|
'branches.head': 'HEAD (current branch)',
|
|
427
429
|
'branches.favorites': 'Favorites',
|
|
428
430
|
'branches.starHint': "Favorited",
|
|
429
|
-
'branches.pickHint': 'Click to select, double-click to
|
|
431
|
+
'branches.pickHint': 'Click to select, double-click to browse its history',
|
|
432
|
+
'branches.headHint': 'Double-click to browse the current branch\'s history',
|
|
430
433
|
'branches.worktree': 'Checked out in another working tree',
|
|
431
434
|
'action.stash': 'Stash',
|
|
432
435
|
'action.favorite': 'Favorite / unfavorite current branch',
|
|
@@ -672,6 +675,7 @@ window.__ModuleLoader__.load({
|
|
|
672
675
|
'action.unavailable': 'Not available in the current state',
|
|
673
676
|
'filter.branch': 'Branch',
|
|
674
677
|
'filter.user': 'User',
|
|
678
|
+
'history.me': 'Me',
|
|
675
679
|
'filter.date': 'Date',
|
|
676
680
|
'filter.path': 'Path',
|
|
677
681
|
'filter.all': 'All',
|
|
@@ -759,7 +763,8 @@ window.__ModuleLoader__.load({
|
|
|
759
763
|
'branches.head': 'HEAD(當前分支)',
|
|
760
764
|
'branches.favorites': '我的最愛',
|
|
761
765
|
'branches.starHint': "已收藏",
|
|
762
|
-
'branches.pickHint': '
|
|
766
|
+
'branches.pickHint': '單擊選中,雙擊查看提交歷史',
|
|
767
|
+
'branches.headHint': '雙擊查看當前分支的提交歷史',
|
|
763
768
|
'branches.worktree': '已喺另一個工作區簽出',
|
|
764
769
|
'action.stash': '貯藏',
|
|
765
770
|
'action.favorite': '加入/取消我的最愛',
|
|
@@ -1005,6 +1010,7 @@ window.__ModuleLoader__.load({
|
|
|
1005
1010
|
'action.unavailable': '當前狀態下唔可用',
|
|
1006
1011
|
'filter.branch': '分支',
|
|
1007
1012
|
'filter.user': '提交者',
|
|
1013
|
+
'history.me': '我',
|
|
1008
1014
|
'filter.date': '日期',
|
|
1009
1015
|
'filter.path': '路徑',
|
|
1010
1016
|
'filter.all': '全部',
|
|
@@ -1084,7 +1090,8 @@ window.__ModuleLoader__.load({
|
|
|
1084
1090
|
'branches.head': 'HEAD(目前分支)',
|
|
1085
1091
|
'branches.favorites': '我的最愛',
|
|
1086
1092
|
'branches.starHint': "已收藏",
|
|
1087
|
-
'branches.pickHint': '
|
|
1093
|
+
'branches.pickHint': '單擊選取,雙擊檢視提交歷史',
|
|
1094
|
+
'branches.headHint': '雙擊檢視目前分支的提交歷史',
|
|
1088
1095
|
'branches.worktree': '已在另一個工作區簽出',
|
|
1089
1096
|
'action.stash': '暫存擱置',
|
|
1090
1097
|
'action.favorite': '加入/移除我的最愛',
|
|
@@ -1330,6 +1337,7 @@ window.__ModuleLoader__.load({
|
|
|
1330
1337
|
'action.unavailable': '目前狀態下無法使用',
|
|
1331
1338
|
'filter.branch': '分支',
|
|
1332
1339
|
'filter.user': '提交者',
|
|
1340
|
+
'history.me': '我',
|
|
1333
1341
|
'filter.date': '日期',
|
|
1334
1342
|
'filter.path': '路徑',
|
|
1335
1343
|
'filter.all': '全部',
|
|
@@ -1409,7 +1417,8 @@ window.__ModuleLoader__.load({
|
|
|
1409
1417
|
'branches.head': 'HEAD(當前分支)',
|
|
1410
1418
|
'branches.favorites': '我的最愛',
|
|
1411
1419
|
'branches.starHint': "已收藏",
|
|
1412
|
-
'branches.pickHint': '
|
|
1420
|
+
'branches.pickHint': '單擊選中,雙擊查看提交歷史',
|
|
1421
|
+
'branches.headHint': '雙擊查看當前分支的提交歷史',
|
|
1413
1422
|
'branches.worktree': '已喺另一個工作區簽出',
|
|
1414
1423
|
'action.stash': '貯藏',
|
|
1415
1424
|
'action.favorite': '加入/取消我的最愛',
|
|
@@ -1655,6 +1664,7 @@ window.__ModuleLoader__.load({
|
|
|
1655
1664
|
'action.unavailable': '當前狀態下唔可用',
|
|
1656
1665
|
'filter.branch': '分支',
|
|
1657
1666
|
'filter.user': '提交者',
|
|
1667
|
+
'history.me': '我',
|
|
1658
1668
|
'filter.date': '日期',
|
|
1659
1669
|
'filter.path': '路徑',
|
|
1660
1670
|
'filter.all': '全部',
|
|
@@ -1734,7 +1744,8 @@ window.__ModuleLoader__.load({
|
|
|
1734
1744
|
'branches.head': 'HEAD(現在のブランチ)',
|
|
1735
1745
|
'branches.favorites': 'お気に入り',
|
|
1736
1746
|
'branches.starHint': "お気に入り済み",
|
|
1737
|
-
'branches.pickHint': '
|
|
1747
|
+
'branches.pickHint': 'クリックで選択、ダブルクリックで履歴を表示',
|
|
1748
|
+
'branches.headHint': 'ダブルクリックで現在のブランチの履歴を表示',
|
|
1738
1749
|
'branches.worktree': '別のワークツリーでチェックアウト済み',
|
|
1739
1750
|
'action.stash': 'スタッシュ',
|
|
1740
1751
|
'action.favorite': '現在のブランチをお気に入りに追加/解除',
|
|
@@ -1980,6 +1991,7 @@ window.__ModuleLoader__.load({
|
|
|
1980
1991
|
'action.unavailable': '現在の状態では使用できません',
|
|
1981
1992
|
'filter.branch': 'ブランチ',
|
|
1982
1993
|
'filter.user': 'コミット者',
|
|
1994
|
+
'history.me': '自分',
|
|
1983
1995
|
'filter.date': '日付',
|
|
1984
1996
|
'filter.path': 'パス',
|
|
1985
1997
|
'filter.all': 'すべて',
|
|
@@ -2059,7 +2071,8 @@ window.__ModuleLoader__.load({
|
|
|
2059
2071
|
'branches.head': 'HEAD(현재 브랜치)',
|
|
2060
2072
|
'branches.favorites': '즐겨찾기',
|
|
2061
2073
|
'branches.starHint': "즐겨찾기",
|
|
2062
|
-
'branches.pickHint': '클릭하여 선택,
|
|
2074
|
+
'branches.pickHint': '클릭하여 선택, 더블클릭으로 커밋 히스토리 보기',
|
|
2075
|
+
'branches.headHint': '더블클릭으로 현재 브랜치의 히스토리 보기',
|
|
2063
2076
|
'branches.worktree': '다른 워크트리에서 체크아웃됨',
|
|
2064
2077
|
'action.stash': '스태시',
|
|
2065
2078
|
'action.favorite': '현재 브랜치 즐겨찾기 추가/해제',
|
|
@@ -2305,6 +2318,7 @@ window.__ModuleLoader__.load({
|
|
|
2305
2318
|
'action.unavailable': '현재 상태에서는 사용할 수 없습니다',
|
|
2306
2319
|
'filter.branch': '브랜치',
|
|
2307
2320
|
'filter.user': '작성자',
|
|
2321
|
+
'history.me': '나',
|
|
2308
2322
|
'filter.date': '날짜',
|
|
2309
2323
|
'filter.path': '경로',
|
|
2310
2324
|
'filter.all': '전체',
|
|
@@ -2384,7 +2398,8 @@ window.__ModuleLoader__.load({
|
|
|
2384
2398
|
'branches.head': 'HEAD (aktueller Branch)',
|
|
2385
2399
|
'branches.favorites': 'Favoriten',
|
|
2386
2400
|
'branches.starHint': "Favorit",
|
|
2387
|
-
'branches.pickHint': 'Klicken zum Auswählen,
|
|
2401
|
+
'branches.pickHint': 'Klicken zum Auswählen, Doppelklick zeigt den Verlauf',
|
|
2402
|
+
'branches.headHint': 'Doppelklick zeigt den Verlauf des aktuellen Branches',
|
|
2388
2403
|
'branches.worktree': 'In einem anderen Arbeitsbaum ausgecheckt',
|
|
2389
2404
|
'action.stash': 'Stash',
|
|
2390
2405
|
'action.favorite': 'Aktuellen Branch als Favorit setzen/entfernen',
|
|
@@ -2630,6 +2645,7 @@ window.__ModuleLoader__.load({
|
|
|
2630
2645
|
'action.unavailable': 'Im aktuellen Zustand nicht verfügbar',
|
|
2631
2646
|
'filter.branch': 'Branch',
|
|
2632
2647
|
'filter.user': 'Autor',
|
|
2648
|
+
'history.me': 'Ich',
|
|
2633
2649
|
'filter.date': 'Datum',
|
|
2634
2650
|
'filter.path': 'Pfad',
|
|
2635
2651
|
'filter.all': 'Alle',
|
|
@@ -2709,7 +2725,8 @@ window.__ModuleLoader__.load({
|
|
|
2709
2725
|
'branches.head': 'HEAD (branche courante)',
|
|
2710
2726
|
'branches.favorites': 'Favoris',
|
|
2711
2727
|
'branches.starHint': "Favori",
|
|
2712
|
-
'branches.pickHint': '
|
|
2728
|
+
'branches.pickHint': 'Clic pour sélectionner, double-clic pour ouvrir l\'historique',
|
|
2729
|
+
'branches.headHint': 'Double-clic pour l\'historique de la branche courante',
|
|
2713
2730
|
'branches.worktree': 'Extraite dans un autre arbre de travail',
|
|
2714
2731
|
'action.stash': 'Remisage',
|
|
2715
2732
|
'action.favorite': 'Ajouter/retirer la branche courante des favoris',
|
|
@@ -2955,6 +2972,7 @@ window.__ModuleLoader__.load({
|
|
|
2955
2972
|
'action.unavailable': 'Indisponible dans l\'état actuel',
|
|
2956
2973
|
'filter.branch': 'Branche',
|
|
2957
2974
|
'filter.user': 'Auteur',
|
|
2975
|
+
'history.me': 'Moi',
|
|
2958
2976
|
'filter.date': 'Date',
|
|
2959
2977
|
'filter.path': 'Chemin',
|
|
2960
2978
|
'filter.all': 'Tout',
|
|
@@ -3034,7 +3052,8 @@ window.__ModuleLoader__.load({
|
|
|
3034
3052
|
'branches.head': 'HEAD (текущая ветка)',
|
|
3035
3053
|
'branches.favorites': 'Избранное',
|
|
3036
3054
|
'branches.starHint': "В избранном",
|
|
3037
|
-
'branches.pickHint': '
|
|
3055
|
+
'branches.pickHint': 'Клик — выбор, двойной клик — история коммитов',
|
|
3056
|
+
'branches.headHint': 'Двойной клик — история текущей ветки',
|
|
3038
3057
|
'branches.worktree': 'Уже развёрнута в другом рабочем дереве',
|
|
3039
3058
|
'action.stash': 'Отложить',
|
|
3040
3059
|
'action.favorite': 'Добавить/убрать текущую ветку из избранного',
|
|
@@ -3280,6 +3299,7 @@ window.__ModuleLoader__.load({
|
|
|
3280
3299
|
'action.unavailable': 'Недоступно в текущем состоянии',
|
|
3281
3300
|
'filter.branch': 'Ветка',
|
|
3282
3301
|
'filter.user': 'Автор',
|
|
3302
|
+
'history.me': 'Я',
|
|
3283
3303
|
'filter.date': 'Дата',
|
|
3284
3304
|
'filter.path': 'Путь',
|
|
3285
3305
|
'filter.all': 'Все',
|
|
@@ -3359,7 +3379,8 @@ window.__ModuleLoader__.load({
|
|
|
3359
3379
|
'branches.head': 'HEAD (branch atual)',
|
|
3360
3380
|
'branches.favorites': 'Favoritas',
|
|
3361
3381
|
'branches.starHint': "Favorito",
|
|
3362
|
-
'branches.pickHint': 'Clique para selecionar, clique
|
|
3382
|
+
'branches.pickHint': 'Clique para selecionar, duplo clique abre o histórico',
|
|
3383
|
+
'branches.headHint': 'Duplo clique abre o histórico do branch atual',
|
|
3363
3384
|
'branches.worktree': 'Já em checkout em outra árvore de trabalho',
|
|
3364
3385
|
'action.stash': 'Stash',
|
|
3365
3386
|
'action.favorite': 'Adicionar/remover a branch atual dos favoritos',
|
|
@@ -3605,6 +3626,7 @@ window.__ModuleLoader__.load({
|
|
|
3605
3626
|
'action.unavailable': 'Indisponível no estado atual',
|
|
3606
3627
|
'filter.branch': 'Branch',
|
|
3607
3628
|
'filter.user': 'Autor',
|
|
3629
|
+
'history.me': 'Eu',
|
|
3608
3630
|
'filter.date': 'Data',
|
|
3609
3631
|
'filter.path': 'Caminho',
|
|
3610
3632
|
'filter.all': 'Tudo',
|
|
@@ -3684,7 +3706,8 @@ window.__ModuleLoader__.load({
|
|
|
3684
3706
|
'branches.head': 'HEAD (branch corrente)',
|
|
3685
3707
|
'branches.favorites': 'Preferiti',
|
|
3686
3708
|
'branches.starHint': "Preferito",
|
|
3687
|
-
'branches.pickHint': 'Clic per selezionare, doppio clic
|
|
3709
|
+
'branches.pickHint': 'Clic per selezionare, doppio clic apre la cronologia',
|
|
3710
|
+
'branches.headHint': 'Doppio clic apre la cronologia del branch corrente',
|
|
3688
3711
|
'branches.worktree': 'Già in checkout in un altro albero di lavoro',
|
|
3689
3712
|
'action.stash': 'Stash',
|
|
3690
3713
|
'action.favorite': 'Aggiungi/rimuovi il branch corrente dai preferiti',
|
|
@@ -3930,6 +3953,7 @@ window.__ModuleLoader__.load({
|
|
|
3930
3953
|
'action.unavailable': 'Non disponibile nello stato attuale',
|
|
3931
3954
|
'filter.branch': 'Branch',
|
|
3932
3955
|
'filter.user': 'Autore',
|
|
3956
|
+
'history.me': 'Io',
|
|
3933
3957
|
'filter.date': 'Data',
|
|
3934
3958
|
'filter.path': 'Percorso',
|
|
3935
3959
|
'filter.all': 'Tutti',
|
|
@@ -4009,7 +4033,8 @@ window.__ModuleLoader__.load({
|
|
|
4009
4033
|
'branches.head': 'HEAD (huidige branch)',
|
|
4010
4034
|
'branches.favorites': 'Favorieten',
|
|
4011
4035
|
'branches.starHint': "Favoriet",
|
|
4012
|
-
'branches.pickHint': 'Klik om te selecteren, dubbelklik
|
|
4036
|
+
'branches.pickHint': 'Klik om te selecteren, dubbelklik opent de geschiedenis',
|
|
4037
|
+
'branches.headHint': 'Dubbelklik opent de geschiedenis van de huidige branch',
|
|
4013
4038
|
'branches.worktree': 'Al uitgecheckt in een andere werkboom',
|
|
4014
4039
|
'action.stash': 'Stash',
|
|
4015
4040
|
'action.favorite': 'Huidige branch aan favorieten toevoegen/onttrekken',
|
|
@@ -4255,6 +4280,7 @@ window.__ModuleLoader__.load({
|
|
|
4255
4280
|
'action.unavailable': 'Niet beschikbaar in de huidige staat',
|
|
4256
4281
|
'filter.branch': 'Branch',
|
|
4257
4282
|
'filter.user': 'Auteur',
|
|
4283
|
+
'history.me': 'Ik',
|
|
4258
4284
|
'filter.date': 'Datum',
|
|
4259
4285
|
'filter.path': 'Pad',
|
|
4260
4286
|
'filter.all': 'Alles',
|
|
@@ -4334,7 +4360,8 @@ window.__ModuleLoader__.load({
|
|
|
4334
4360
|
'branches.head': 'HEAD (bieżąca gałąź)',
|
|
4335
4361
|
'branches.favorites': 'Ulubione',
|
|
4336
4362
|
'branches.starHint': "Ulubione",
|
|
4337
|
-
'branches.pickHint': 'Kliknij, aby
|
|
4363
|
+
'branches.pickHint': 'Kliknij, aby zaznaczyć, dwuklik otwiera historię',
|
|
4364
|
+
'branches.headHint': 'Dwuklik otwiera historię bieżącej gałęzi',
|
|
4338
4365
|
'branches.worktree': 'Już wybrana w innym drzewie roboczym',
|
|
4339
4366
|
'action.stash': 'Schowek',
|
|
4340
4367
|
'action.favorite': 'Dodaj/usuń bieżącą gałąź z ulubionych',
|
|
@@ -4580,6 +4607,7 @@ window.__ModuleLoader__.load({
|
|
|
4580
4607
|
'action.unavailable': 'Niedostępne w bieżącym stanie',
|
|
4581
4608
|
'filter.branch': 'Gałąź',
|
|
4582
4609
|
'filter.user': 'Autor',
|
|
4610
|
+
'history.me': 'Ja',
|
|
4583
4611
|
'filter.date': 'Data',
|
|
4584
4612
|
'filter.path': 'Ścieżka',
|
|
4585
4613
|
'filter.all': 'Wszystkie',
|
|
@@ -4659,7 +4687,8 @@ window.__ModuleLoader__.load({
|
|
|
4659
4687
|
'branches.head': 'HEAD (aktuell gren)',
|
|
4660
4688
|
'branches.favorites': 'Favoriter',
|
|
4661
4689
|
'branches.starHint': "Favorit",
|
|
4662
|
-
'branches.pickHint': 'Klicka för att välja,
|
|
4690
|
+
'branches.pickHint': 'Klicka för att välja, dubbelklick visar historiken',
|
|
4691
|
+
'branches.headHint': 'Dubbelklick visar historiken för aktuell gren',
|
|
4663
4692
|
'branches.worktree': 'Redan utcheckad i ett annat arbetsträd',
|
|
4664
4693
|
'action.stash': 'Stash',
|
|
4665
4694
|
'action.favorite': 'Lägg till/ta bort aktuell gren som favorit',
|
|
@@ -4905,6 +4934,7 @@ window.__ModuleLoader__.load({
|
|
|
4905
4934
|
'action.unavailable': 'Inte tillgängligt i aktuellt läge',
|
|
4906
4935
|
'filter.branch': 'Gren',
|
|
4907
4936
|
'filter.user': 'Författare',
|
|
4937
|
+
'history.me': 'Jag',
|
|
4908
4938
|
'filter.date': 'Datum',
|
|
4909
4939
|
'filter.path': 'Sökväg',
|
|
4910
4940
|
'filter.all': 'Alla',
|
|
@@ -4984,7 +5014,8 @@ window.__ModuleLoader__.load({
|
|
|
4984
5014
|
'branches.head': 'HEAD (geçerli dal)',
|
|
4985
5015
|
'branches.favorites': 'Sık kullanılanlar',
|
|
4986
5016
|
'branches.starHint': "Favori",
|
|
4987
|
-
'branches.pickHint': 'Seçmek için
|
|
5017
|
+
'branches.pickHint': 'Seçmek için tek tık, geçmişi görmek için çift tık',
|
|
5018
|
+
'branches.headHint': 'Çift tık, geçerli dalın geçmişini açar',
|
|
4988
5019
|
'branches.worktree': 'Başka bir çalışma ağacında çıkarılmış',
|
|
4989
5020
|
'action.stash': 'Saklama',
|
|
4990
5021
|
'action.favorite': 'Geçerli dalı sık kullanılanlara ekle/çıkar',
|
|
@@ -5230,6 +5261,7 @@ window.__ModuleLoader__.load({
|
|
|
5230
5261
|
'action.unavailable': 'Geçerli durumda kullanılamaz',
|
|
5231
5262
|
'filter.branch': 'Dal',
|
|
5232
5263
|
'filter.user': 'Yazar',
|
|
5264
|
+
'history.me': 'Ben',
|
|
5233
5265
|
'filter.date': 'Tarih',
|
|
5234
5266
|
'filter.path': 'Yol',
|
|
5235
5267
|
'filter.all': 'Tümü',
|
|
@@ -5309,7 +5341,8 @@ window.__ModuleLoader__.load({
|
|
|
5309
5341
|
'branches.head': 'HEAD (branch saat ini)',
|
|
5310
5342
|
'branches.favorites': 'Favorit',
|
|
5311
5343
|
'branches.starHint': "Favorit",
|
|
5312
|
-
'branches.pickHint': 'Klik untuk memilih, klik ganda
|
|
5344
|
+
'branches.pickHint': 'Klik untuk memilih, klik ganda membuka riwayat',
|
|
5345
|
+
'branches.headHint': 'Klik ganda membuka riwayat branch saat ini',
|
|
5313
5346
|
'branches.worktree': 'Sudah di-checkout di worktree lain',
|
|
5314
5347
|
'action.stash': 'Stash',
|
|
5315
5348
|
'action.favorite': 'Tambah/hapus branch saat ini dari favorit',
|
|
@@ -5555,6 +5588,7 @@ window.__ModuleLoader__.load({
|
|
|
5555
5588
|
'action.unavailable': 'Tidak tersedia pada keadaan saat ini',
|
|
5556
5589
|
'filter.branch': 'Branch',
|
|
5557
5590
|
'filter.user': 'Penulis',
|
|
5591
|
+
'history.me': 'Saya',
|
|
5558
5592
|
'filter.date': 'Tanggal',
|
|
5559
5593
|
'filter.path': 'Jalur',
|
|
5560
5594
|
'filter.all': 'Semua',
|
|
@@ -5634,7 +5668,8 @@ window.__ModuleLoader__.load({
|
|
|
5634
5668
|
'branches.head': 'HEAD (nhánh hiện tại)',
|
|
5635
5669
|
'branches.favorites': 'Yêu thích',
|
|
5636
5670
|
'branches.starHint': "Đã yêu thích",
|
|
5637
|
-
'branches.pickHint': '
|
|
5671
|
+
'branches.pickHint': 'Bấm để chọn, nhấp đúp mở lịch sử commit',
|
|
5672
|
+
'branches.headHint': 'Nhấp đúp mở lịch sử của nhánh hiện tại',
|
|
5638
5673
|
'branches.worktree': 'Đã được checkout ở một worktree khác',
|
|
5639
5674
|
'action.stash': 'Tạm cất',
|
|
5640
5675
|
'action.favorite': 'Thêm/bỏ nhánh hiện tại khỏi yêu thích',
|
|
@@ -5880,6 +5915,7 @@ window.__ModuleLoader__.load({
|
|
|
5880
5915
|
'action.unavailable': 'Không dùng được ở trạng thái hiện tại',
|
|
5881
5916
|
'filter.branch': 'Nhánh',
|
|
5882
5917
|
'filter.user': 'Tác giả',
|
|
5918
|
+
'history.me': 'Tôi',
|
|
5883
5919
|
'filter.date': 'Ngày',
|
|
5884
5920
|
'filter.path': 'Đường dẫn',
|
|
5885
5921
|
'filter.all': 'Tất cả',
|
|
@@ -5959,7 +5995,8 @@ window.__ModuleLoader__.load({
|
|
|
5959
5995
|
'branches.head': 'HEAD (الفرع الحالي)',
|
|
5960
5996
|
'branches.favorites': 'المفضلة',
|
|
5961
5997
|
'branches.starHint': "مفضل",
|
|
5962
|
-
'branches.pickHint': 'انقر للتحديد،
|
|
5998
|
+
'branches.pickHint': 'انقر للتحديد، ونقرتان مزدوجتان لعرض السجل',
|
|
5999
|
+
'branches.headHint': 'نقرتان مزدوجتان لعرض سجل الفرع الحالي',
|
|
5963
6000
|
'branches.worktree': 'مُستخرَج بالفعل في شجرة عمل أخرى',
|
|
5964
6001
|
'action.stash': 'التخزين المؤقت',
|
|
5965
6002
|
'action.favorite': 'إضافة الفرع الحالي إلى المفضلة أو إزالته',
|
|
@@ -6205,6 +6242,7 @@ window.__ModuleLoader__.load({
|
|
|
6205
6242
|
'action.unavailable': 'غير متاح في الحالة الحالية',
|
|
6206
6243
|
'filter.branch': 'الفرع',
|
|
6207
6244
|
'filter.user': 'المؤلف',
|
|
6245
|
+
'history.me': 'أنا',
|
|
6208
6246
|
'filter.date': 'التاريخ',
|
|
6209
6247
|
'filter.path': 'المسار',
|
|
6210
6248
|
'filter.all': 'الكل',
|
|
@@ -6284,7 +6322,8 @@ window.__ModuleLoader__.load({
|
|
|
6284
6322
|
'branches.head': 'HEAD (वर्तमान शाखा)',
|
|
6285
6323
|
'branches.favorites': 'पसंदीदा',
|
|
6286
6324
|
'branches.starHint': "पसंदीदा",
|
|
6287
|
-
'branches.pickHint': 'चुनने के लिए क्लिक करें,
|
|
6325
|
+
'branches.pickHint': 'चुनने के लिए क्लिक करें, इतिहास देखने के लिए डबल-क्लिक',
|
|
6326
|
+
'branches.headHint': 'डबल-क्लिक से वर्तमान ब्रांच का इतिहास देखें',
|
|
6288
6327
|
'branches.worktree': 'पहले से किसी अन्य वर्कट्री में चेकआउट है',
|
|
6289
6328
|
'action.stash': 'स्टैश',
|
|
6290
6329
|
'action.favorite': 'वर्तमान शाखा को पसंदीदा में जोड़ें/हटाएँ',
|
|
@@ -6530,6 +6569,7 @@ window.__ModuleLoader__.load({
|
|
|
6530
6569
|
'action.unavailable': 'वर्तमान स्थिति में उपलब्ध नहीं',
|
|
6531
6570
|
'filter.branch': 'शाखा',
|
|
6532
6571
|
'filter.user': 'लेखक',
|
|
6572
|
+
'history.me': 'मैं',
|
|
6533
6573
|
'filter.date': 'दिनांक',
|
|
6534
6574
|
'filter.path': 'पथ',
|
|
6535
6575
|
'filter.all': 'सभी',
|
|
@@ -6609,7 +6649,8 @@ window.__ModuleLoader__.load({
|
|
|
6609
6649
|
'branches.head': 'HEAD (แบรนช์ปัจจุบัน)',
|
|
6610
6650
|
'branches.favorites': 'รายการโปรด',
|
|
6611
6651
|
'branches.starHint': "รายการโปรด",
|
|
6612
|
-
'branches.pickHint': 'คลิกเพื่อเลือก
|
|
6652
|
+
'branches.pickHint': 'คลิกเพื่อเลือก ดับเบิลคลิกเพื่อดูประวัติคอมมิต',
|
|
6653
|
+
'branches.headHint': 'ดับเบิลคลิกเพื่อดูประวัติของแบรนช์ปัจจุบัน',
|
|
6613
6654
|
'branches.worktree': 'เช็กเอาต์อยู่ในเวิร์กทรีอื่นแล้ว',
|
|
6614
6655
|
'action.stash': 'สแตช',
|
|
6615
6656
|
'action.favorite': 'เพิ่ม/移除แบรนช์ปัจจุบันในรายการโปรด',
|
|
@@ -6855,6 +6896,7 @@ window.__ModuleLoader__.load({
|
|
|
6855
6896
|
'action.unavailable': 'ใช้ไม่ได้ในสถานะปัจจุบัน',
|
|
6856
6897
|
'filter.branch': 'แบรนช์',
|
|
6857
6898
|
'filter.user': 'ผู้เขียน',
|
|
6899
|
+
'history.me': 'ฉัน',
|
|
6858
6900
|
'filter.date': 'วันที่',
|
|
6859
6901
|
'filter.path': 'พาธ',
|
|
6860
6902
|
'filter.all': 'ทั้งหมด',
|
|
@@ -9302,11 +9344,16 @@ window.__ModuleLoader__.load({
|
|
|
9302
9344
|
closeRef.current()
|
|
9303
9345
|
}
|
|
9304
9346
|
const onKey = (event) => { if (event.key === 'Escape') closeRef.current() }
|
|
9305
|
-
|
|
9347
|
+
/* pointerdown, not mousedown: the list pointer-down handlers call
|
|
9348
|
+
preventDefault(), which suppresses the COMPATIBILITY mouse events
|
|
9349
|
+
(mousedown included) — a capture-phase mousedown listener then never
|
|
9350
|
+
fires and the menu stayed open when the next press landed on a list
|
|
9351
|
+
blank (user report). pointerdown always dispatches. */
|
|
9352
|
+
const timer = setTimeout(() => document.addEventListener('pointerdown', onDown, true), 0)
|
|
9306
9353
|
document.addEventListener('keydown', onKey)
|
|
9307
9354
|
return () => {
|
|
9308
9355
|
clearTimeout(timer)
|
|
9309
|
-
document.removeEventListener('
|
|
9356
|
+
document.removeEventListener('pointerdown', onDown, true)
|
|
9310
9357
|
document.removeEventListener('keydown', onKey)
|
|
9311
9358
|
}
|
|
9312
9359
|
}, [])
|
|
@@ -10293,14 +10340,18 @@ window.__ModuleLoader__.load({
|
|
|
10293
10340
|
// A single click only SELECTS (now with Shift/Ctrl+click multi-select —
|
|
10294
10341
|
// the modifiers are resolved one level up in BranchTree): checking a
|
|
10295
10342
|
// branch out is a real change of working tree, and a stray click in a
|
|
10296
|
-
// long list must not do that (reported on v0.3.7).
|
|
10297
|
-
//
|
|
10343
|
+
// long list must not do that (reported on v0.3.7). Since v0.19.3 a
|
|
10344
|
+
// double-click BROWSES that branch's commit history (checkout moved into
|
|
10345
|
+
// the right-click menu), right-click has the whole menu.
|
|
10298
10346
|
return E('div', {
|
|
10299
10347
|
className: 'dig-row dig-row-' + kind + (props.selected === true ? ' dig-row-selected' : ''),
|
|
10300
10348
|
style: { paddingLeft: (BRANCH_INDENT_BASE + (props.depth === undefined ? 0 : props.depth) * BRANCH_INDENT_STEP) + 'px' },
|
|
10349
|
+
/* Rubber-band anchor: the marquee intersects [data-mkey] rows to
|
|
10350
|
+
rebuild the picked set (selKey = local:/remote:/tag: prefixed). */
|
|
10351
|
+
'data-mkey': props.selKey,
|
|
10301
10352
|
title: where + ' · ' + t('branches.pickHint'),
|
|
10302
10353
|
onClick: (event) => props.onPick(event, entry, props.selKey),
|
|
10303
|
-
onDoubleClick: () => props.
|
|
10354
|
+
onDoubleClick: () => { if (props.onBrowse !== undefined) props.onBrowse(entry) },
|
|
10304
10355
|
onContextMenu: (event) => { event.preventDefault(); props.onMenu(event, entry) },
|
|
10305
10356
|
},
|
|
10306
10357
|
E('span', { className: 'dig-row-icon dig-tone-' + tone + (starred === true ? ' dig-star-filled' : '') }, E(Icon, { name: glyph, size: 12 })),
|
|
@@ -10352,6 +10403,10 @@ window.__ModuleLoader__.load({
|
|
|
10352
10403
|
// favorites section and the local tree show the starred rows first.
|
|
10353
10404
|
const locals = (branches === null ? [] : branches.local).filter((entry) => match(entry.name))
|
|
10354
10405
|
.sort((a, b) => (favorites.indexOf(a.name) >= 0 ? 0 : 1) - (favorites.indexOf(b.name) >= 0 ? 0 : 1))
|
|
10406
|
+
/* The HEAD row acts like a branch row since v0.19.3: click selects,
|
|
10407
|
+
double-click browses the current branch's history, right-click opens
|
|
10408
|
+
the same row menu (quick jump back to HEAD lives there). */
|
|
10409
|
+
const headEntry = locals.find((entry) => entry.head === true) || null
|
|
10355
10410
|
/* A refresh that makes a picked ref disappear must not keep it selected. */
|
|
10356
10411
|
useEffect(() => {
|
|
10357
10412
|
setPicks((previous) => {
|
|
@@ -10439,7 +10494,7 @@ window.__ModuleLoader__.load({
|
|
|
10439
10494
|
selected: picks.has(branchSelKeyOf(entry)) === true,
|
|
10440
10495
|
starred: entry.tag !== true && entry.remote !== true && favorites.indexOf(entry.name) >= 0,
|
|
10441
10496
|
onPick: handlePick,
|
|
10442
|
-
|
|
10497
|
+
onBrowse: props.onBrowse, onMenu: onRowMenu,
|
|
10443
10498
|
})
|
|
10444
10499
|
const rowsOf = (node, prefix, depth) => {
|
|
10445
10500
|
const out = []
|
|
@@ -10504,6 +10559,30 @@ window.__ModuleLoader__.load({
|
|
|
10504
10559
|
}
|
|
10505
10560
|
props.onBranchMenu(event, entry, null)
|
|
10506
10561
|
}
|
|
10562
|
+
/* v0.19.3: the tree rubber-bands like the other two lists (shared hook).
|
|
10563
|
+
Tree rows have no fixed height and render unfolded-only, so the row
|
|
10564
|
+
rects are a LIVE DOM measure over the [data-mkey] rows BranchRow
|
|
10565
|
+
carries; section/folder headers are buttons and never start a drag.
|
|
10566
|
+
Favorited branches render twice (favorites + local sections) — both
|
|
10567
|
+
map to one selKey, so a Set dedupes them for free. */
|
|
10568
|
+
const treeHostRef = useRef(null)
|
|
10569
|
+
const treeScrollRef = useRef(null)
|
|
10570
|
+
const treeMarquee = useRubberBand({
|
|
10571
|
+
scrollRef: treeScrollRef,
|
|
10572
|
+
drawRef: treeHostRef,
|
|
10573
|
+
rectsOf: () => {
|
|
10574
|
+
const host = treeScrollRef.current
|
|
10575
|
+
if (host === null) return []
|
|
10576
|
+
const hostRect = host.getBoundingClientRect()
|
|
10577
|
+
const rects = []
|
|
10578
|
+
host.querySelectorAll('[data-mkey]').forEach((row) => {
|
|
10579
|
+
const rect = row.getBoundingClientRect()
|
|
10580
|
+
rects.push({ key: row.getAttribute('data-mkey'), top: rect.top - hostRect.top + host.scrollTop, bottom: rect.bottom - hostRect.top + host.scrollTop })
|
|
10581
|
+
})
|
|
10582
|
+
return rects
|
|
10583
|
+
},
|
|
10584
|
+
onPick: (keys) => setPicks(new Set(keys)),
|
|
10585
|
+
})
|
|
10507
10586
|
/* List-scope keyboard: Ctrl/Cmd+C copies the picked names, Delete asks
|
|
10508
10587
|
Panel for a grouped delete (head branches are skipped — git refuses
|
|
10509
10588
|
them anyway), Ctrl+A selects every visible row, Esc clears. */
|
|
@@ -10533,6 +10612,7 @@ window.__ModuleLoader__.load({
|
|
|
10533
10612
|
}
|
|
10534
10613
|
return E('div', {
|
|
10535
10614
|
className: 'dig-tree',
|
|
10615
|
+
ref: treeHostRef,
|
|
10536
10616
|
tabIndex: 0,
|
|
10537
10617
|
onKeyDown: onTreeKeyDown,
|
|
10538
10618
|
onPointerDownCapture: (event) => {
|
|
@@ -10545,12 +10625,26 @@ window.__ModuleLoader__.load({
|
|
|
10545
10625
|
placeholder: t('branches.filter'),
|
|
10546
10626
|
onChange: (event) => setFilter(event.target.value),
|
|
10547
10627
|
})),
|
|
10548
|
-
E('div', {
|
|
10549
|
-
|
|
10628
|
+
E('div', {
|
|
10629
|
+
className: 'dig-tree-scroll',
|
|
10630
|
+
ref: treeScrollRef,
|
|
10631
|
+
onPointerDown: treeMarquee.onPointerDown,
|
|
10632
|
+
onClickCapture: (event) => {
|
|
10633
|
+
if (treeMarquee.swallowRef.current === true) { event.preventDefault(); event.stopPropagation() }
|
|
10634
|
+
},
|
|
10635
|
+
},
|
|
10636
|
+
headEntry === null ? E('div', { className: 'dig-section-head dig-section-head-static' }, t('branches.head')) : E('div', {
|
|
10637
|
+
className: 'dig-section-head dig-section-head-static dig-head-branch',
|
|
10638
|
+
title: headEntry.name + ' · ' + t('branches.headHint'),
|
|
10639
|
+
onClick: (event) => { event.stopPropagation(); handlePick(event, headEntry, branchSelKeyOf(headEntry)) },
|
|
10640
|
+
onDoubleClick: () => { if (props.onBrowse !== undefined) props.onBrowse(headEntry) },
|
|
10641
|
+
onContextMenu: (event) => { event.preventDefault(); onRowMenu(event, headEntry) },
|
|
10642
|
+
}, t('branches.head')),
|
|
10550
10643
|
favorites.length === 0 ? null : section('favorites', t('branches.favorites'), locals.filter((entry) => favorites.indexOf(entry.name) >= 0), (entry) => entry),
|
|
10551
10644
|
section('local', t('branches.local'), locals, (entry) => entry, true),
|
|
10552
10645
|
section('remote', t('branches.remote'), remotes, (entry) => Object.assign({}, entry, { remote: true })),
|
|
10553
|
-
section('tags', t('branches.tags'), tags, (entry) => Object.assign({}, entry, { tag: true })))
|
|
10646
|
+
section('tags', t('branches.tags'), tags, (entry) => Object.assign({}, entry, { tag: true }))),
|
|
10647
|
+
E(MarqueeBox, { box: treeMarquee.box }))
|
|
10554
10648
|
}
|
|
10555
10649
|
|
|
10556
10650
|
/* ============================== changes ============================== */
|
|
@@ -10645,10 +10739,11 @@ window.__ModuleLoader__.load({
|
|
|
10645
10739
|
const anchorRef = props.anchorRef
|
|
10646
10740
|
const changeItemsRef = useRef(null)
|
|
10647
10741
|
const changeOffsetsRef = useRef(null)
|
|
10648
|
-
|
|
10649
|
-
|
|
10650
|
-
|
|
10651
|
-
|
|
10742
|
+
/* The marquee lives in the shared useRubberBand hook (v0.19.3: history
|
|
10743
|
+
and the branch tree drag the same way). changesHostRef is the DRAW
|
|
10744
|
+
host — the hook comment explains why the paint rect must be the list
|
|
10745
|
+
wrapper's, not the scroll container's (v0.19.2 offset report). */
|
|
10746
|
+
const changesHostRef = useRef(null)
|
|
10652
10747
|
/* Rows in their RENDER order (the order windowing flattens them into) —
|
|
10653
10748
|
Shift-click spans follow what the eye sees, not the git order. */
|
|
10654
10749
|
const orderedRowKeys = () => {
|
|
@@ -10700,96 +10795,10 @@ window.__ModuleLoader__.load({
|
|
|
10700
10795
|
not yank the pane away (v0.18.3). */
|
|
10701
10796
|
setSelection((previous) => previous.size === 1 && previous.has(key) === true ? previous : new Set([key]))
|
|
10702
10797
|
}, [setSelection, anchorRef, props.onDiff])
|
|
10703
|
-
/*
|
|
10704
|
-
heights + the prefix-sum offsets
|
|
10705
|
-
|
|
10706
|
-
|
|
10707
|
-
is a viewport-anchored absolute div inside .dig-changes (invariant 13:
|
|
10708
|
-
never position:fixed inside the workbench's contain:layout). ---- */
|
|
10709
|
-
const marqueeKeysAt = (contentTop, contentBottom) => {
|
|
10710
|
-
const items = changeItemsRef.current
|
|
10711
|
-
const offsets = changeOffsetsRef.current
|
|
10712
|
-
if (items === null || offsets === null || items.length === 0) return []
|
|
10713
|
-
const first = Math.max(0, windowStart(offsets, contentTop))
|
|
10714
|
-
const last = Math.min(items.length - 1, windowStart(offsets, contentBottom))
|
|
10715
|
-
const keys = []
|
|
10716
|
-
for (let index = first; index <= last; index += 1) {
|
|
10717
|
-
const entry = items[index]
|
|
10718
|
-
if (entry.kind === 'row' && entry.key.indexOf('ignored:') !== 0) keys.push(entry.key)
|
|
10719
|
-
}
|
|
10720
|
-
return keys
|
|
10721
|
-
}
|
|
10722
|
-
const syncMarquee = () => {
|
|
10723
|
-
const state = marqueeState.current
|
|
10724
|
-
const host = changesScrollRef.current
|
|
10725
|
-
if (state === null || host === null) return
|
|
10726
|
-
const rect = host.getBoundingClientRect()
|
|
10727
|
-
const contentY0 = state.startY - rect.top + state.scrollY0
|
|
10728
|
-
const contentY1 = state.clientY - rect.top + host.scrollTop
|
|
10729
|
-
setSelection(new Set(marqueeKeysAt(Math.min(contentY0, contentY1), Math.max(contentY0, contentY1))))
|
|
10730
|
-
setMarquee({
|
|
10731
|
-
x: Math.min(state.startX, state.clientX) - rect.left,
|
|
10732
|
-
y: Math.min(state.startY, state.clientY) - rect.top,
|
|
10733
|
-
w: Math.abs(state.clientX - state.startX),
|
|
10734
|
-
h: Math.abs(state.clientY - state.startY),
|
|
10735
|
-
})
|
|
10736
|
-
/* Auto-scroll when the pointer rides an edge; keep the loop alive while
|
|
10737
|
-
it does so the selection follows the content streaming past. */
|
|
10738
|
-
const EDGE = 28
|
|
10739
|
-
let speed = 0
|
|
10740
|
-
if (state.clientY - rect.top < EDGE) speed = -Math.ceil((EDGE - (state.clientY - rect.top)) / 3)
|
|
10741
|
-
else if (rect.bottom - state.clientY < EDGE) speed = Math.ceil((EDGE - (rect.bottom - state.clientY)) / 3)
|
|
10742
|
-
if (speed !== 0) {
|
|
10743
|
-
host.scrollTop += speed * 2
|
|
10744
|
-
cancelAnimationFrame(marqueeFrame.current)
|
|
10745
|
-
marqueeFrame.current = requestAnimationFrame(syncMarquee)
|
|
10746
|
-
}
|
|
10747
|
-
}
|
|
10748
|
-
const scheduleMarqueeSync = () => {
|
|
10749
|
-
cancelAnimationFrame(marqueeFrame.current)
|
|
10750
|
-
marqueeFrame.current = requestAnimationFrame(syncMarquee)
|
|
10751
|
-
}
|
|
10752
|
-
const onListPointerDown = (event) => {
|
|
10753
|
-
if (event.button !== 0) return
|
|
10754
|
-
const host = changesScrollRef.current
|
|
10755
|
-
if (host === null) return
|
|
10756
|
-
if (event.target instanceof Element && event.target.closest('button, input, textarea, select, a') !== null) return
|
|
10757
|
-
// Keep the browser from starting a text selection under the drag; the
|
|
10758
|
-
// real clicks still fire (only the selection machinery is suppressed).
|
|
10759
|
-
event.preventDefault()
|
|
10760
|
-
const rect = host.getBoundingClientRect()
|
|
10761
|
-
marqueeState.current = {
|
|
10762
|
-
startX: event.clientX,
|
|
10763
|
-
startY: event.clientY,
|
|
10764
|
-
scrollY0: host.scrollTop,
|
|
10765
|
-
clientX: event.clientX,
|
|
10766
|
-
clientY: event.clientY,
|
|
10767
|
-
active: false,
|
|
10768
|
-
}
|
|
10769
|
-
const onMove = (moveEvent) => {
|
|
10770
|
-
const state = marqueeState.current
|
|
10771
|
-
if (state === null) return
|
|
10772
|
-
state.clientX = moveEvent.clientX
|
|
10773
|
-
state.clientY = moveEvent.clientY
|
|
10774
|
-
if (state.active === false && Math.abs(moveEvent.clientX - state.startX) < 4 && Math.abs(moveEvent.clientY - state.startY) < 4) return
|
|
10775
|
-
state.active = true
|
|
10776
|
-
marqueeSwallowClick.current = true
|
|
10777
|
-
scheduleMarqueeSync()
|
|
10778
|
-
}
|
|
10779
|
-
const onUp = () => {
|
|
10780
|
-
window.removeEventListener('pointermove', onMove)
|
|
10781
|
-
window.removeEventListener('pointerup', onUp)
|
|
10782
|
-
cancelAnimationFrame(marqueeFrame.current)
|
|
10783
|
-
const wasActive = marqueeState.current !== null && marqueeState.current.active === true
|
|
10784
|
-
marqueeState.current = null
|
|
10785
|
-
setMarquee(null)
|
|
10786
|
-
// The drag's pointerup is followed by a synthetic click on whatever
|
|
10787
|
-
// row is under it — swallow exactly that one click.
|
|
10788
|
-
if (wasActive === true) setTimeout(() => { marqueeSwallowClick.current = false }, 0)
|
|
10789
|
-
}
|
|
10790
|
-
window.addEventListener('pointermove', onMove)
|
|
10791
|
-
window.addEventListener('pointerup', onUp)
|
|
10792
|
-
}
|
|
10798
|
+
/* Rubber-band selection now comes from the shared useRubberBand hook
|
|
10799
|
+
(see its comment): the fixed row heights + the prefix-sum offsets
|
|
10800
|
+
still turn a pixel span back into row keys without any DOM rows —
|
|
10801
|
+
the rectsOf below is the exact former marqueeKeysAt, reshaped. */
|
|
10793
10802
|
const changes = summary === null ? null : summary.changes
|
|
10794
10803
|
const conflicted = changes === null ? [] : changes.conflicted
|
|
10795
10804
|
const staged = changes === null ? [] : changes.staged
|
|
@@ -10859,6 +10868,22 @@ window.__ModuleLoader__.load({
|
|
|
10859
10868
|
mounted. A tree with thousands of files scrolls at frame rate. */
|
|
10860
10869
|
const changesScrollRef = useRef(null)
|
|
10861
10870
|
const changesViewport = useViewportBox(changesScrollRef)
|
|
10871
|
+
const changeMarquee = useRubberBand({
|
|
10872
|
+
scrollRef: changesScrollRef,
|
|
10873
|
+
drawRef: changesHostRef,
|
|
10874
|
+
rectsOf: () => {
|
|
10875
|
+
const items = changeItemsRef.current
|
|
10876
|
+
const offsets = changeOffsetsRef.current
|
|
10877
|
+
if (items === null || offsets === null || items.length === 0) return []
|
|
10878
|
+
const rects = []
|
|
10879
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
10880
|
+
const entry = items[index]
|
|
10881
|
+
if (entry.kind === 'row' && entry.key.indexOf('ignored:') !== 0) rects.push({ key: entry.key, top: offsets[index], bottom: offsets[index + 1] })
|
|
10882
|
+
}
|
|
10883
|
+
return rects
|
|
10884
|
+
},
|
|
10885
|
+
onPick: (keys) => setSelection(new Set(keys)),
|
|
10886
|
+
})
|
|
10862
10887
|
const changeItems = useMemo(() => {
|
|
10863
10888
|
const out = []
|
|
10864
10889
|
const groupHead = (key, title, entries) => E('div', { className: 'dig-group-head' },
|
|
@@ -11042,7 +11067,7 @@ window.__ModuleLoader__.load({
|
|
|
11042
11067
|
props.onBulkDiscard === undefined ? null : E('button', { type: 'button', className: 'dig-link dig-link-danger', onClick: () => props.onBulkDiscard() }, t('action.discard')),
|
|
11043
11068
|
props.onCopySelection === undefined ? null : E('button', { type: 'button', className: 'dig-link', onClick: () => props.onCopySelection() }, t('selection.copyPaths')),
|
|
11044
11069
|
E('button', { type: 'button', className: 'dig-link', onClick: () => setSelection(new Set()) }, t('selection.clear')))
|
|
11045
|
-
return E('div', { className: 'dig-changes' },
|
|
11070
|
+
return E('div', { className: 'dig-changes', ref: changesHostRef },
|
|
11046
11071
|
aiConfirmDialog,
|
|
11047
11072
|
head,
|
|
11048
11073
|
E('div', { className: 'dig-changes-body' },
|
|
@@ -11055,9 +11080,9 @@ window.__ModuleLoader__.load({
|
|
|
11055
11080
|
onPointerDownCapture: (event) => {
|
|
11056
11081
|
if (isTypingTarget(event.target) === false) event.currentTarget.focus({ preventScroll: true })
|
|
11057
11082
|
},
|
|
11058
|
-
onPointerDown:
|
|
11083
|
+
onPointerDown: changeMarquee.onPointerDown,
|
|
11059
11084
|
onClickCapture: (event) => {
|
|
11060
|
-
if (
|
|
11085
|
+
if (changeMarquee.swallowRef.current === true) { event.preventDefault(); event.stopPropagation() }
|
|
11061
11086
|
},
|
|
11062
11087
|
},
|
|
11063
11088
|
collapsed ? null : [
|
|
@@ -11074,10 +11099,7 @@ window.__ModuleLoader__.load({
|
|
|
11074
11099
|
and nothing on the list (group head, rows) is ever covered. */
|
|
11075
11100
|
selectionBar,
|
|
11076
11101
|
composer),
|
|
11077
|
-
|
|
11078
|
-
className: 'dig-marquee',
|
|
11079
|
-
style: { left: marquee.x + 'px', top: marquee.y + 'px', width: marquee.w + 'px', height: marquee.h + 'px' },
|
|
11080
|
-
}))
|
|
11102
|
+
E(MarqueeBox, { box: changeMarquee.box }))
|
|
11081
11103
|
}
|
|
11082
11104
|
|
|
11083
11105
|
/* ============================== history ============================== */
|
|
@@ -11143,6 +11165,122 @@ window.__ModuleLoader__.load({
|
|
|
11143
11165
|
return { scrollTop: scrollTop, viewH: viewH }
|
|
11144
11166
|
}
|
|
11145
11167
|
|
|
11168
|
+
/* ---- rubber-band (marquee) selection, shared by all three lists ----
|
|
11169
|
+
One hook, three wiring sites (changes, history, branch tree). The
|
|
11170
|
+
contract: `scrollRef` is the scrolling container the SELECTION math
|
|
11171
|
+
runs against, `drawRef` is the element the marquee box is PAINTED in
|
|
11172
|
+
(its own rect — v0.18.x painted from the scroll container's rect while
|
|
11173
|
+
the box lived inside the list wrapper, so the toolbar height above the
|
|
11174
|
+
list shifted every box visually upward; reported v0.19.2), `rectsOf`
|
|
11175
|
+
returns {key,top,bottom} rows in scroll-content coordinates (exact
|
|
11176
|
+
prefix-sums for the windowed lists, a live DOM measure for the
|
|
11177
|
+
variable-height tree), and `onPick` receives the intersecting keys.
|
|
11178
|
+
A 4px threshold keeps plain clicks clicky; pointerdown is preventDefault-ed
|
|
11179
|
+
so drags never start a native text selection; edge auto-scroll and the
|
|
11180
|
+
rAF-throttled sync carry over unchanged from v0.18.x. */
|
|
11181
|
+
function useRubberBand(options) {
|
|
11182
|
+
const [box, setBox] = useState(null)
|
|
11183
|
+
const stateRef = useRef(null)
|
|
11184
|
+
const frameRef = useRef(0)
|
|
11185
|
+
const swallowRef = useRef(false)
|
|
11186
|
+
const optionsRef = useRef(options)
|
|
11187
|
+
optionsRef.current = options
|
|
11188
|
+
const sync = () => {
|
|
11189
|
+
const opts = optionsRef.current
|
|
11190
|
+
const state = stateRef.current
|
|
11191
|
+
const host = opts.scrollRef.current
|
|
11192
|
+
const draw = opts.drawRef.current
|
|
11193
|
+
if (state === null || host === null || draw === null) return
|
|
11194
|
+
const hostRect = host.getBoundingClientRect()
|
|
11195
|
+
const drawRect = draw.getBoundingClientRect()
|
|
11196
|
+
const y0 = state.startY - hostRect.top + state.scrollY0
|
|
11197
|
+
const y1 = state.clientY - hostRect.top + host.scrollTop
|
|
11198
|
+
const low = Math.min(y0, y1)
|
|
11199
|
+
const high = Math.max(y0, y1)
|
|
11200
|
+
const keys = []
|
|
11201
|
+
for (const rect of opts.rectsOf()) {
|
|
11202
|
+
if (rect.top < high && rect.bottom > low) keys.push(rect.key)
|
|
11203
|
+
}
|
|
11204
|
+
opts.onPick(keys)
|
|
11205
|
+
setBox({
|
|
11206
|
+
x: Math.min(state.startX, state.clientX) - drawRect.left,
|
|
11207
|
+
y: Math.min(state.startY, state.clientY) - drawRect.top,
|
|
11208
|
+
w: Math.abs(state.clientX - state.startX),
|
|
11209
|
+
h: Math.abs(state.clientY - state.startY),
|
|
11210
|
+
})
|
|
11211
|
+
/* Auto-scroll when the pointer rides an edge; keep the loop alive while
|
|
11212
|
+
it does so the selection follows the content streaming past. */
|
|
11213
|
+
const EDGE = 28
|
|
11214
|
+
let speed = 0
|
|
11215
|
+
if (state.clientY - hostRect.top < EDGE) speed = -Math.ceil((EDGE - (state.clientY - hostRect.top)) / 3)
|
|
11216
|
+
else if (hostRect.bottom - state.clientY < EDGE) speed = Math.ceil((EDGE - (hostRect.bottom - state.clientY)) / 3)
|
|
11217
|
+
if (speed !== 0) {
|
|
11218
|
+
host.scrollTop += speed * 2
|
|
11219
|
+
cancelAnimationFrame(frameRef.current)
|
|
11220
|
+
frameRef.current = requestAnimationFrame(sync)
|
|
11221
|
+
}
|
|
11222
|
+
}
|
|
11223
|
+
const schedule = () => {
|
|
11224
|
+
cancelAnimationFrame(frameRef.current)
|
|
11225
|
+
frameRef.current = requestAnimationFrame(sync)
|
|
11226
|
+
}
|
|
11227
|
+
useEffect(() => () => cancelAnimationFrame(frameRef.current), [])
|
|
11228
|
+
const onPointerDown = (event) => {
|
|
11229
|
+
if (event.button !== 0) return
|
|
11230
|
+
const opts = optionsRef.current
|
|
11231
|
+
const host = opts.scrollRef.current
|
|
11232
|
+
if (host === null) return
|
|
11233
|
+
if (event.target instanceof Element && event.target.closest('button, input, textarea, select, a') !== null) return
|
|
11234
|
+
// Keep the browser from starting a text selection under the drag (the
|
|
11235
|
+
// history/tree lists dragged out blue text before); the real clicks
|
|
11236
|
+
// still fire — only the selection machinery is suppressed.
|
|
11237
|
+
event.preventDefault()
|
|
11238
|
+
stateRef.current = {
|
|
11239
|
+
startX: event.clientX,
|
|
11240
|
+
startY: event.clientY,
|
|
11241
|
+
scrollY0: host.scrollTop,
|
|
11242
|
+
clientX: event.clientX,
|
|
11243
|
+
clientY: event.clientY,
|
|
11244
|
+
active: false,
|
|
11245
|
+
}
|
|
11246
|
+
const onMove = (moveEvent) => {
|
|
11247
|
+
const state = stateRef.current
|
|
11248
|
+
if (state === null) return
|
|
11249
|
+
state.clientX = moveEvent.clientX
|
|
11250
|
+
state.clientY = moveEvent.clientY
|
|
11251
|
+
if (state.active === false && Math.abs(moveEvent.clientX - state.startX) < 4 && Math.abs(moveEvent.clientY - state.startY) < 4) return
|
|
11252
|
+
state.active = true
|
|
11253
|
+
swallowRef.current = true
|
|
11254
|
+
schedule()
|
|
11255
|
+
}
|
|
11256
|
+
const onUp = () => {
|
|
11257
|
+
window.removeEventListener('pointermove', onMove)
|
|
11258
|
+
window.removeEventListener('pointerup', onUp)
|
|
11259
|
+
cancelAnimationFrame(frameRef.current)
|
|
11260
|
+
const wasActive = stateRef.current !== null && stateRef.current.active === true
|
|
11261
|
+
stateRef.current = null
|
|
11262
|
+
setBox(null)
|
|
11263
|
+
// The drag's pointerup is followed by a synthetic click on whatever
|
|
11264
|
+
// row is under it — swallow exactly that one click.
|
|
11265
|
+
if (wasActive === true) setTimeout(() => { swallowRef.current = false }, 0)
|
|
11266
|
+
}
|
|
11267
|
+
window.addEventListener('pointermove', onMove)
|
|
11268
|
+
window.addEventListener('pointerup', onUp)
|
|
11269
|
+
}
|
|
11270
|
+
return { box: box, onPointerDown: onPointerDown, swallowRef: swallowRef }
|
|
11271
|
+
}
|
|
11272
|
+
|
|
11273
|
+
/* The marquee box itself: a viewport-anchored absolute div inside the
|
|
11274
|
+
draw host (invariant 13: never position:fixed inside the workbench's
|
|
11275
|
+
contain:layout). */
|
|
11276
|
+
function MarqueeBox(props) {
|
|
11277
|
+
if (props.box === null) return null
|
|
11278
|
+
return E('div', {
|
|
11279
|
+
className: 'dig-marquee',
|
|
11280
|
+
style: { left: props.box.x + 'px', top: props.box.y + 'px', width: props.box.w + 'px', height: props.box.h + 'px' },
|
|
11281
|
+
})
|
|
11282
|
+
}
|
|
11283
|
+
|
|
11146
11284
|
/* First index whose cumulative offset exceeds `at` (binary search over the
|
|
11147
11285
|
prefix-sum array; offsets.length === items.length + 1). */
|
|
11148
11286
|
function windowStart(offsets, at) {
|
|
@@ -11249,11 +11387,44 @@ window.__ModuleLoader__.load({
|
|
|
11249
11387
|
return 0
|
|
11250
11388
|
}
|
|
11251
11389
|
|
|
11390
|
+
/* The author filter pins "me (name)" to the top (v0.19.3). The identity
|
|
11391
|
+
is resolved once per repo root and kept for the panel lifetime — git
|
|
11392
|
+
identities don't churn within a session — so re-opening the Git tab or
|
|
11393
|
+
hopping sessions never re-runs `git config`. */
|
|
11394
|
+
const gitMeCache = new Map()
|
|
11395
|
+
|
|
11252
11396
|
function HistoryList(props) {
|
|
11253
11397
|
const t = props.t
|
|
11254
11398
|
const commits = props.commits
|
|
11255
11399
|
const historyScrollRef = useRef(null)
|
|
11400
|
+
const [me, setMe] = useState(() => (props.base !== undefined && gitMeCache.has(props.base.repoRoot) === true ? gitMeCache.get(props.base.repoRoot) : null))
|
|
11401
|
+
useEffect(() => {
|
|
11402
|
+
const base = props.base
|
|
11403
|
+
if (base === undefined || base.repoRoot === undefined) return
|
|
11404
|
+
if (gitMeCache.has(base.repoRoot) === true) { setMe(gitMeCache.get(base.repoRoot)); return }
|
|
11405
|
+
let alive = true
|
|
11406
|
+
props.request('me', Object.assign({}, base)).then((data) => {
|
|
11407
|
+
const who = data !== null && typeof data === 'object' && typeof data.name === 'string' && data.name !== ''
|
|
11408
|
+
? { name: data.name, email: typeof data.email === 'string' ? data.email : '' }
|
|
11409
|
+
: null
|
|
11410
|
+
gitMeCache.set(base.repoRoot, who)
|
|
11411
|
+
if (alive === true) setMe(who)
|
|
11412
|
+
}).catch((error) => { void error })
|
|
11413
|
+
return () => { alive = false }
|
|
11414
|
+
}, [props.base])
|
|
11256
11415
|
const viewport = useViewportBox(historyScrollRef)
|
|
11416
|
+
/* v0.19.3: the commit log rubber-bands exactly like the changes list —
|
|
11417
|
+
same hook, same gestures. Fixed HISTORY_ROW_H rows make the rects a
|
|
11418
|
+
pure index map, so even a windowed (mostly unmounted) list drags
|
|
11419
|
+
perfectly; picks land in the SAME pickedHashes set the Shift/Ctrl
|
|
11420
|
+
clicks and the commit context menu already share. */
|
|
11421
|
+
const historyHostRef = useRef(null)
|
|
11422
|
+
const historyMarquee = useRubberBand({
|
|
11423
|
+
scrollRef: historyScrollRef,
|
|
11424
|
+
drawRef: historyHostRef,
|
|
11425
|
+
rectsOf: () => rows.map((row, index) => ({ key: row.commit.hash, top: index * HISTORY_ROW_H, bottom: (index + 1) * HISTORY_ROW_H })),
|
|
11426
|
+
onPick: (keys) => { if (props.onPicksChange !== undefined) props.onPicksChange(new Set(keys)) },
|
|
11427
|
+
})
|
|
11257
11428
|
const pickAnchor = useRef(null)
|
|
11258
11429
|
const [text, setText] = useState('')
|
|
11259
11430
|
const [author, setAuthor] = useState('')
|
|
@@ -11309,6 +11480,92 @@ window.__ModuleLoader__.load({
|
|
|
11309
11480
|
}, [commits, text, author, refName, since, order])
|
|
11310
11481
|
|
|
11311
11482
|
const rows = useMemo(() => buildRows(filtered), [filtered])
|
|
11483
|
+
|
|
11484
|
+
/* Weibo-style infinite scroll (v0.19.3): descending lists auto-load when
|
|
11485
|
+
the user scrolls near the bottom edge, ascending ones near the top
|
|
11486
|
+
edge — wheel and scrollbar drags both surface as scroll events, no
|
|
11487
|
+
extra clicks. The button stays as the visible affordance. The guard
|
|
11488
|
+
keeps one request in flight; it resets when busy settles. */
|
|
11489
|
+
const infiniteGuardRef = useRef(false)
|
|
11490
|
+
const growAnchorRef = useRef(null)
|
|
11491
|
+
const mergeLocatedRef = useRef(false)
|
|
11492
|
+
const requestMore = () => {
|
|
11493
|
+
if (props.hasMore !== true || props.busy === true || infiniteGuardRef.current === true) return
|
|
11494
|
+
const host = historyScrollRef.current
|
|
11495
|
+
growAnchorRef.current = { length: rows.length, height: host === null ? 0 : host.scrollHeight }
|
|
11496
|
+
infiniteGuardRef.current = true
|
|
11497
|
+
props.onLoadMore()
|
|
11498
|
+
}
|
|
11499
|
+
/* The button itself is the trigger (weibo-style, v0.19.3): an
|
|
11500
|
+
IntersectionObserver on the load-more button fires wherever it sits —
|
|
11501
|
+
bottom while descending, top while ascending — covering wheel,
|
|
11502
|
+
scrollbar drags, first paint, and short pages alike. Re-created per
|
|
11503
|
+
page so a settled load re-evaluates: if the button is STILL visible
|
|
11504
|
+
(huge viewport / tiny page) the next page follows automatically. */
|
|
11505
|
+
const loadMoreRef = useRef(null)
|
|
11506
|
+
/* A scroll that lands on the button WHILE a guarded load is still in
|
|
11507
|
+
flight loses its one IO callback to the busy bail-out, and the
|
|
11508
|
+
observer is not re-created when that load finishes with the same
|
|
11509
|
+
page size (mount refresh). Re-check the button's geometry when busy
|
|
11510
|
+
settles: still within the rootMargin window → continue loading. */
|
|
11511
|
+
useEffect(() => {
|
|
11512
|
+
if (props.busy !== false) return
|
|
11513
|
+
infiniteGuardRef.current = false
|
|
11514
|
+
const node = loadMoreRef.current
|
|
11515
|
+
const root = historyScrollRef.current
|
|
11516
|
+
if (node === null || root === null || props.hasMore !== true) return
|
|
11517
|
+
const rect = node.getBoundingClientRect()
|
|
11518
|
+
const box = root.getBoundingClientRect()
|
|
11519
|
+
if (rect.bottom >= box.top - 240 && rect.top <= box.bottom + 240) requestMore()
|
|
11520
|
+
}, [props.busy])
|
|
11521
|
+
useEffect(() => {
|
|
11522
|
+
const node = loadMoreRef.current
|
|
11523
|
+
const root = historyScrollRef.current
|
|
11524
|
+
if (node === null || root === null || props.hasMore !== true) return
|
|
11525
|
+
let frame = 0
|
|
11526
|
+
const io = new IntersectionObserver((entries) => {
|
|
11527
|
+
if (entries.some((entry) => entry.isIntersecting === true) === false) return
|
|
11528
|
+
if (frame === 0) {
|
|
11529
|
+
frame = requestAnimationFrame(() => { frame = 0; requestMore() })
|
|
11530
|
+
}
|
|
11531
|
+
}, { root: root, rootMargin: '240px 0px' })
|
|
11532
|
+
io.observe(node)
|
|
11533
|
+
return () => { if (frame !== 0) cancelAnimationFrame(frame); io.disconnect() }
|
|
11534
|
+
}, [props.hasMore, order, rows.length])
|
|
11535
|
+
/* Ascending growth inserts rows above the viewport: re-anchor by the
|
|
11536
|
+
height delta so the rows the user was reading stay put. */
|
|
11537
|
+
useEffect(() => {
|
|
11538
|
+
const anchor = growAnchorRef.current
|
|
11539
|
+
if (anchor === null || rows.length <= anchor.length) return
|
|
11540
|
+
growAnchorRef.current = null
|
|
11541
|
+
const host = historyScrollRef.current
|
|
11542
|
+
if (order !== 'asc' || host === null) return
|
|
11543
|
+
host.scrollTop = host.scrollTop + (host.scrollHeight - anchor.height)
|
|
11544
|
+
}, [rows.length, order])
|
|
11545
|
+
/* v0.19.3 IDEA-style merge memory: when a browsed branch has a merge
|
|
11546
|
+
relationship with the PREVIOUSLY browsed one, its list opens scrolled
|
|
11547
|
+
to that merge commit ('merge-point' resolved it) — the "2 was merged
|
|
11548
|
+
here" moment is on screen immediately, while 1's newer commits stay
|
|
11549
|
+
one wheel-scroll away. Fires only on anchor CHANGE; null = no
|
|
11550
|
+
relationship (or first hop), leave the top alone. */
|
|
11551
|
+
useEffect(() => {
|
|
11552
|
+
const anchor = props.mergeAnchor
|
|
11553
|
+
if (anchor === undefined || anchor === null) { mergeLocatedRef.current = false; return }
|
|
11554
|
+
const index = rows.findIndex((row) => row.commit.hash === anchor)
|
|
11555
|
+
if (index < 0) {
|
|
11556
|
+
/* The merge point can sit below the first page on a long branch
|
|
11557
|
+
(real repos easily outrun LIMIT). Keep auto-loading — the same
|
|
11558
|
+
weibo path as the edge trigger — until it surfaces or the log
|
|
11559
|
+
is exhausted; one location per anchor, so later 'load more'
|
|
11560
|
+
never yanks the view back. */
|
|
11561
|
+
if (mergeLocatedRef.current === false && props.hasMore === true && props.busy !== true) requestMore()
|
|
11562
|
+
return
|
|
11563
|
+
}
|
|
11564
|
+
if (mergeLocatedRef.current === true) return
|
|
11565
|
+
mergeLocatedRef.current = true
|
|
11566
|
+
const host = historyScrollRef.current
|
|
11567
|
+
if (host !== null) host.scrollTop = Math.max(0, index * HISTORY_ROW_H - 8)
|
|
11568
|
+
}, [props.mergeAnchor, rows.length, props.busy])
|
|
11312
11569
|
/* Multi-select gestures (issue #13): the rendered row order is what the
|
|
11313
11570
|
Shift-range walks, and the range is computed over the FILTERED rows —
|
|
11314
11571
|
the same list the eye sees. A plain click only SELECTS (single-replace,
|
|
@@ -11410,7 +11667,12 @@ window.__ModuleLoader__.load({
|
|
|
11410
11667
|
}),
|
|
11411
11668
|
E(FilterSelect, {
|
|
11412
11669
|
label: t('filter.user'), value: author, hint: t('filter.user'),
|
|
11413
|
-
|
|
11670
|
+
/* "Me" first (v0.19.3): resolved from git config and offered even
|
|
11671
|
+
when this branch has no commits of mine — selecting it over an
|
|
11672
|
+
empty list is the answer "I never committed here". */
|
|
11673
|
+
options: [allOption]
|
|
11674
|
+
.concat(me !== null && me.name !== '' ? [{ value: me.name, label: t('history.me') + ' (' + me.name + ')' }] : [])
|
|
11675
|
+
.concat(authors.map((name) => ({ value: name, label: name }))),
|
|
11414
11676
|
onOpenMenu: (event, options) => openFilterMenu(event, options, author, setAuthor),
|
|
11415
11677
|
}),
|
|
11416
11678
|
E(FilterSelect, {
|
|
@@ -11447,12 +11709,22 @@ window.__ModuleLoader__.load({
|
|
|
11447
11709
|
const winEnd = Math.min(rows.length, winStart + Math.ceil(viewport.viewH / HISTORY_ROW_H) + WINDOW_OVERSCAN * 2)
|
|
11448
11710
|
const visibleRows = rows.slice(winStart, winEnd)
|
|
11449
11711
|
|
|
11450
|
-
|
|
11712
|
+
/* Ascending renders oldest-first, so load-more (older commits) lives
|
|
11713
|
+
at the TOP; descending keeps it at the bottom where it always was. */
|
|
11714
|
+
const loadMoreButton = props.hasMore === true
|
|
11715
|
+
? E('button', { ref: loadMoreRef, type: 'button', className: 'dig-load-more', disabled: props.busy === true, onClick: requestMore }, t('history.loadMore'))
|
|
11716
|
+
: null
|
|
11717
|
+
|
|
11718
|
+
return E('div', { className: 'dig-history', ref: historyHostRef }, filterBar,
|
|
11451
11719
|
E('div', {
|
|
11452
11720
|
className: 'dig-history-scroll',
|
|
11453
11721
|
ref: historyScrollRef,
|
|
11454
11722
|
tabIndex: 0,
|
|
11455
11723
|
onKeyDown: onListKeyDown,
|
|
11724
|
+
onPointerDown: historyMarquee.onPointerDown,
|
|
11725
|
+
onClickCapture: (event) => {
|
|
11726
|
+
if (historyMarquee.swallowRef.current === true) { event.preventDefault(); event.stopPropagation() }
|
|
11727
|
+
},
|
|
11456
11728
|
onPointerDownCapture: (event) => {
|
|
11457
11729
|
if (isTypingTarget(event.target) === false) event.currentTarget.focus({ preventScroll: true })
|
|
11458
11730
|
},
|
|
@@ -11471,6 +11743,7 @@ window.__ModuleLoader__.load({
|
|
|
11471
11743
|
onMouseLeave: (event) => event.currentTarget.removeAttribute('data-hover-lane'),
|
|
11472
11744
|
},
|
|
11473
11745
|
rows.length === 0 ? E('div', { className: 'dig-empty' }, dirtyFilter ? t('filter.none') : t('history.empty')) : null,
|
|
11746
|
+
order === 'asc' ? loadMoreButton : null,
|
|
11474
11747
|
winStart > 0 ? E('div', { key: 'pad-top', style: { height: String(winStart * HISTORY_ROW_H) + 'px' } }) : null,
|
|
11475
11748
|
visibleRows.map((row) => E(CommitRow, {
|
|
11476
11749
|
key: row.commit.hash,
|
|
@@ -11480,9 +11753,8 @@ window.__ModuleLoader__.load({
|
|
|
11480
11753
|
onRowClick: handleRowClick, onOpenDetail: props.onSelect, onMenu: props.onMenu, openMenuAt: props.openMenuAt,
|
|
11481
11754
|
})),
|
|
11482
11755
|
winEnd < rows.length ? E('div', { key: 'pad-bottom', style: { height: String((rows.length - winEnd) * HISTORY_ROW_H) + 'px' } }) : null,
|
|
11483
|
-
|
|
11484
|
-
|
|
11485
|
-
: null))
|
|
11756
|
+
order === 'desc' ? loadMoreButton : null),
|
|
11757
|
+
E(MarqueeBox, { box: historyMarquee.box }))
|
|
11486
11758
|
}
|
|
11487
11759
|
|
|
11488
11760
|
function CommitDetail(props) {
|
|
@@ -11956,15 +12228,25 @@ window.__ModuleLoader__.load({
|
|
|
11956
12228
|
return data
|
|
11957
12229
|
}, [base])
|
|
11958
12230
|
|
|
12231
|
+
/* ---- branch browsing (v0.19.3, IDEA-style) ----
|
|
12232
|
+
Double-clicking a tree branch no longer checks it out: it walks THAT
|
|
12233
|
+
branch's log (browseRev = '' keeps the HEAD view). The LAST browsed
|
|
12234
|
+
branch is remembered, so 2-then-1 lands 1's list on the merge commit
|
|
12235
|
+
that carried 2 into 1 ('merge-point') — 1's newer commits stay one
|
|
12236
|
+
wheel-scroll away, nothing is truncated. */
|
|
12237
|
+
const [browseRev, setBrowseRev] = useState('')
|
|
12238
|
+
const [mergeAnchor, setMergeAnchor] = useState(null)
|
|
12239
|
+
const lastBrowseRef = useRef(null)
|
|
11959
12240
|
const loadCommits = useCallback(async (skip, overridePath) => {
|
|
11960
12241
|
const activePath = overridePath === undefined ? pathFilter : overridePath
|
|
11961
12242
|
const payload = Object.assign({}, base, { skip: skip === undefined ? 0 : skip, limit: 120 })
|
|
12243
|
+
if (browseRev !== '') payload.rev = browseRev
|
|
11962
12244
|
if (activePath !== undefined && activePath !== '') payload.path = activePath
|
|
11963
12245
|
const data = await request('log', payload)
|
|
11964
12246
|
setCommits((previous) => (skip === undefined || skip === 0 ? data.commits : previous.concat(data.commits)))
|
|
11965
12247
|
setHasMore(data.hasMore)
|
|
11966
12248
|
return data
|
|
11967
|
-
}, [base, pathFilter])
|
|
12249
|
+
}, [base, pathFilter, browseRev])
|
|
11968
12250
|
|
|
11969
12251
|
/* `fetchRemote` turns the refresh into what the rail button means: "see
|
|
11970
12252
|
whether the remote has anything new to pull" — a `git fetch --prune`
|
|
@@ -12007,13 +12289,40 @@ window.__ModuleLoader__.load({
|
|
|
12007
12289
|
(a paged fetch never reaches this path), replaced on every
|
|
12008
12290
|
refresh — the auto-refresh included — so a session switch through
|
|
12009
12291
|
the same workspace seeds fresh data, not the mount-time
|
|
12010
|
-
snapshot.
|
|
12011
|
-
|
|
12292
|
+
snapshot. A branch-browsing page (browseRev) must not evict the
|
|
12293
|
+
HEAD page: the next mount opens on HEAD and would otherwise be
|
|
12294
|
+
seeded with another branch's log. */
|
|
12295
|
+
if (browseRev === '') writePaintPage(repoRoot, { summary: summaryData, branches: branchesData, commits: page.commits, hasMore: page.hasMore })
|
|
12012
12296
|
})
|
|
12013
12297
|
} finally {
|
|
12014
12298
|
if (withFetch === true) setRefreshSpinning(false)
|
|
12015
12299
|
}
|
|
12016
|
-
}, [guard, loadSummary, loadBranches, loadCommits, repoRoot, remoteReady, base])
|
|
12300
|
+
}, [guard, loadSummary, loadBranches, loadCommits, repoRoot, remoteReady, base, browseRev])
|
|
12301
|
+
|
|
12302
|
+
/* Double-click a branch (or the HEAD row): browse its history. The merge
|
|
12303
|
+
anchor comes from the PREVIOUSLY browsed branch — 'merge-point' finds
|
|
12304
|
+
the newest merge that carried it into the one now opened, and the
|
|
12305
|
+
history list scrolls there once (null = no relationship, top). */
|
|
12306
|
+
const browseBranch = useCallback(async (entry) => {
|
|
12307
|
+
const name = String(entry.name)
|
|
12308
|
+
const previous = lastBrowseRef.current
|
|
12309
|
+
if (previous === name) return
|
|
12310
|
+
lastBrowseRef.current = name
|
|
12311
|
+
setMergeAnchor(null)
|
|
12312
|
+
setBrowseRev(name)
|
|
12313
|
+
if (previous !== null) {
|
|
12314
|
+
try {
|
|
12315
|
+
const data = await request('merge-point', Object.assign({}, base, { from: previous, to: name }))
|
|
12316
|
+
if (data !== null && typeof data === 'object' && typeof data.hash === 'string' && data.hash !== '') setMergeAnchor(data.hash)
|
|
12317
|
+
} catch (error) { void error }
|
|
12318
|
+
}
|
|
12319
|
+
}, [base, request])
|
|
12320
|
+
/* A repository switch forgets the browsing context entirely. */
|
|
12321
|
+
useEffect(() => {
|
|
12322
|
+
lastBrowseRef.current = null
|
|
12323
|
+
setBrowseRev('')
|
|
12324
|
+
setMergeAnchor(null)
|
|
12325
|
+
}, [repoRoot])
|
|
12017
12326
|
|
|
12018
12327
|
useEffect(() => {
|
|
12019
12328
|
if (repoRoot === null) return undefined
|
|
@@ -12963,6 +13272,9 @@ window.__ModuleLoader__.load({
|
|
|
12963
13272
|
onPicksChange: setPickedCommits,
|
|
12964
13273
|
onSelectAll: (hashes) => setPickedCommits(new Set(hashes)),
|
|
12965
13274
|
onKeyDown: onHistoryKeyDown,
|
|
13275
|
+
mergeAnchor: mergeAnchor,
|
|
13276
|
+
base: base,
|
|
13277
|
+
request: request,
|
|
12966
13278
|
})
|
|
12967
13279
|
|
|
12968
13280
|
const diffPane = patch === '' && patchLoading === false ? null : E('div', { className: 'dig-diff-pane', 'data-pane': 'diff', style: paneStyle('diff'), onContextMenu: onDiffPaneContextMenu },
|
|
@@ -13512,19 +13824,19 @@ window.__ModuleLoader__.load({
|
|
|
13512
13824
|
title: t('toolbar.tree'),
|
|
13513
13825
|
onClick: () => foldTree(!treeOpen),
|
|
13514
13826
|
}, E(Icon, { name: treeOpen ? 'eye' : 'eyeOff' }))),
|
|
13515
|
-
treeOpen ? E('div', { className: 'dig-compact-tree', 'data-pane': 'tree', style: paneStyle('tree') }, E(BranchTree, { t: t, branches: branches, favorites: favorites, onCheckout: checkout, onBranchMenu: branchMenu, onDeleteEntries: deleteBranchEntries, onCopyValues: copyValues, onPicksChange: setBranchPicks })) : null,
|
|
13827
|
+
treeOpen ? E('div', { className: 'dig-compact-tree', 'data-pane': 'tree', style: paneStyle('tree') }, E(BranchTree, { t: t, branches: branches, favorites: favorites, onCheckout: checkout, onBrowse: browseBranch, onBranchMenu: branchMenu, onDeleteEntries: deleteBranchEntries, onCopyValues: copyValues, onPicksChange: setBranchPicks })) : null,
|
|
13516
13828
|
treeOpen ? gutter('tree', 'prev') : null,
|
|
13517
13829
|
view === 'changes' ? changesPane : E('div', { className: 'dig-pane dig-pane-main' }, historyPane, diffGutter, diffPane))
|
|
13518
13830
|
} else if (columns) {
|
|
13519
13831
|
body = E('div', { className: 'dig-body dig-body-columns', ref: bodyRef },
|
|
13520
|
-
treeOpen ? E('div', { className: 'dig-pane dig-pane-tree', 'data-pane': 'tree', style: paneStyle('tree') }, E(BranchTree, { t: t, branches: branches, favorites: favorites, onCheckout: checkout, onBranchMenu: branchMenu, onDeleteEntries: deleteBranchEntries, onCopyValues: copyValues, onPicksChange: setBranchPicks })) : null,
|
|
13832
|
+
treeOpen ? E('div', { className: 'dig-pane dig-pane-tree', 'data-pane': 'tree', style: paneStyle('tree') }, E(BranchTree, { t: t, branches: branches, favorites: favorites, onCheckout: checkout, onBrowse: browseBranch, onBranchMenu: branchMenu, onDeleteEntries: deleteBranchEntries, onCopyValues: copyValues, onPicksChange: setBranchPicks })) : null,
|
|
13521
13833
|
treeOpen ? gutter('tree', 'prev') : null,
|
|
13522
13834
|
E('div', { className: 'dig-pane dig-pane-main' }, historyPane, diffGutter, diffPane),
|
|
13523
13835
|
gutter('changes', 'next'),
|
|
13524
13836
|
E('div', { className: 'dig-pane dig-pane-changes', 'data-pane': 'changes', style: paneStyle('changes') }, changesPane))
|
|
13525
13837
|
} else {
|
|
13526
13838
|
body = E('div', { className: 'dig-body dig-body-stack', ref: bodyRef },
|
|
13527
|
-
treeOpen ? E('div', { className: 'dig-pane dig-pane-tree-stack', 'data-pane': 'tree', style: paneStyle('tree') }, E(BranchTree, { t: t, branches: branches, favorites: favorites, onCheckout: checkout, onBranchMenu: branchMenu, onDeleteEntries: deleteBranchEntries, onCopyValues: copyValues, onPicksChange: setBranchPicks })) : null,
|
|
13839
|
+
treeOpen ? E('div', { className: 'dig-pane dig-pane-tree-stack', 'data-pane': 'tree', style: paneStyle('tree') }, E(BranchTree, { t: t, branches: branches, favorites: favorites, onCheckout: checkout, onBrowse: browseBranch, onBranchMenu: branchMenu, onDeleteEntries: deleteBranchEntries, onCopyValues: copyValues, onPicksChange: setBranchPicks })) : null,
|
|
13528
13840
|
treeOpen ? gutter('tree', 'prev') : null,
|
|
13529
13841
|
E('div', { className: 'dig-pane dig-pane-changes-stack', 'data-pane': 'changes', style: paneStyle('changes') }, changesPane),
|
|
13530
13842
|
gutter('changes', 'prev'),
|
|
@@ -14049,7 +14361,10 @@ window.__ModuleLoader__.load({
|
|
|
14049
14361
|
'.dig-picker-name{flex:none;font-weight:600}',
|
|
14050
14362
|
'.dig-picker-kind{color:var(--dsw-alias-label-tertiary);flex:none}',
|
|
14051
14363
|
'.dig-picker-path{color:var(--dsw-alias-label-tertiary);margin-left:auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:50%}',
|
|
14052
|
-
|
|
14364
|
+
/* position:relative: the marquee box paints against this wrapper (the
|
|
14365
|
+
scroll container is offset by the search field — same rule as
|
|
14366
|
+
.dig-history above). */
|
|
14367
|
+
'.dig-tree{display:flex;flex-direction:column;min-height:0;flex:1;position:relative}',
|
|
14053
14368
|
'.dig-tree-scroll{flex:1;overflow:auto;padding-bottom:6px}',
|
|
14054
14369
|
'.dig-section{display:flex;flex-direction:column}',
|
|
14055
14370
|
'.dig-section-head{display:flex;align-items:center;gap:4px;width:100%;padding:3px 8px;border:none;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-weight:600;cursor:pointer;text-align:left}',
|
|
@@ -14072,6 +14387,10 @@ window.__ModuleLoader__.load({
|
|
|
14072
14387
|
'.dig-chevron-open{transform:rotate(0deg)}',
|
|
14073
14388
|
'.dig-row{display:flex;align-items:center;gap:5px;padding:2px 8px;cursor:pointer;white-space:nowrap;overflow:hidden}',
|
|
14074
14389
|
'.dig-row:hover{background:var(--dsw-alias-interactive-bg-hover)}',
|
|
14390
|
+
/* v0.19.3: the HEAD section row behaves like a branch row (click
|
|
14391
|
+
selects, double-click browses, right-click menus) — same hover cue. */
|
|
14392
|
+
'.dig-head-branch{cursor:pointer;transition:background .12s}',
|
|
14393
|
+
'.dig-head-branch:hover{background:var(--dsw-alias-interactive-bg-hover)}',
|
|
14075
14394
|
'.dig-row-head{color:var(--dsw-alias-state-warn-primary,var(--dsw-alias-brand-primary))}',
|
|
14076
14395
|
'.dig-row-selected{background:var(--dsw-alias-interactive-bg-active,var(--dsw-alias-interactive-bg-hover));color:var(--dsw-alias-label-primary);box-shadow:inset 2px 0 0 var(--dsw-alias-brand-primary)}',
|
|
14077
14396
|
'.dig-row-remote .dig-row-label{color:var(--dsw-alias-label-secondary);font-weight:500}',
|
|
@@ -14090,7 +14409,10 @@ window.__ModuleLoader__.load({
|
|
|
14090
14409
|
'.dig-badge.dig-badge-ahead,.dig-badge.dig-badge-behind{padding:0 6px}',
|
|
14091
14410
|
'.dig-badge-muted{opacity:.8}',
|
|
14092
14411
|
'.dig-badge-worktree{padding:0 3px;display:inline-flex;align-items:center;height:14px}',
|
|
14093
|
-
|
|
14412
|
+
/* position:relative: the marquee box (rubber-band selection) paints
|
|
14413
|
+
against this wrapper — the scroll container below is offset by the
|
|
14414
|
+
filter bar, and painting from the wrong rect was the v0.19.2 drift. */
|
|
14415
|
+
'.dig-history{display:flex;flex-direction:column;min-height:0;flex:1;position:relative}',
|
|
14094
14416
|
'.dig-history-scroll{flex:1;overflow:auto}',
|
|
14095
14417
|
'.dig-commit{display:flex;align-items:center;gap:6px;padding:1px 8px 1px 0;cursor:pointer;height:26px;overflow:hidden}',
|
|
14096
14418
|
'.dig-commit:hover{background:var(--dsw-alias-interactive-bg-hover)}',
|
package/src/index.js
CHANGED
|
@@ -778,6 +778,40 @@ async function log(payload) {
|
|
|
778
778
|
const hasMore = all.length > limit
|
|
779
779
|
return { repoRoot: root, commits: hasMore ? all.slice(0, limit) : all, hasMore, skip, limit }
|
|
780
780
|
}
|
|
781
|
+
/* The merge that carried `from` INTO `to`, newest first — the anchor the
|
|
782
|
+
branch tree's double-click-to-browse lands on (IDEA-style: double-click
|
|
783
|
+
branch 2, then branch 1, and 1's list opens scrolled to the merge commit
|
|
784
|
+
where 2 was merged in, so "2 has been merged here" is visible at a
|
|
785
|
+
glance even though 1 kept moving. No merge relationship -> null, and the
|
|
786
|
+
caller just opens the list at the top. Pure rev arithmetic, no network. */
|
|
787
|
+
/* Who am I here: git config resolves user.name/email through its usual
|
|
788
|
+
cascade (repo-local overrides global), so the answer is per repository.
|
|
789
|
+
The client caches it per repo root for the whole panel lifetime — a git
|
|
790
|
+
identity rarely changes mid-session — and pins it to the top of the
|
|
791
|
+
author filter (v0.19.3), even when this branch has no commits by me:
|
|
792
|
+
picking it and seeing an empty list IS the answer ("I never committed
|
|
793
|
+
here"). Empty strings mean no identity is configured. */
|
|
794
|
+
async function me(payload) {
|
|
795
|
+
const cwd = cwdOf(payload)
|
|
796
|
+
const root = await repoRootOf(cwd, payload)
|
|
797
|
+
const name = await runGit(root, ['config', 'user.name'])
|
|
798
|
+
const email = await runGit(root, ['config', 'user.email'])
|
|
799
|
+
return {
|
|
800
|
+
repoRoot: root,
|
|
801
|
+
name: name.code === 0 ? name.stdout.trim() : '',
|
|
802
|
+
email: email.code === 0 ? email.stdout.trim() : '',
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
async function mergePoint(payload) {
|
|
807
|
+
const cwd = cwdOf(payload)
|
|
808
|
+
const root = await repoRootOf(cwd, payload)
|
|
809
|
+
const from = requireRef(payload.from, 'from')
|
|
810
|
+
const to = requireRef(payload.to, 'to')
|
|
811
|
+
const out = await git(root, ['rev-list', '--merges', '--ancestry-path', '--max-count=1', from + '..' + to])
|
|
812
|
+
const hash = out.trim().split('\n')[0] || ''
|
|
813
|
+
return { repoRoot: root, hash: hash === '' ? null : hash }
|
|
814
|
+
}
|
|
781
815
|
|
|
782
816
|
async function commitDetail(payload) {
|
|
783
817
|
const cwd = cwdOf(payload)
|
|
@@ -1939,6 +1973,8 @@ const METHODS = {
|
|
|
1939
1973
|
stats,
|
|
1940
1974
|
branches,
|
|
1941
1975
|
log,
|
|
1976
|
+
me,
|
|
1977
|
+
'merge-point': mergePoint,
|
|
1942
1978
|
commitDetail,
|
|
1943
1979
|
diff,
|
|
1944
1980
|
compare,
|