dsh-ide-git 0.19.2 → 0.19.4
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 +542 -150
- package/src/index.js +36 -0
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 })),
|
|
@@ -10324,6 +10375,24 @@ window.__ModuleLoader__.load({
|
|
|
10324
10375
|
selKeys (`local:name` / `remote:name` / `tag:name` — the prefix keeps a
|
|
10325
10376
|
branch and a tag that share a name from colliding). */
|
|
10326
10377
|
const [picks, setPicks] = useState(() => new Set())
|
|
10378
|
+
/* v0.19.4: the Panel bumps clearSignal when ANOTHER list gains the
|
|
10379
|
+
selection (or a blank click clears everywhere) — drop ours. Monotonic
|
|
10380
|
+
epochs start at 0, so the mount pass never clears. */
|
|
10381
|
+
const treeClearSignal = props.clearSignal === undefined ? 0 : props.clearSignal
|
|
10382
|
+
const seenTreeClear = useRef(treeClearSignal)
|
|
10383
|
+
/* v0.19.4 echo guard: only USER gestures may flow up to the Panel — the
|
|
10384
|
+
mirror effect below reports picks exactly once per gesture. Reactive
|
|
10385
|
+
setPicks calls (the clear-signal effect above, the survival filter)
|
|
10386
|
+
must NOT re-enter the forwarder, or the epoch it bumps clears the
|
|
10387
|
+
selection the OTHER list just made (the T1b echo bug). */
|
|
10388
|
+
const reportGestureRef = useRef(false)
|
|
10389
|
+
useEffect(() => {
|
|
10390
|
+
if (seenTreeClear.current !== treeClearSignal) {
|
|
10391
|
+
seenTreeClear.current = treeClearSignal
|
|
10392
|
+
reportGestureRef.current = false
|
|
10393
|
+
setPicks(new Set())
|
|
10394
|
+
}
|
|
10395
|
+
}, [treeClearSignal])
|
|
10327
10396
|
const pickAnchor = useRef(null)
|
|
10328
10397
|
const visibleRef = useRef([])
|
|
10329
10398
|
const [foldedFolders, setFoldedFolders] = useState(() => {
|
|
@@ -10352,8 +10421,13 @@ window.__ModuleLoader__.load({
|
|
|
10352
10421
|
// favorites section and the local tree show the starred rows first.
|
|
10353
10422
|
const locals = (branches === null ? [] : branches.local).filter((entry) => match(entry.name))
|
|
10354
10423
|
.sort((a, b) => (favorites.indexOf(a.name) >= 0 ? 0 : 1) - (favorites.indexOf(b.name) >= 0 ? 0 : 1))
|
|
10424
|
+
/* The HEAD row acts like a branch row since v0.19.3: click selects,
|
|
10425
|
+
double-click browses the current branch's history, right-click opens
|
|
10426
|
+
the same row menu (quick jump back to HEAD lives there). */
|
|
10427
|
+
const headEntry = locals.find((entry) => entry.head === true) || null
|
|
10355
10428
|
/* A refresh that makes a picked ref disappear must not keep it selected. */
|
|
10356
10429
|
useEffect(() => {
|
|
10430
|
+
reportGestureRef.current = false
|
|
10357
10431
|
setPicks((previous) => {
|
|
10358
10432
|
if (previous.size === 0) return previous
|
|
10359
10433
|
const alive = new Set()
|
|
@@ -10371,6 +10445,8 @@ window.__ModuleLoader__.load({
|
|
|
10371
10445
|
render-time call would trip the hooks rules): the rail reads them so
|
|
10372
10446
|
its buttons can act on the selection instead of only on HEAD. */
|
|
10373
10447
|
useEffect(() => {
|
|
10448
|
+
if (reportGestureRef.current !== true) return
|
|
10449
|
+
reportGestureRef.current = false
|
|
10374
10450
|
if (props.onPicksChange !== undefined) props.onPicksChange(picks)
|
|
10375
10451
|
}, [picks])
|
|
10376
10452
|
/**
|
|
@@ -10405,6 +10481,7 @@ window.__ModuleLoader__.load({
|
|
|
10405
10481
|
return out
|
|
10406
10482
|
}
|
|
10407
10483
|
const handlePick = useCallback((event, entry, selKey) => {
|
|
10484
|
+
reportGestureRef.current = true
|
|
10408
10485
|
const ordered = visibleRef.current.map((leaf) => branchSelKeyOf(leaf.entry))
|
|
10409
10486
|
if (event.shiftKey === true) {
|
|
10410
10487
|
/* No anchor yet (fresh tree, nothing clicked): VS Code semantics —
|
|
@@ -10439,7 +10516,7 @@ window.__ModuleLoader__.load({
|
|
|
10439
10516
|
selected: picks.has(branchSelKeyOf(entry)) === true,
|
|
10440
10517
|
starred: entry.tag !== true && entry.remote !== true && favorites.indexOf(entry.name) >= 0,
|
|
10441
10518
|
onPick: handlePick,
|
|
10442
|
-
|
|
10519
|
+
onBrowse: props.onBrowse, onMenu: onRowMenu,
|
|
10443
10520
|
})
|
|
10444
10521
|
const rowsOf = (node, prefix, depth) => {
|
|
10445
10522
|
const out = []
|
|
@@ -10504,6 +10581,30 @@ window.__ModuleLoader__.load({
|
|
|
10504
10581
|
}
|
|
10505
10582
|
props.onBranchMenu(event, entry, null)
|
|
10506
10583
|
}
|
|
10584
|
+
/* v0.19.3: the tree rubber-bands like the other two lists (shared hook).
|
|
10585
|
+
Tree rows have no fixed height and render unfolded-only, so the row
|
|
10586
|
+
rects are a LIVE DOM measure over the [data-mkey] rows BranchRow
|
|
10587
|
+
carries; section/folder headers are buttons and never start a drag.
|
|
10588
|
+
Favorited branches render twice (favorites + local sections) — both
|
|
10589
|
+
map to one selKey, so a Set dedupes them for free. */
|
|
10590
|
+
const treeHostRef = useRef(null)
|
|
10591
|
+
const treeScrollRef = useRef(null)
|
|
10592
|
+
const treeMarquee = useRubberBand({
|
|
10593
|
+
scrollRef: treeScrollRef,
|
|
10594
|
+
drawRef: treeHostRef,
|
|
10595
|
+
rectsOf: () => {
|
|
10596
|
+
const host = treeScrollRef.current
|
|
10597
|
+
if (host === null) return []
|
|
10598
|
+
const hostRect = host.getBoundingClientRect()
|
|
10599
|
+
const rects = []
|
|
10600
|
+
host.querySelectorAll('[data-mkey]').forEach((row) => {
|
|
10601
|
+
const rect = row.getBoundingClientRect()
|
|
10602
|
+
rects.push({ key: row.getAttribute('data-mkey'), top: rect.top - hostRect.top + host.scrollTop, bottom: rect.bottom - hostRect.top + host.scrollTop })
|
|
10603
|
+
})
|
|
10604
|
+
return rects
|
|
10605
|
+
},
|
|
10606
|
+
onPick: (keys) => { reportGestureRef.current = true; setPicks(new Set(keys)) },
|
|
10607
|
+
})
|
|
10507
10608
|
/* List-scope keyboard: Ctrl/Cmd+C copies the picked names, Delete asks
|
|
10508
10609
|
Panel for a grouped delete (head branches are skipped — git refuses
|
|
10509
10610
|
them anyway), Ctrl+A selects every visible row, Esc clears. */
|
|
@@ -10533,6 +10634,7 @@ window.__ModuleLoader__.load({
|
|
|
10533
10634
|
}
|
|
10534
10635
|
return E('div', {
|
|
10535
10636
|
className: 'dig-tree',
|
|
10637
|
+
ref: treeHostRef,
|
|
10536
10638
|
tabIndex: 0,
|
|
10537
10639
|
onKeyDown: onTreeKeyDown,
|
|
10538
10640
|
onPointerDownCapture: (event) => {
|
|
@@ -10545,12 +10647,35 @@ window.__ModuleLoader__.load({
|
|
|
10545
10647
|
placeholder: t('branches.filter'),
|
|
10546
10648
|
onChange: (event) => setFilter(event.target.value),
|
|
10547
10649
|
})),
|
|
10548
|
-
E('div', {
|
|
10549
|
-
|
|
10650
|
+
E('div', {
|
|
10651
|
+
className: 'dig-tree-scroll',
|
|
10652
|
+
ref: treeScrollRef,
|
|
10653
|
+
onPointerDown: treeMarquee.onPointerDown,
|
|
10654
|
+
onClickCapture: (event) => {
|
|
10655
|
+
if (treeMarquee.swallowRef.current === true) { event.preventDefault(); event.stopPropagation(); return }
|
|
10656
|
+
/* v0.19.4: a blank click (not on a branch row or a control) clears
|
|
10657
|
+
the picks here AND everywhere else — "selection follows the last
|
|
10658
|
+
gesture". The Set goes up through the same forwarder a row click
|
|
10659
|
+
uses, so the other two lists reset as well. */
|
|
10660
|
+
const target = event.target
|
|
10661
|
+
if (target instanceof Element && target.closest('[data-mkey],button,input,textarea,select,a') === null) {
|
|
10662
|
+
reportGestureRef.current = true
|
|
10663
|
+
setPicks(new Set())
|
|
10664
|
+
}
|
|
10665
|
+
},
|
|
10666
|
+
},
|
|
10667
|
+
headEntry === null ? E('div', { className: 'dig-section-head dig-section-head-static' }, t('branches.head')) : E('div', {
|
|
10668
|
+
className: 'dig-section-head dig-section-head-static dig-head-branch',
|
|
10669
|
+
title: headEntry.name + ' · ' + t('branches.headHint'),
|
|
10670
|
+
onClick: (event) => { event.stopPropagation(); handlePick(event, headEntry, branchSelKeyOf(headEntry)) },
|
|
10671
|
+
onDoubleClick: () => { if (props.onBrowse !== undefined) props.onBrowse(headEntry) },
|
|
10672
|
+
onContextMenu: (event) => { event.preventDefault(); onRowMenu(event, headEntry) },
|
|
10673
|
+
}, t('branches.head')),
|
|
10550
10674
|
favorites.length === 0 ? null : section('favorites', t('branches.favorites'), locals.filter((entry) => favorites.indexOf(entry.name) >= 0), (entry) => entry),
|
|
10551
10675
|
section('local', t('branches.local'), locals, (entry) => entry, true),
|
|
10552
10676
|
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 })))
|
|
10677
|
+
section('tags', t('branches.tags'), tags, (entry) => Object.assign({}, entry, { tag: true }))),
|
|
10678
|
+
E(MarqueeBox, { box: treeMarquee.box }))
|
|
10554
10679
|
}
|
|
10555
10680
|
|
|
10556
10681
|
/* ============================== changes ============================== */
|
|
@@ -10645,10 +10770,11 @@ window.__ModuleLoader__.load({
|
|
|
10645
10770
|
const anchorRef = props.anchorRef
|
|
10646
10771
|
const changeItemsRef = useRef(null)
|
|
10647
10772
|
const changeOffsetsRef = useRef(null)
|
|
10648
|
-
|
|
10649
|
-
|
|
10650
|
-
|
|
10651
|
-
|
|
10773
|
+
/* The marquee lives in the shared useRubberBand hook (v0.19.3: history
|
|
10774
|
+
and the branch tree drag the same way). changesHostRef is the DRAW
|
|
10775
|
+
host — the hook comment explains why the paint rect must be the list
|
|
10776
|
+
wrapper's, not the scroll container's (v0.19.2 offset report). */
|
|
10777
|
+
const changesHostRef = useRef(null)
|
|
10652
10778
|
/* Rows in their RENDER order (the order windowing flattens them into) —
|
|
10653
10779
|
Shift-click spans follow what the eye sees, not the git order. */
|
|
10654
10780
|
const orderedRowKeys = () => {
|
|
@@ -10700,96 +10826,10 @@ window.__ModuleLoader__.load({
|
|
|
10700
10826
|
not yank the pane away (v0.18.3). */
|
|
10701
10827
|
setSelection((previous) => previous.size === 1 && previous.has(key) === true ? previous : new Set([key]))
|
|
10702
10828
|
}, [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
|
-
}
|
|
10829
|
+
/* Rubber-band selection now comes from the shared useRubberBand hook
|
|
10830
|
+
(see its comment): the fixed row heights + the prefix-sum offsets
|
|
10831
|
+
still turn a pixel span back into row keys without any DOM rows —
|
|
10832
|
+
the rectsOf below is the exact former marqueeKeysAt, reshaped. */
|
|
10793
10833
|
const changes = summary === null ? null : summary.changes
|
|
10794
10834
|
const conflicted = changes === null ? [] : changes.conflicted
|
|
10795
10835
|
const staged = changes === null ? [] : changes.staged
|
|
@@ -10859,6 +10899,22 @@ window.__ModuleLoader__.load({
|
|
|
10859
10899
|
mounted. A tree with thousands of files scrolls at frame rate. */
|
|
10860
10900
|
const changesScrollRef = useRef(null)
|
|
10861
10901
|
const changesViewport = useViewportBox(changesScrollRef)
|
|
10902
|
+
const changeMarquee = useRubberBand({
|
|
10903
|
+
scrollRef: changesScrollRef,
|
|
10904
|
+
drawRef: changesHostRef,
|
|
10905
|
+
rectsOf: () => {
|
|
10906
|
+
const items = changeItemsRef.current
|
|
10907
|
+
const offsets = changeOffsetsRef.current
|
|
10908
|
+
if (items === null || offsets === null || items.length === 0) return []
|
|
10909
|
+
const rects = []
|
|
10910
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
10911
|
+
const entry = items[index]
|
|
10912
|
+
if (entry.kind === 'row' && entry.key.indexOf('ignored:') !== 0) rects.push({ key: entry.key, top: offsets[index], bottom: offsets[index + 1] })
|
|
10913
|
+
}
|
|
10914
|
+
return rects
|
|
10915
|
+
},
|
|
10916
|
+
onPick: (keys) => setSelection(new Set(keys)),
|
|
10917
|
+
})
|
|
10862
10918
|
const changeItems = useMemo(() => {
|
|
10863
10919
|
const out = []
|
|
10864
10920
|
const groupHead = (key, title, entries) => E('div', { className: 'dig-group-head' },
|
|
@@ -11042,7 +11098,7 @@ window.__ModuleLoader__.load({
|
|
|
11042
11098
|
props.onBulkDiscard === undefined ? null : E('button', { type: 'button', className: 'dig-link dig-link-danger', onClick: () => props.onBulkDiscard() }, t('action.discard')),
|
|
11043
11099
|
props.onCopySelection === undefined ? null : E('button', { type: 'button', className: 'dig-link', onClick: () => props.onCopySelection() }, t('selection.copyPaths')),
|
|
11044
11100
|
E('button', { type: 'button', className: 'dig-link', onClick: () => setSelection(new Set()) }, t('selection.clear')))
|
|
11045
|
-
return E('div', { className: 'dig-changes' },
|
|
11101
|
+
return E('div', { className: 'dig-changes', ref: changesHostRef },
|
|
11046
11102
|
aiConfirmDialog,
|
|
11047
11103
|
head,
|
|
11048
11104
|
E('div', { className: 'dig-changes-body' },
|
|
@@ -11055,9 +11111,13 @@ window.__ModuleLoader__.load({
|
|
|
11055
11111
|
onPointerDownCapture: (event) => {
|
|
11056
11112
|
if (isTypingTarget(event.target) === false) event.currentTarget.focus({ preventScroll: true })
|
|
11057
11113
|
},
|
|
11058
|
-
onPointerDown:
|
|
11114
|
+
onPointerDown: changeMarquee.onPointerDown,
|
|
11059
11115
|
onClickCapture: (event) => {
|
|
11060
|
-
if (
|
|
11116
|
+
if (changeMarquee.swallowRef.current === true) { event.preventDefault(); event.stopPropagation(); return }
|
|
11117
|
+
/* v0.19.4: blank click clears — the wrapped setter also bumps the
|
|
11118
|
+
other lists' epochs, so this is a clear-everywhere gesture. */
|
|
11119
|
+
const target = event.target
|
|
11120
|
+
if (target instanceof Element && target.closest('.dig-row,.dig-group-head,button,input,textarea,select,a') === null) setSelection(new Set())
|
|
11061
11121
|
},
|
|
11062
11122
|
},
|
|
11063
11123
|
collapsed ? null : [
|
|
@@ -11074,10 +11134,7 @@ window.__ModuleLoader__.load({
|
|
|
11074
11134
|
and nothing on the list (group head, rows) is ever covered. */
|
|
11075
11135
|
selectionBar,
|
|
11076
11136
|
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
|
-
}))
|
|
11137
|
+
E(MarqueeBox, { box: changeMarquee.box }))
|
|
11081
11138
|
}
|
|
11082
11139
|
|
|
11083
11140
|
/* ============================== history ============================== */
|
|
@@ -11143,6 +11200,122 @@ window.__ModuleLoader__.load({
|
|
|
11143
11200
|
return { scrollTop: scrollTop, viewH: viewH }
|
|
11144
11201
|
}
|
|
11145
11202
|
|
|
11203
|
+
/* ---- rubber-band (marquee) selection, shared by all three lists ----
|
|
11204
|
+
One hook, three wiring sites (changes, history, branch tree). The
|
|
11205
|
+
contract: `scrollRef` is the scrolling container the SELECTION math
|
|
11206
|
+
runs against, `drawRef` is the element the marquee box is PAINTED in
|
|
11207
|
+
(its own rect — v0.18.x painted from the scroll container's rect while
|
|
11208
|
+
the box lived inside the list wrapper, so the toolbar height above the
|
|
11209
|
+
list shifted every box visually upward; reported v0.19.2), `rectsOf`
|
|
11210
|
+
returns {key,top,bottom} rows in scroll-content coordinates (exact
|
|
11211
|
+
prefix-sums for the windowed lists, a live DOM measure for the
|
|
11212
|
+
variable-height tree), and `onPick` receives the intersecting keys.
|
|
11213
|
+
A 4px threshold keeps plain clicks clicky; pointerdown is preventDefault-ed
|
|
11214
|
+
so drags never start a native text selection; edge auto-scroll and the
|
|
11215
|
+
rAF-throttled sync carry over unchanged from v0.18.x. */
|
|
11216
|
+
function useRubberBand(options) {
|
|
11217
|
+
const [box, setBox] = useState(null)
|
|
11218
|
+
const stateRef = useRef(null)
|
|
11219
|
+
const frameRef = useRef(0)
|
|
11220
|
+
const swallowRef = useRef(false)
|
|
11221
|
+
const optionsRef = useRef(options)
|
|
11222
|
+
optionsRef.current = options
|
|
11223
|
+
const sync = () => {
|
|
11224
|
+
const opts = optionsRef.current
|
|
11225
|
+
const state = stateRef.current
|
|
11226
|
+
const host = opts.scrollRef.current
|
|
11227
|
+
const draw = opts.drawRef.current
|
|
11228
|
+
if (state === null || host === null || draw === null) return
|
|
11229
|
+
const hostRect = host.getBoundingClientRect()
|
|
11230
|
+
const drawRect = draw.getBoundingClientRect()
|
|
11231
|
+
const y0 = state.startY - hostRect.top + state.scrollY0
|
|
11232
|
+
const y1 = state.clientY - hostRect.top + host.scrollTop
|
|
11233
|
+
const low = Math.min(y0, y1)
|
|
11234
|
+
const high = Math.max(y0, y1)
|
|
11235
|
+
const keys = []
|
|
11236
|
+
for (const rect of opts.rectsOf()) {
|
|
11237
|
+
if (rect.top < high && rect.bottom > low) keys.push(rect.key)
|
|
11238
|
+
}
|
|
11239
|
+
opts.onPick(keys)
|
|
11240
|
+
setBox({
|
|
11241
|
+
x: Math.min(state.startX, state.clientX) - drawRect.left,
|
|
11242
|
+
y: Math.min(state.startY, state.clientY) - drawRect.top,
|
|
11243
|
+
w: Math.abs(state.clientX - state.startX),
|
|
11244
|
+
h: Math.abs(state.clientY - state.startY),
|
|
11245
|
+
})
|
|
11246
|
+
/* Auto-scroll when the pointer rides an edge; keep the loop alive while
|
|
11247
|
+
it does so the selection follows the content streaming past. */
|
|
11248
|
+
const EDGE = 28
|
|
11249
|
+
let speed = 0
|
|
11250
|
+
if (state.clientY - hostRect.top < EDGE) speed = -Math.ceil((EDGE - (state.clientY - hostRect.top)) / 3)
|
|
11251
|
+
else if (hostRect.bottom - state.clientY < EDGE) speed = Math.ceil((EDGE - (hostRect.bottom - state.clientY)) / 3)
|
|
11252
|
+
if (speed !== 0) {
|
|
11253
|
+
host.scrollTop += speed * 2
|
|
11254
|
+
cancelAnimationFrame(frameRef.current)
|
|
11255
|
+
frameRef.current = requestAnimationFrame(sync)
|
|
11256
|
+
}
|
|
11257
|
+
}
|
|
11258
|
+
const schedule = () => {
|
|
11259
|
+
cancelAnimationFrame(frameRef.current)
|
|
11260
|
+
frameRef.current = requestAnimationFrame(sync)
|
|
11261
|
+
}
|
|
11262
|
+
useEffect(() => () => cancelAnimationFrame(frameRef.current), [])
|
|
11263
|
+
const onPointerDown = (event) => {
|
|
11264
|
+
if (event.button !== 0) return
|
|
11265
|
+
const opts = optionsRef.current
|
|
11266
|
+
const host = opts.scrollRef.current
|
|
11267
|
+
if (host === null) return
|
|
11268
|
+
if (event.target instanceof Element && event.target.closest('button, input, textarea, select, a') !== null) return
|
|
11269
|
+
// Keep the browser from starting a text selection under the drag (the
|
|
11270
|
+
// history/tree lists dragged out blue text before); the real clicks
|
|
11271
|
+
// still fire — only the selection machinery is suppressed.
|
|
11272
|
+
event.preventDefault()
|
|
11273
|
+
stateRef.current = {
|
|
11274
|
+
startX: event.clientX,
|
|
11275
|
+
startY: event.clientY,
|
|
11276
|
+
scrollY0: host.scrollTop,
|
|
11277
|
+
clientX: event.clientX,
|
|
11278
|
+
clientY: event.clientY,
|
|
11279
|
+
active: false,
|
|
11280
|
+
}
|
|
11281
|
+
const onMove = (moveEvent) => {
|
|
11282
|
+
const state = stateRef.current
|
|
11283
|
+
if (state === null) return
|
|
11284
|
+
state.clientX = moveEvent.clientX
|
|
11285
|
+
state.clientY = moveEvent.clientY
|
|
11286
|
+
if (state.active === false && Math.abs(moveEvent.clientX - state.startX) < 4 && Math.abs(moveEvent.clientY - state.startY) < 4) return
|
|
11287
|
+
state.active = true
|
|
11288
|
+
swallowRef.current = true
|
|
11289
|
+
schedule()
|
|
11290
|
+
}
|
|
11291
|
+
const onUp = () => {
|
|
11292
|
+
window.removeEventListener('pointermove', onMove)
|
|
11293
|
+
window.removeEventListener('pointerup', onUp)
|
|
11294
|
+
cancelAnimationFrame(frameRef.current)
|
|
11295
|
+
const wasActive = stateRef.current !== null && stateRef.current.active === true
|
|
11296
|
+
stateRef.current = null
|
|
11297
|
+
setBox(null)
|
|
11298
|
+
// The drag's pointerup is followed by a synthetic click on whatever
|
|
11299
|
+
// row is under it — swallow exactly that one click.
|
|
11300
|
+
if (wasActive === true) setTimeout(() => { swallowRef.current = false }, 0)
|
|
11301
|
+
}
|
|
11302
|
+
window.addEventListener('pointermove', onMove)
|
|
11303
|
+
window.addEventListener('pointerup', onUp)
|
|
11304
|
+
}
|
|
11305
|
+
return { box: box, onPointerDown: onPointerDown, swallowRef: swallowRef }
|
|
11306
|
+
}
|
|
11307
|
+
|
|
11308
|
+
/* The marquee box itself: a viewport-anchored absolute div inside the
|
|
11309
|
+
draw host (invariant 13: never position:fixed inside the workbench's
|
|
11310
|
+
contain:layout). */
|
|
11311
|
+
function MarqueeBox(props) {
|
|
11312
|
+
if (props.box === null) return null
|
|
11313
|
+
return E('div', {
|
|
11314
|
+
className: 'dig-marquee',
|
|
11315
|
+
style: { left: props.box.x + 'px', top: props.box.y + 'px', width: props.box.w + 'px', height: props.box.h + 'px' },
|
|
11316
|
+
})
|
|
11317
|
+
}
|
|
11318
|
+
|
|
11146
11319
|
/* First index whose cumulative offset exceeds `at` (binary search over the
|
|
11147
11320
|
prefix-sum array; offsets.length === items.length + 1). */
|
|
11148
11321
|
function windowStart(offsets, at) {
|
|
@@ -11249,11 +11422,44 @@ window.__ModuleLoader__.load({
|
|
|
11249
11422
|
return 0
|
|
11250
11423
|
}
|
|
11251
11424
|
|
|
11425
|
+
/* The author filter pins "me (name)" to the top (v0.19.3). The identity
|
|
11426
|
+
is resolved once per repo root and kept for the panel lifetime — git
|
|
11427
|
+
identities don't churn within a session — so re-opening the Git tab or
|
|
11428
|
+
hopping sessions never re-runs `git config`. */
|
|
11429
|
+
const gitMeCache = new Map()
|
|
11430
|
+
|
|
11252
11431
|
function HistoryList(props) {
|
|
11253
11432
|
const t = props.t
|
|
11254
11433
|
const commits = props.commits
|
|
11255
11434
|
const historyScrollRef = useRef(null)
|
|
11435
|
+
const [me, setMe] = useState(() => (props.base !== undefined && gitMeCache.has(props.base.repoRoot) === true ? gitMeCache.get(props.base.repoRoot) : null))
|
|
11436
|
+
useEffect(() => {
|
|
11437
|
+
const base = props.base
|
|
11438
|
+
if (base === undefined || base.repoRoot === undefined) return
|
|
11439
|
+
if (gitMeCache.has(base.repoRoot) === true) { setMe(gitMeCache.get(base.repoRoot)); return }
|
|
11440
|
+
let alive = true
|
|
11441
|
+
props.request('me', Object.assign({}, base)).then((data) => {
|
|
11442
|
+
const who = data !== null && typeof data === 'object' && typeof data.name === 'string' && data.name !== ''
|
|
11443
|
+
? { name: data.name, email: typeof data.email === 'string' ? data.email : '' }
|
|
11444
|
+
: null
|
|
11445
|
+
gitMeCache.set(base.repoRoot, who)
|
|
11446
|
+
if (alive === true) setMe(who)
|
|
11447
|
+
}).catch((error) => { void error })
|
|
11448
|
+
return () => { alive = false }
|
|
11449
|
+
}, [props.base])
|
|
11256
11450
|
const viewport = useViewportBox(historyScrollRef)
|
|
11451
|
+
/* v0.19.3: the commit log rubber-bands exactly like the changes list —
|
|
11452
|
+
same hook, same gestures. Fixed HISTORY_ROW_H rows make the rects a
|
|
11453
|
+
pure index map, so even a windowed (mostly unmounted) list drags
|
|
11454
|
+
perfectly; picks land in the SAME pickedHashes set the Shift/Ctrl
|
|
11455
|
+
clicks and the commit context menu already share. */
|
|
11456
|
+
const historyHostRef = useRef(null)
|
|
11457
|
+
const historyMarquee = useRubberBand({
|
|
11458
|
+
scrollRef: historyScrollRef,
|
|
11459
|
+
drawRef: historyHostRef,
|
|
11460
|
+
rectsOf: () => rows.map((row, index) => ({ key: row.commit.hash, top: index * HISTORY_ROW_H, bottom: (index + 1) * HISTORY_ROW_H })),
|
|
11461
|
+
onPick: (keys) => { if (props.onPicksChange !== undefined) props.onPicksChange(new Set(keys)) },
|
|
11462
|
+
})
|
|
11257
11463
|
const pickAnchor = useRef(null)
|
|
11258
11464
|
const [text, setText] = useState('')
|
|
11259
11465
|
const [author, setAuthor] = useState('')
|
|
@@ -11309,6 +11515,92 @@ window.__ModuleLoader__.load({
|
|
|
11309
11515
|
}, [commits, text, author, refName, since, order])
|
|
11310
11516
|
|
|
11311
11517
|
const rows = useMemo(() => buildRows(filtered), [filtered])
|
|
11518
|
+
|
|
11519
|
+
/* Weibo-style infinite scroll (v0.19.3): descending lists auto-load when
|
|
11520
|
+
the user scrolls near the bottom edge, ascending ones near the top
|
|
11521
|
+
edge — wheel and scrollbar drags both surface as scroll events, no
|
|
11522
|
+
extra clicks. The button stays as the visible affordance. The guard
|
|
11523
|
+
keeps one request in flight; it resets when busy settles. */
|
|
11524
|
+
const infiniteGuardRef = useRef(false)
|
|
11525
|
+
const growAnchorRef = useRef(null)
|
|
11526
|
+
const mergeLocatedRef = useRef(false)
|
|
11527
|
+
const requestMore = () => {
|
|
11528
|
+
if (props.hasMore !== true || props.busy === true || infiniteGuardRef.current === true) return
|
|
11529
|
+
const host = historyScrollRef.current
|
|
11530
|
+
growAnchorRef.current = { length: rows.length, height: host === null ? 0 : host.scrollHeight }
|
|
11531
|
+
infiniteGuardRef.current = true
|
|
11532
|
+
props.onLoadMore()
|
|
11533
|
+
}
|
|
11534
|
+
/* The button itself is the trigger (weibo-style, v0.19.3): an
|
|
11535
|
+
IntersectionObserver on the load-more button fires wherever it sits —
|
|
11536
|
+
bottom while descending, top while ascending — covering wheel,
|
|
11537
|
+
scrollbar drags, first paint, and short pages alike. Re-created per
|
|
11538
|
+
page so a settled load re-evaluates: if the button is STILL visible
|
|
11539
|
+
(huge viewport / tiny page) the next page follows automatically. */
|
|
11540
|
+
const loadMoreRef = useRef(null)
|
|
11541
|
+
/* A scroll that lands on the button WHILE a guarded load is still in
|
|
11542
|
+
flight loses its one IO callback to the busy bail-out, and the
|
|
11543
|
+
observer is not re-created when that load finishes with the same
|
|
11544
|
+
page size (mount refresh). Re-check the button's geometry when busy
|
|
11545
|
+
settles: still within the rootMargin window → continue loading. */
|
|
11546
|
+
useEffect(() => {
|
|
11547
|
+
if (props.busy !== false) return
|
|
11548
|
+
infiniteGuardRef.current = false
|
|
11549
|
+
const node = loadMoreRef.current
|
|
11550
|
+
const root = historyScrollRef.current
|
|
11551
|
+
if (node === null || root === null || props.hasMore !== true) return
|
|
11552
|
+
const rect = node.getBoundingClientRect()
|
|
11553
|
+
const box = root.getBoundingClientRect()
|
|
11554
|
+
if (rect.bottom >= box.top - 240 && rect.top <= box.bottom + 240) requestMore()
|
|
11555
|
+
}, [props.busy])
|
|
11556
|
+
useEffect(() => {
|
|
11557
|
+
const node = loadMoreRef.current
|
|
11558
|
+
const root = historyScrollRef.current
|
|
11559
|
+
if (node === null || root === null || props.hasMore !== true) return
|
|
11560
|
+
let frame = 0
|
|
11561
|
+
const io = new IntersectionObserver((entries) => {
|
|
11562
|
+
if (entries.some((entry) => entry.isIntersecting === true) === false) return
|
|
11563
|
+
if (frame === 0) {
|
|
11564
|
+
frame = requestAnimationFrame(() => { frame = 0; requestMore() })
|
|
11565
|
+
}
|
|
11566
|
+
}, { root: root, rootMargin: '240px 0px' })
|
|
11567
|
+
io.observe(node)
|
|
11568
|
+
return () => { if (frame !== 0) cancelAnimationFrame(frame); io.disconnect() }
|
|
11569
|
+
}, [props.hasMore, order, rows.length])
|
|
11570
|
+
/* Ascending growth inserts rows above the viewport: re-anchor by the
|
|
11571
|
+
height delta so the rows the user was reading stay put. */
|
|
11572
|
+
useEffect(() => {
|
|
11573
|
+
const anchor = growAnchorRef.current
|
|
11574
|
+
if (anchor === null || rows.length <= anchor.length) return
|
|
11575
|
+
growAnchorRef.current = null
|
|
11576
|
+
const host = historyScrollRef.current
|
|
11577
|
+
if (order !== 'asc' || host === null) return
|
|
11578
|
+
host.scrollTop = host.scrollTop + (host.scrollHeight - anchor.height)
|
|
11579
|
+
}, [rows.length, order])
|
|
11580
|
+
/* v0.19.3 IDEA-style merge memory: when a browsed branch has a merge
|
|
11581
|
+
relationship with the PREVIOUSLY browsed one, its list opens scrolled
|
|
11582
|
+
to that merge commit ('merge-point' resolved it) — the "2 was merged
|
|
11583
|
+
here" moment is on screen immediately, while 1's newer commits stay
|
|
11584
|
+
one wheel-scroll away. Fires only on anchor CHANGE; null = no
|
|
11585
|
+
relationship (or first hop), leave the top alone. */
|
|
11586
|
+
useEffect(() => {
|
|
11587
|
+
const anchor = props.mergeAnchor
|
|
11588
|
+
if (anchor === undefined || anchor === null) { mergeLocatedRef.current = false; return }
|
|
11589
|
+
const index = rows.findIndex((row) => row.commit.hash === anchor)
|
|
11590
|
+
if (index < 0) {
|
|
11591
|
+
/* The merge point can sit below the first page on a long branch
|
|
11592
|
+
(real repos easily outrun LIMIT). Keep auto-loading — the same
|
|
11593
|
+
weibo path as the edge trigger — until it surfaces or the log
|
|
11594
|
+
is exhausted; one location per anchor, so later 'load more'
|
|
11595
|
+
never yanks the view back. */
|
|
11596
|
+
if (mergeLocatedRef.current === false && props.hasMore === true && props.busy !== true) requestMore()
|
|
11597
|
+
return
|
|
11598
|
+
}
|
|
11599
|
+
if (mergeLocatedRef.current === true) return
|
|
11600
|
+
mergeLocatedRef.current = true
|
|
11601
|
+
const host = historyScrollRef.current
|
|
11602
|
+
if (host !== null) host.scrollTop = Math.max(0, index * HISTORY_ROW_H - 8)
|
|
11603
|
+
}, [props.mergeAnchor, rows.length, props.busy])
|
|
11312
11604
|
/* Multi-select gestures (issue #13): the rendered row order is what the
|
|
11313
11605
|
Shift-range walks, and the range is computed over the FILTERED rows —
|
|
11314
11606
|
the same list the eye sees. A plain click only SELECTS (single-replace,
|
|
@@ -11410,7 +11702,12 @@ window.__ModuleLoader__.load({
|
|
|
11410
11702
|
}),
|
|
11411
11703
|
E(FilterSelect, {
|
|
11412
11704
|
label: t('filter.user'), value: author, hint: t('filter.user'),
|
|
11413
|
-
|
|
11705
|
+
/* "Me" first (v0.19.3): resolved from git config and offered even
|
|
11706
|
+
when this branch has no commits of mine — selecting it over an
|
|
11707
|
+
empty list is the answer "I never committed here". */
|
|
11708
|
+
options: [allOption]
|
|
11709
|
+
.concat(me !== null && me.name !== '' ? [{ value: me.name, label: t('history.me') + ' (' + me.name + ')' }] : [])
|
|
11710
|
+
.concat(authors.map((name) => ({ value: name, label: name }))),
|
|
11414
11711
|
onOpenMenu: (event, options) => openFilterMenu(event, options, author, setAuthor),
|
|
11415
11712
|
}),
|
|
11416
11713
|
E(FilterSelect, {
|
|
@@ -11447,12 +11744,27 @@ window.__ModuleLoader__.load({
|
|
|
11447
11744
|
const winEnd = Math.min(rows.length, winStart + Math.ceil(viewport.viewH / HISTORY_ROW_H) + WINDOW_OVERSCAN * 2)
|
|
11448
11745
|
const visibleRows = rows.slice(winStart, winEnd)
|
|
11449
11746
|
|
|
11450
|
-
|
|
11747
|
+
/* Ascending renders oldest-first, so load-more (older commits) lives
|
|
11748
|
+
at the TOP; descending keeps it at the bottom where it always was. */
|
|
11749
|
+
const loadMoreButton = props.hasMore === true
|
|
11750
|
+
? E('button', { ref: loadMoreRef, type: 'button', className: 'dig-load-more', disabled: props.busy === true, onClick: requestMore }, t('history.loadMore'))
|
|
11751
|
+
: null
|
|
11752
|
+
|
|
11753
|
+
return E('div', { className: 'dig-history', ref: historyHostRef }, filterBar,
|
|
11451
11754
|
E('div', {
|
|
11452
11755
|
className: 'dig-history-scroll',
|
|
11453
11756
|
ref: historyScrollRef,
|
|
11454
11757
|
tabIndex: 0,
|
|
11455
11758
|
onKeyDown: onListKeyDown,
|
|
11759
|
+
onPointerDown: historyMarquee.onPointerDown,
|
|
11760
|
+
onClickCapture: (event) => {
|
|
11761
|
+
if (historyMarquee.swallowRef.current === true) { event.preventDefault(); event.stopPropagation(); return }
|
|
11762
|
+
/* v0.19.4: blank click clears — see the changes/tree twins. */
|
|
11763
|
+
const target = event.target
|
|
11764
|
+
if (target instanceof Element && target.closest('.dig-commit,button,input,textarea,select,a') === null) {
|
|
11765
|
+
if (props.onPicksChange !== undefined) props.onPicksChange(new Set())
|
|
11766
|
+
}
|
|
11767
|
+
},
|
|
11456
11768
|
onPointerDownCapture: (event) => {
|
|
11457
11769
|
if (isTypingTarget(event.target) === false) event.currentTarget.focus({ preventScroll: true })
|
|
11458
11770
|
},
|
|
@@ -11471,6 +11783,7 @@ window.__ModuleLoader__.load({
|
|
|
11471
11783
|
onMouseLeave: (event) => event.currentTarget.removeAttribute('data-hover-lane'),
|
|
11472
11784
|
},
|
|
11473
11785
|
rows.length === 0 ? E('div', { className: 'dig-empty' }, dirtyFilter ? t('filter.none') : t('history.empty')) : null,
|
|
11786
|
+
order === 'asc' ? loadMoreButton : null,
|
|
11474
11787
|
winStart > 0 ? E('div', { key: 'pad-top', style: { height: String(winStart * HISTORY_ROW_H) + 'px' } }) : null,
|
|
11475
11788
|
visibleRows.map((row) => E(CommitRow, {
|
|
11476
11789
|
key: row.commit.hash,
|
|
@@ -11480,9 +11793,8 @@ window.__ModuleLoader__.load({
|
|
|
11480
11793
|
onRowClick: handleRowClick, onOpenDetail: props.onSelect, onMenu: props.onMenu, openMenuAt: props.openMenuAt,
|
|
11481
11794
|
})),
|
|
11482
11795
|
winEnd < rows.length ? E('div', { key: 'pad-bottom', style: { height: String((rows.length - winEnd) * HISTORY_ROW_H) + 'px' } }) : null,
|
|
11483
|
-
|
|
11484
|
-
|
|
11485
|
-
: null))
|
|
11796
|
+
order === 'desc' ? loadMoreButton : null),
|
|
11797
|
+
E(MarqueeBox, { box: historyMarquee.box }))
|
|
11486
11798
|
}
|
|
11487
11799
|
|
|
11488
11800
|
function CommitDetail(props) {
|
|
@@ -11573,6 +11885,28 @@ window.__ModuleLoader__.load({
|
|
|
11573
11885
|
/* The branch tree reports its picks up (onPicksChange) so the rail can act
|
|
11574
11886
|
on the selection: "select main, press pull" must pull main, not HEAD. */
|
|
11575
11887
|
const [branchPicks, setBranchPicks] = useState(() => new Set())
|
|
11888
|
+
/* v0.19.4: the three lists' selections are mutually exclusive — a pick in
|
|
11889
|
+
one list (row click or rubber band) clears the other two, and a blank
|
|
11890
|
+
click inside a list clears everything too. Each interaction bumps the
|
|
11891
|
+
OTHER lists' epoch counters; the effects below (and a clearSignal prop
|
|
11892
|
+
into BranchTree, whose picks live inside it) reset on change. Epochs
|
|
11893
|
+
are monotonic and start at 0, so the mount pass clears nothing. */
|
|
11894
|
+
const [pickEpochs, setPickEpochs] = useState(() => ({ changes: 0, history: 0, tree: 0 }))
|
|
11895
|
+
const clearPicksExcept = useCallback((owner) => {
|
|
11896
|
+
setPickEpochs((previous) => ({
|
|
11897
|
+
changes: owner === 'changes' ? previous.changes : previous.changes + 1,
|
|
11898
|
+
history: owner === 'history' ? previous.history : previous.history + 1,
|
|
11899
|
+
tree: owner === 'tree' ? previous.tree : previous.tree + 1,
|
|
11900
|
+
}))
|
|
11901
|
+
}, [])
|
|
11902
|
+
useEffect(() => { if (pickEpochs.changes > 0) setChangeSelection(new Set()) }, [pickEpochs.changes])
|
|
11903
|
+
useEffect(() => { if (pickEpochs.history > 0) setPickedCommits(new Set()) }, [pickEpochs.history])
|
|
11904
|
+
/* Forwarders: the raw setters wrapped so EVERY selection gesture reports
|
|
11905
|
+
an owner. The updates arrive either as a Set or as a functional
|
|
11906
|
+
updater (the memoised rows hold stale closures) — forwarded as-is. */
|
|
11907
|
+
const onBranchPicks = useCallback((update) => { setBranchPicks(update); clearPicksExcept('tree') }, [clearPicksExcept])
|
|
11908
|
+
const onChangeSelection = useCallback((update) => { setChangeSelection(update); clearPicksExcept('changes') }, [clearPicksExcept])
|
|
11909
|
+
const onCommitPicks = useCallback((update) => { setPickedCommits(update); clearPicksExcept('history') }, [clearPicksExcept])
|
|
11576
11910
|
const [menu, setMenu] = useState(null)
|
|
11577
11911
|
const [dialog, setDialog] = useState(null)
|
|
11578
11912
|
const [tick, setTick] = useState(0)
|
|
@@ -11956,15 +12290,25 @@ window.__ModuleLoader__.load({
|
|
|
11956
12290
|
return data
|
|
11957
12291
|
}, [base])
|
|
11958
12292
|
|
|
12293
|
+
/* ---- branch browsing (v0.19.3, IDEA-style) ----
|
|
12294
|
+
Double-clicking a tree branch no longer checks it out: it walks THAT
|
|
12295
|
+
branch's log (browseRev = '' keeps the HEAD view). The LAST browsed
|
|
12296
|
+
branch is remembered, so 2-then-1 lands 1's list on the merge commit
|
|
12297
|
+
that carried 2 into 1 ('merge-point') — 1's newer commits stay one
|
|
12298
|
+
wheel-scroll away, nothing is truncated. */
|
|
12299
|
+
const [browseRev, setBrowseRev] = useState('')
|
|
12300
|
+
const [mergeAnchor, setMergeAnchor] = useState(null)
|
|
12301
|
+
const lastBrowseRef = useRef(null)
|
|
11959
12302
|
const loadCommits = useCallback(async (skip, overridePath) => {
|
|
11960
12303
|
const activePath = overridePath === undefined ? pathFilter : overridePath
|
|
11961
12304
|
const payload = Object.assign({}, base, { skip: skip === undefined ? 0 : skip, limit: 120 })
|
|
12305
|
+
if (browseRev !== '') payload.rev = browseRev
|
|
11962
12306
|
if (activePath !== undefined && activePath !== '') payload.path = activePath
|
|
11963
12307
|
const data = await request('log', payload)
|
|
11964
12308
|
setCommits((previous) => (skip === undefined || skip === 0 ? data.commits : previous.concat(data.commits)))
|
|
11965
12309
|
setHasMore(data.hasMore)
|
|
11966
12310
|
return data
|
|
11967
|
-
}, [base, pathFilter])
|
|
12311
|
+
}, [base, pathFilter, browseRev])
|
|
11968
12312
|
|
|
11969
12313
|
/* `fetchRemote` turns the refresh into what the rail button means: "see
|
|
11970
12314
|
whether the remote has anything new to pull" — a `git fetch --prune`
|
|
@@ -12007,13 +12351,40 @@ window.__ModuleLoader__.load({
|
|
|
12007
12351
|
(a paged fetch never reaches this path), replaced on every
|
|
12008
12352
|
refresh — the auto-refresh included — so a session switch through
|
|
12009
12353
|
the same workspace seeds fresh data, not the mount-time
|
|
12010
|
-
snapshot.
|
|
12011
|
-
|
|
12354
|
+
snapshot. A branch-browsing page (browseRev) must not evict the
|
|
12355
|
+
HEAD page: the next mount opens on HEAD and would otherwise be
|
|
12356
|
+
seeded with another branch's log. */
|
|
12357
|
+
if (browseRev === '') writePaintPage(repoRoot, { summary: summaryData, branches: branchesData, commits: page.commits, hasMore: page.hasMore })
|
|
12012
12358
|
})
|
|
12013
12359
|
} finally {
|
|
12014
12360
|
if (withFetch === true) setRefreshSpinning(false)
|
|
12015
12361
|
}
|
|
12016
|
-
}, [guard, loadSummary, loadBranches, loadCommits, repoRoot, remoteReady, base])
|
|
12362
|
+
}, [guard, loadSummary, loadBranches, loadCommits, repoRoot, remoteReady, base, browseRev])
|
|
12363
|
+
|
|
12364
|
+
/* Double-click a branch (or the HEAD row): browse its history. The merge
|
|
12365
|
+
anchor comes from the PREVIOUSLY browsed branch — 'merge-point' finds
|
|
12366
|
+
the newest merge that carried it into the one now opened, and the
|
|
12367
|
+
history list scrolls there once (null = no relationship, top). */
|
|
12368
|
+
const browseBranch = useCallback(async (entry) => {
|
|
12369
|
+
const name = String(entry.name)
|
|
12370
|
+
const previous = lastBrowseRef.current
|
|
12371
|
+
if (previous === name) return
|
|
12372
|
+
lastBrowseRef.current = name
|
|
12373
|
+
setMergeAnchor(null)
|
|
12374
|
+
setBrowseRev(name)
|
|
12375
|
+
if (previous !== null) {
|
|
12376
|
+
try {
|
|
12377
|
+
const data = await request('merge-point', Object.assign({}, base, { from: previous, to: name }))
|
|
12378
|
+
if (data !== null && typeof data === 'object' && typeof data.hash === 'string' && data.hash !== '') setMergeAnchor(data.hash)
|
|
12379
|
+
} catch (error) { void error }
|
|
12380
|
+
}
|
|
12381
|
+
}, [base, request])
|
|
12382
|
+
/* A repository switch forgets the browsing context entirely. */
|
|
12383
|
+
useEffect(() => {
|
|
12384
|
+
lastBrowseRef.current = null
|
|
12385
|
+
setBrowseRev('')
|
|
12386
|
+
setMergeAnchor(null)
|
|
12387
|
+
}, [repoRoot])
|
|
12017
12388
|
|
|
12018
12389
|
useEffect(() => {
|
|
12019
12390
|
if (repoRoot === null) return undefined
|
|
@@ -12960,9 +13331,12 @@ window.__ModuleLoader__.load({
|
|
|
12960
13331
|
onLoadMore: () => { void guard(() => loadCommits(commits.length)) },
|
|
12961
13332
|
/* ---- multi-select (issue #13) ---- */
|
|
12962
13333
|
pickedHashes: pickedCommits,
|
|
12963
|
-
onPicksChange:
|
|
12964
|
-
onSelectAll: (hashes) =>
|
|
13334
|
+
onPicksChange: onCommitPicks,
|
|
13335
|
+
onSelectAll: (hashes) => onCommitPicks(new Set(hashes)),
|
|
12965
13336
|
onKeyDown: onHistoryKeyDown,
|
|
13337
|
+
mergeAnchor: mergeAnchor,
|
|
13338
|
+
base: base,
|
|
13339
|
+
request: request,
|
|
12966
13340
|
})
|
|
12967
13341
|
|
|
12968
13342
|
const diffPane = patch === '' && patchLoading === false ? null : E('div', { className: 'dig-diff-pane', 'data-pane': 'diff', style: paneStyle('diff'), onContextMenu: onDiffPaneContextMenu },
|
|
@@ -13231,7 +13605,7 @@ window.__ModuleLoader__.load({
|
|
|
13231
13605
|
onChangeMenu: changeMenu,
|
|
13232
13606
|
/* ---- multi-select (issue #13) ---- */
|
|
13233
13607
|
selection: changeSelection,
|
|
13234
|
-
onSelectionChange:
|
|
13608
|
+
onSelectionChange: onChangeSelection,
|
|
13235
13609
|
anchorRef: changeAnchor,
|
|
13236
13610
|
onBulkStage: bulkStageChanges,
|
|
13237
13611
|
onBulkUnstage: bulkUnstageChanges,
|
|
@@ -13512,19 +13886,19 @@ window.__ModuleLoader__.load({
|
|
|
13512
13886
|
title: t('toolbar.tree'),
|
|
13513
13887
|
onClick: () => foldTree(!treeOpen),
|
|
13514
13888
|
}, 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:
|
|
13889
|
+
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: onBranchPicks, clearSignal: pickEpochs.tree })) : null,
|
|
13516
13890
|
treeOpen ? gutter('tree', 'prev') : null,
|
|
13517
13891
|
view === 'changes' ? changesPane : E('div', { className: 'dig-pane dig-pane-main' }, historyPane, diffGutter, diffPane))
|
|
13518
13892
|
} else if (columns) {
|
|
13519
13893
|
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:
|
|
13894
|
+
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: onBranchPicks, clearSignal: pickEpochs.tree })) : null,
|
|
13521
13895
|
treeOpen ? gutter('tree', 'prev') : null,
|
|
13522
13896
|
E('div', { className: 'dig-pane dig-pane-main' }, historyPane, diffGutter, diffPane),
|
|
13523
13897
|
gutter('changes', 'next'),
|
|
13524
13898
|
E('div', { className: 'dig-pane dig-pane-changes', 'data-pane': 'changes', style: paneStyle('changes') }, changesPane))
|
|
13525
13899
|
} else {
|
|
13526
13900
|
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:
|
|
13901
|
+
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: onBranchPicks, clearSignal: pickEpochs.tree })) : null,
|
|
13528
13902
|
treeOpen ? gutter('tree', 'prev') : null,
|
|
13529
13903
|
E('div', { className: 'dig-pane dig-pane-changes-stack', 'data-pane': 'changes', style: paneStyle('changes') }, changesPane),
|
|
13530
13904
|
gutter('changes', 'prev'),
|
|
@@ -14049,7 +14423,10 @@ window.__ModuleLoader__.load({
|
|
|
14049
14423
|
'.dig-picker-name{flex:none;font-weight:600}',
|
|
14050
14424
|
'.dig-picker-kind{color:var(--dsw-alias-label-tertiary);flex:none}',
|
|
14051
14425
|
'.dig-picker-path{color:var(--dsw-alias-label-tertiary);margin-left:auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:50%}',
|
|
14052
|
-
|
|
14426
|
+
/* position:relative: the marquee box paints against this wrapper (the
|
|
14427
|
+
scroll container is offset by the search field — same rule as
|
|
14428
|
+
.dig-history above). */
|
|
14429
|
+
'.dig-tree{display:flex;flex-direction:column;min-height:0;flex:1;position:relative}',
|
|
14053
14430
|
'.dig-tree-scroll{flex:1;overflow:auto;padding-bottom:6px}',
|
|
14054
14431
|
'.dig-section{display:flex;flex-direction:column}',
|
|
14055
14432
|
'.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,8 +14449,19 @@ window.__ModuleLoader__.load({
|
|
|
14072
14449
|
'.dig-chevron-open{transform:rotate(0deg)}',
|
|
14073
14450
|
'.dig-row{display:flex;align-items:center;gap:5px;padding:2px 8px;cursor:pointer;white-space:nowrap;overflow:hidden}',
|
|
14074
14451
|
'.dig-row:hover{background:var(--dsw-alias-interactive-bg-hover)}',
|
|
14452
|
+
/* v0.19.3: the HEAD section row behaves like a branch row (click
|
|
14453
|
+
selects, double-click browses, right-click menus) — same hover cue. */
|
|
14454
|
+
'.dig-head-branch{cursor:pointer;transition:background .12s}',
|
|
14455
|
+
'.dig-head-branch:hover{background:var(--dsw-alias-interactive-bg-hover)}',
|
|
14075
14456
|
'.dig-row-head{color:var(--dsw-alias-state-warn-primary,var(--dsw-alias-brand-primary))}',
|
|
14076
|
-
|
|
14457
|
+
/* v0.19.4: the flat alias background read as "barely selected" over the
|
|
14458
|
+
translucency/frost surfaces the any-background plugin produces. Mixing
|
|
14459
|
+
the token with the row's own TEXT color keeps the theme hues (light
|
|
14460
|
+
theme darkens, dark theme lightens — both gain contrast), and because
|
|
14461
|
+
the text color is near-opaque the mix carries an alpha FLOOR (~42%):
|
|
14462
|
+
the token's own translucency still dominates the look (58%), so the
|
|
14463
|
+
selection follows the plugin's transparency while staying readable. */
|
|
14464
|
+
'.dig-row-selected{background:color-mix(in srgb,var(--dsw-alias-interactive-bg-active,var(--dsw-alias-interactive-bg-hover,var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1)))) 58%,currentColor 42%);color:var(--dsw-alias-label-primary);box-shadow:inset 2px 0 0 var(--dsw-alias-brand-primary)}',
|
|
14077
14465
|
'.dig-row-remote .dig-row-label{color:var(--dsw-alias-label-secondary);font-weight:500}',
|
|
14078
14466
|
'.dig-row-tag .dig-row-label{color:var(--dsw-alias-state-warn-primary,var(--dsw-alias-brand-primary));font-weight:500}',
|
|
14079
14467
|
'.dig-row-head .dig-row-label{font-weight:600}',
|
|
@@ -14090,11 +14478,15 @@ window.__ModuleLoader__.load({
|
|
|
14090
14478
|
'.dig-badge.dig-badge-ahead,.dig-badge.dig-badge-behind{padding:0 6px}',
|
|
14091
14479
|
'.dig-badge-muted{opacity:.8}',
|
|
14092
14480
|
'.dig-badge-worktree{padding:0 3px;display:inline-flex;align-items:center;height:14px}',
|
|
14093
|
-
|
|
14481
|
+
/* position:relative: the marquee box (rubber-band selection) paints
|
|
14482
|
+
against this wrapper — the scroll container below is offset by the
|
|
14483
|
+
filter bar, and painting from the wrong rect was the v0.19.2 drift. */
|
|
14484
|
+
'.dig-history{display:flex;flex-direction:column;min-height:0;flex:1;position:relative}',
|
|
14094
14485
|
'.dig-history-scroll{flex:1;overflow:auto}',
|
|
14095
14486
|
'.dig-commit{display:flex;align-items:center;gap:6px;padding:1px 8px 1px 0;cursor:pointer;height:26px;overflow:hidden}',
|
|
14096
14487
|
'.dig-commit:hover{background:var(--dsw-alias-interactive-bg-hover)}',
|
|
14097
|
-
|
|
14488
|
+
/* Same floor + tint mix as .dig-row-selected (see it). */
|
|
14489
|
+
'.dig-commit-selected{background:color-mix(in srgb,var(--dsw-alias-interactive-bg-active,var(--dsw-alias-interactive-bg-hover,var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1)))) 58%,currentColor 42%);color:var(--dsw-alias-label-primary);box-shadow:inset 2px 0 0 var(--dsw-alias-brand-primary)}',
|
|
14098
14490
|
// A favorited branch renders its star SOLID (BranchRow adds this class
|
|
14099
14491
|
// and flips the tone to warn) — the hollow green HEAD star read as
|
|
14100
14492
|
// "not favorited" (v0.18.5).
|