@youtyan/code-viewer 0.1.12 → 0.1.14
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/package.json +1 -1
- package/web/app.js +825 -79
- package/web/index.html +1 -0
- package/web/style.css +218 -3
- package/web-src/routes.ts +16 -1
- package/web-src/server/git.ts +77 -57
- package/web-src/server/preview.ts +4 -1
- package/web-src/types.ts +1 -0
package/web/index.html
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
<nav class="app-menu" aria-label="Views">
|
|
27
27
|
<a class="app-menu-item active" data-route="repo" href="/">Repository</a>
|
|
28
28
|
<a class="app-menu-item active" data-route="diff" href="/todif?from=HEAD&to=worktree">Diff Viewer</a>
|
|
29
|
+
<a class="app-menu-item" data-route="help" href="/help">Help</a>
|
|
29
30
|
</nav>
|
|
30
31
|
<div class="global-actions">
|
|
31
32
|
<button id="theme" title="toggle theme">🌗</button>
|
package/web/style.css
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
--accent-emph: #0550ae;
|
|
23
23
|
--accent-subtle: #ddf4ff;
|
|
24
24
|
--accent-muted: #54aeff66;
|
|
25
|
+
--scope-accent: #bf8700;
|
|
25
26
|
/* success / danger / attention / done */
|
|
26
27
|
--success: #1a7f37;
|
|
27
28
|
--success-emph: #1f883d;
|
|
@@ -70,6 +71,7 @@
|
|
|
70
71
|
--accent-emph: #1f6feb;
|
|
71
72
|
--accent-subtle: rgba(56,139,253,0.10);
|
|
72
73
|
--accent-muted: rgba(56,139,253,0.40);
|
|
74
|
+
--scope-accent: #d29922;
|
|
73
75
|
--success: #3fb950;
|
|
74
76
|
--success-emph: #238636;
|
|
75
77
|
--success-subtle: rgba(46,160,67,0.15);
|
|
@@ -804,6 +806,16 @@ html, body {
|
|
|
804
806
|
padding: 0 0 32px;
|
|
805
807
|
z-index: 30;
|
|
806
808
|
}
|
|
809
|
+
body[data-focus-scope="sidebar"] #sidebar {
|
|
810
|
+
background: var(--bg-soft);
|
|
811
|
+
outline: none;
|
|
812
|
+
box-shadow: none;
|
|
813
|
+
}
|
|
814
|
+
body[data-focus-scope="main"] #content {
|
|
815
|
+
background: var(--bg);
|
|
816
|
+
outline: none;
|
|
817
|
+
box-shadow: none;
|
|
818
|
+
}
|
|
807
819
|
.sb-head {
|
|
808
820
|
display: flex; justify-content: space-between; align-items: center;
|
|
809
821
|
padding: 14px 16px 10px;
|
|
@@ -926,6 +938,140 @@ body.gdp-resizing #sidebar-resizer {
|
|
|
926
938
|
body.gdp-resizing { cursor: col-resize !important; user-select: none; }
|
|
927
939
|
body.gdp-resizing * { user-select: none !important; }
|
|
928
940
|
|
|
941
|
+
body.gdp-help-page #topbar,
|
|
942
|
+
body.gdp-help-page #sidebar,
|
|
943
|
+
body.gdp-help-page #sidebar-resizer {
|
|
944
|
+
display: none;
|
|
945
|
+
}
|
|
946
|
+
body.gdp-help-page #content {
|
|
947
|
+
margin-left: 0;
|
|
948
|
+
padding-top: calc(var(--global-header-h) + 28px);
|
|
949
|
+
}
|
|
950
|
+
.gdp-help-shell {
|
|
951
|
+
width: 100%;
|
|
952
|
+
max-width: none;
|
|
953
|
+
margin: 0;
|
|
954
|
+
}
|
|
955
|
+
.gdp-help-header {
|
|
956
|
+
display: flex;
|
|
957
|
+
align-items: center;
|
|
958
|
+
justify-content: space-between;
|
|
959
|
+
gap: 16px;
|
|
960
|
+
margin-bottom: 22px;
|
|
961
|
+
}
|
|
962
|
+
.gdp-help-header h1 {
|
|
963
|
+
margin: 0;
|
|
964
|
+
font-size: 28px;
|
|
965
|
+
}
|
|
966
|
+
.gdp-help-language {
|
|
967
|
+
appearance: none;
|
|
968
|
+
min-width: 92px;
|
|
969
|
+
height: 34px;
|
|
970
|
+
border: 1px solid var(--border);
|
|
971
|
+
border-radius: 6px;
|
|
972
|
+
background:
|
|
973
|
+
linear-gradient(45deg, transparent 50%, var(--fg-muted) 50%) right 15px center / 5px 5px no-repeat,
|
|
974
|
+
linear-gradient(135deg, var(--fg-muted) 50%, transparent 50%) right 10px center / 5px 5px no-repeat,
|
|
975
|
+
var(--bg);
|
|
976
|
+
color: var(--fg);
|
|
977
|
+
padding: 0 34px 0 12px;
|
|
978
|
+
font: inherit;
|
|
979
|
+
font-size: 13px;
|
|
980
|
+
line-height: 32px;
|
|
981
|
+
cursor: pointer;
|
|
982
|
+
}
|
|
983
|
+
.gdp-help-language:hover {
|
|
984
|
+
border-color: var(--border-strong);
|
|
985
|
+
}
|
|
986
|
+
.gdp-help-language:focus {
|
|
987
|
+
border-color: var(--accent);
|
|
988
|
+
outline: none;
|
|
989
|
+
box-shadow: 0 0 0 3px var(--accent-muted);
|
|
990
|
+
}
|
|
991
|
+
.gdp-help-layout {
|
|
992
|
+
display: grid;
|
|
993
|
+
grid-template-columns: 220px minmax(0, 1fr);
|
|
994
|
+
gap: 28px;
|
|
995
|
+
align-items: start;
|
|
996
|
+
}
|
|
997
|
+
.gdp-help-nav {
|
|
998
|
+
position: sticky;
|
|
999
|
+
top: calc(var(--global-header-h) + 20px);
|
|
1000
|
+
display: grid;
|
|
1001
|
+
gap: 4px;
|
|
1002
|
+
padding-right: 16px;
|
|
1003
|
+
border-right: 1px solid var(--border-muted);
|
|
1004
|
+
}
|
|
1005
|
+
.gdp-help-nav button {
|
|
1006
|
+
appearance: none;
|
|
1007
|
+
border: 0;
|
|
1008
|
+
border-left: 3px solid transparent;
|
|
1009
|
+
border-radius: 0 6px 6px 0;
|
|
1010
|
+
background: transparent;
|
|
1011
|
+
color: var(--fg-muted);
|
|
1012
|
+
padding: 8px 10px;
|
|
1013
|
+
text-align: left;
|
|
1014
|
+
font: inherit;
|
|
1015
|
+
cursor: pointer;
|
|
1016
|
+
}
|
|
1017
|
+
.gdp-help-nav button:hover {
|
|
1018
|
+
background: var(--bg-mute);
|
|
1019
|
+
color: var(--fg);
|
|
1020
|
+
}
|
|
1021
|
+
.gdp-help-nav button.active {
|
|
1022
|
+
border-left-color: var(--accent);
|
|
1023
|
+
background: var(--accent-subtle);
|
|
1024
|
+
color: var(--accent-emph);
|
|
1025
|
+
font-weight: 700;
|
|
1026
|
+
}
|
|
1027
|
+
.gdp-help-content h2 {
|
|
1028
|
+
margin: 0 0 8px;
|
|
1029
|
+
font-size: 24px;
|
|
1030
|
+
}
|
|
1031
|
+
.gdp-help-content > p {
|
|
1032
|
+
margin: 0 0 24px;
|
|
1033
|
+
color: var(--fg-muted);
|
|
1034
|
+
}
|
|
1035
|
+
.gdp-help-group {
|
|
1036
|
+
margin: 0 0 28px;
|
|
1037
|
+
}
|
|
1038
|
+
.gdp-help-group h3 {
|
|
1039
|
+
margin: 0 0 10px;
|
|
1040
|
+
font-size: 16px;
|
|
1041
|
+
}
|
|
1042
|
+
.gdp-help-group table {
|
|
1043
|
+
width: 100%;
|
|
1044
|
+
border-collapse: collapse;
|
|
1045
|
+
border-top: 1px solid var(--border-muted);
|
|
1046
|
+
}
|
|
1047
|
+
.gdp-help-group th,
|
|
1048
|
+
.gdp-help-group td {
|
|
1049
|
+
padding: 11px 8px;
|
|
1050
|
+
border-bottom: 1px solid var(--border-muted);
|
|
1051
|
+
vertical-align: top;
|
|
1052
|
+
}
|
|
1053
|
+
.gdp-help-group th {
|
|
1054
|
+
width: 230px;
|
|
1055
|
+
color: var(--fg);
|
|
1056
|
+
font-weight: 600;
|
|
1057
|
+
text-align: left;
|
|
1058
|
+
}
|
|
1059
|
+
.gdp-help-group td {
|
|
1060
|
+
color: var(--fg-muted);
|
|
1061
|
+
}
|
|
1062
|
+
.gdp-help-group kbd {
|
|
1063
|
+
display: inline-block;
|
|
1064
|
+
min-width: 24px;
|
|
1065
|
+
padding: 2px 6px;
|
|
1066
|
+
border: 1px solid var(--border);
|
|
1067
|
+
border-bottom-color: var(--border-strong);
|
|
1068
|
+
border-radius: 5px;
|
|
1069
|
+
background: var(--bg-soft);
|
|
1070
|
+
color: var(--fg);
|
|
1071
|
+
font: 12px "Monaspace Neon", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
|
1072
|
+
text-align: center;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
929
1075
|
/* ===== Tree view ===== */
|
|
930
1076
|
#filelist.tree {
|
|
931
1077
|
padding: 4px 0 0;
|
|
@@ -954,6 +1100,12 @@ body.gdp-resizing * { user-select: none !important; }
|
|
|
954
1100
|
#filelist.tree .tree-dir.children-omitted .dir-label {
|
|
955
1101
|
border-bottom: 1px dashed var(--border-strong);
|
|
956
1102
|
}
|
|
1103
|
+
#filelist.tree .tree-dir.children-omitted-ignored {
|
|
1104
|
+
cursor: pointer;
|
|
1105
|
+
}
|
|
1106
|
+
#filelist.tree .tree-dir.children-omitted-internal {
|
|
1107
|
+
cursor: default;
|
|
1108
|
+
}
|
|
957
1109
|
#filelist.tree .tree-dir .chev {
|
|
958
1110
|
display: flex;
|
|
959
1111
|
width: 16px;
|
|
@@ -972,6 +1124,11 @@ body.gdp-resizing * { user-select: none !important; }
|
|
|
972
1124
|
width: 12px;
|
|
973
1125
|
height: 12px;
|
|
974
1126
|
}
|
|
1127
|
+
#filelist.tree .tree-dir .chev-spacer {
|
|
1128
|
+
width: 16px;
|
|
1129
|
+
height: 16px;
|
|
1130
|
+
display: inline-block;
|
|
1131
|
+
}
|
|
975
1132
|
#filelist.tree .tree-dir .dir-label {
|
|
976
1133
|
min-width: 0;
|
|
977
1134
|
display: flex;
|
|
@@ -992,7 +1149,7 @@ body.gdp-resizing * { user-select: none !important; }
|
|
|
992
1149
|
}
|
|
993
1150
|
#filelist.tree .tree-dir .dir-omitted {
|
|
994
1151
|
flex: 0 0 auto;
|
|
995
|
-
max-width:
|
|
1152
|
+
max-width: 62px;
|
|
996
1153
|
overflow: hidden;
|
|
997
1154
|
text-overflow: ellipsis;
|
|
998
1155
|
border: 1px solid var(--border-muted);
|
|
@@ -1003,6 +1160,16 @@ body.gdp-resizing * { user-select: none !important; }
|
|
|
1003
1160
|
line-height: 15px;
|
|
1004
1161
|
text-transform: uppercase;
|
|
1005
1162
|
}
|
|
1163
|
+
#filelist.tree .tree-dir .dir-omitted-ignored {
|
|
1164
|
+
color: var(--accent);
|
|
1165
|
+
border-color: var(--accent-muted);
|
|
1166
|
+
background: var(--accent-subtle);
|
|
1167
|
+
}
|
|
1168
|
+
#filelist.tree .tree-dir .dir-omitted-internal {
|
|
1169
|
+
color: var(--fg-subtle);
|
|
1170
|
+
border-color: var(--border-muted);
|
|
1171
|
+
background: var(--bg-soft);
|
|
1172
|
+
}
|
|
1006
1173
|
#filelist.tree .tree-dir .dir-icon {
|
|
1007
1174
|
color: #54aeff;
|
|
1008
1175
|
flex-shrink: 0;
|
|
@@ -1519,6 +1686,8 @@ table.d2h-diff-table tr.gdp-diff-line-target > td:first-child {
|
|
|
1519
1686
|
}
|
|
1520
1687
|
.gdp-copy-path.copied { color: var(--success); }
|
|
1521
1688
|
.gdp-copy-path.failed { color: var(--danger); }
|
|
1689
|
+
.gdp-copy-source.copied { color: var(--success); }
|
|
1690
|
+
.gdp-copy-source.failed { color: var(--danger); }
|
|
1522
1691
|
.gdp-open-path.opened { color: var(--success); }
|
|
1523
1692
|
.gdp-open-path.failed { color: var(--danger); }
|
|
1524
1693
|
.gdp-file-unfold[aria-pressed="true"] {
|
|
@@ -1738,6 +1907,23 @@ table.d2h-diff-table tr.gdp-diff-line-target > td:first-child {
|
|
|
1738
1907
|
color: var(--fg);
|
|
1739
1908
|
border-bottom-color: var(--blob-tab-active);
|
|
1740
1909
|
}
|
|
1910
|
+
.gdp-source-tabs .gdp-copy-source {
|
|
1911
|
+
margin-left: auto;
|
|
1912
|
+
align-self: center;
|
|
1913
|
+
width: 28px;
|
|
1914
|
+
height: 28px;
|
|
1915
|
+
padding: 0;
|
|
1916
|
+
border: 1px solid transparent;
|
|
1917
|
+
border-radius: 6px;
|
|
1918
|
+
}
|
|
1919
|
+
.gdp-source-virtual-copy {
|
|
1920
|
+
flex: 0 0 auto;
|
|
1921
|
+
width: 28px;
|
|
1922
|
+
height: 28px;
|
|
1923
|
+
padding: 0;
|
|
1924
|
+
border: 1px solid transparent;
|
|
1925
|
+
border-radius: 6px;
|
|
1926
|
+
}
|
|
1741
1927
|
.gdp-source-viewer > [hidden],
|
|
1742
1928
|
.gdp-markdown-layout[hidden],
|
|
1743
1929
|
.gdp-source-table[hidden] {
|
|
@@ -1754,6 +1940,25 @@ table.d2h-diff-table tr.gdp-diff-line-target > td:first-child {
|
|
|
1754
1940
|
.gdp-source-table tr.gdp-source-line-target {
|
|
1755
1941
|
background: var(--line-hit-bg);
|
|
1756
1942
|
}
|
|
1943
|
+
.gdp-source-table tr.gdp-source-cursor .gdp-source-line-number,
|
|
1944
|
+
.gdp-source-virtual-row.gdp-source-cursor .gdp-source-virtual-line-number {
|
|
1945
|
+
color: var(--fg);
|
|
1946
|
+
box-shadow: inset 3px 0 0 var(--scope-accent), inset -1px 0 0 var(--border-muted);
|
|
1947
|
+
}
|
|
1948
|
+
.gdp-source-table tr.gdp-source-cursor .gdp-source-line-code,
|
|
1949
|
+
.gdp-source-virtual-row.gdp-source-cursor .gdp-source-virtual-line-code {
|
|
1950
|
+
background: color-mix(in oklab, var(--bg) 90%, var(--scope-accent) 10%) !important;
|
|
1951
|
+
}
|
|
1952
|
+
.gdp-source-table tr.gdp-source-cursor .gdp-source-line-code::before,
|
|
1953
|
+
.gdp-source-virtual-row.gdp-source-cursor .gdp-source-virtual-line-code::before {
|
|
1954
|
+
content: "";
|
|
1955
|
+
display: inline-block;
|
|
1956
|
+
width: 2px;
|
|
1957
|
+
height: 1.05em;
|
|
1958
|
+
margin: 0 8px 0 -8px;
|
|
1959
|
+
vertical-align: -0.16em;
|
|
1960
|
+
background: var(--scope-accent);
|
|
1961
|
+
}
|
|
1757
1962
|
.gdp-source-table tr.gdp-source-line-target .gdp-source-line-number,
|
|
1758
1963
|
.gdp-source-table tr.gdp-source-line-target .gdp-source-line-code,
|
|
1759
1964
|
.gdp-source-virtual-row.gdp-source-line-target {
|
|
@@ -1907,6 +2112,9 @@ table.d2h-diff-table tr.gdp-diff-line-target > td:first-child {
|
|
|
1907
2112
|
border-color: var(--accent);
|
|
1908
2113
|
color: var(--accent);
|
|
1909
2114
|
}
|
|
2115
|
+
.gdp-source-virtual-copy:hover {
|
|
2116
|
+
border-color: var(--border);
|
|
2117
|
+
}
|
|
1910
2118
|
.gdp-source-virtual-scroller {
|
|
1911
2119
|
position: relative;
|
|
1912
2120
|
overflow: auto;
|
|
@@ -3055,7 +3263,7 @@ body.gdp-file-detail-page #empty {
|
|
|
3055
3263
|
font-style: italic;
|
|
3056
3264
|
}
|
|
3057
3265
|
|
|
3058
|
-
/* ===== Media (binary image/video) ===== */
|
|
3266
|
+
/* ===== Media (binary image/video/audio) ===== */
|
|
3059
3267
|
.gdp-media {
|
|
3060
3268
|
display: flex;
|
|
3061
3269
|
gap: 16px;
|
|
@@ -3091,7 +3299,8 @@ body.gdp-file-detail-page #empty {
|
|
|
3091
3299
|
color: var(--danger);
|
|
3092
3300
|
}
|
|
3093
3301
|
.gdp-media img,
|
|
3094
|
-
.gdp-media video
|
|
3302
|
+
.gdp-media video,
|
|
3303
|
+
.gdp-media audio {
|
|
3095
3304
|
max-width: 100%;
|
|
3096
3305
|
max-height: 70vh;
|
|
3097
3306
|
border: 1px solid var(--border);
|
|
@@ -3106,6 +3315,12 @@ body.gdp-file-detail-page #empty {
|
|
|
3106
3315
|
background-position: 0 0, 0 8px, 8px -8px, -8px 0;
|
|
3107
3316
|
object-fit: contain;
|
|
3108
3317
|
}
|
|
3318
|
+
.gdp-media audio {
|
|
3319
|
+
width: min(100%, 480px);
|
|
3320
|
+
max-height: none;
|
|
3321
|
+
padding: 8px;
|
|
3322
|
+
background-image: none;
|
|
3323
|
+
}
|
|
3109
3324
|
.gdp-media .media-empty {
|
|
3110
3325
|
width: 100%;
|
|
3111
3326
|
min-height: 120px;
|
package/web-src/routes.ts
CHANGED
|
@@ -19,9 +19,10 @@ export type AppRoute =
|
|
|
19
19
|
| { screen: 'repo'; ref: string; path: string; range: DiffRange }
|
|
20
20
|
| { screen: 'diff'; range: DiffRange; path?: string; line?: SourceLineTarget }
|
|
21
21
|
| { screen: 'file'; path: string; ref: string; range: DiffRange; view?: 'blob' | 'detail'; line?: SourceLineTarget }
|
|
22
|
+
| { screen: 'help'; range: DiffRange; lang: string; section: string }
|
|
22
23
|
| { screen: 'unknown'; reason: 'unknown-pathname' | 'missing-path'; rawPathname: string; rawSearch: string; range: DiffRange };
|
|
23
24
|
|
|
24
|
-
export const SPA_PATHS = ['/todif', '/todiff', '/file'] as const;
|
|
25
|
+
export const SPA_PATHS = ['/todif', '/todiff', '/file', '/help'] as const;
|
|
25
26
|
export const APP_ENTRY_PATHS = ['/', '/index.html'] as const;
|
|
26
27
|
|
|
27
28
|
export function assertNever(value: never): never {
|
|
@@ -89,6 +90,13 @@ export function parseRoute(pathname: string, search: string, fallbackRange: Diff
|
|
|
89
90
|
if (!path) return { screen: 'unknown', reason: 'missing-path', rawPathname: pathname, rawSearch: search, range };
|
|
90
91
|
return { screen: 'file', path, ref, range, view: target ? 'blob' : 'detail', ...(line ? { line } : {}) };
|
|
91
92
|
}
|
|
93
|
+
case '/help':
|
|
94
|
+
return {
|
|
95
|
+
screen: 'help',
|
|
96
|
+
range,
|
|
97
|
+
lang: params.get('lang') || 'en',
|
|
98
|
+
section: params.get('section') || 'keybindings',
|
|
99
|
+
};
|
|
92
100
|
default:
|
|
93
101
|
return { screen: 'unknown', reason: 'unknown-pathname', rawPathname: pathname, rawSearch: search, range };
|
|
94
102
|
}
|
|
@@ -119,6 +127,13 @@ export function buildRoute(route: AppRoute): string {
|
|
|
119
127
|
'&to=' + encodeURIComponent(route.range.to || 'worktree') +
|
|
120
128
|
(route.path ? '&path=' + encodeURIComponent(route.path) : '') +
|
|
121
129
|
(route.line ? '&line=' + encodeURIComponent(formatLineTarget(route.line)) : '');
|
|
130
|
+
case 'help': {
|
|
131
|
+
const params = new URLSearchParams();
|
|
132
|
+
if (route.lang && route.lang !== 'en') params.set('lang', route.lang);
|
|
133
|
+
if (route.section && route.section !== 'keybindings') params.set('section', route.section);
|
|
134
|
+
const qs = params.toString();
|
|
135
|
+
return '/help' + (qs ? '?' + qs : '');
|
|
136
|
+
}
|
|
122
137
|
case 'unknown':
|
|
123
138
|
return '/todif?from=' + encodeURIComponent(route.range.from || '') +
|
|
124
139
|
'&to=' + encodeURIComponent(route.range.to || 'worktree');
|
package/web-src/server/git.ts
CHANGED
|
@@ -18,8 +18,11 @@ export type GitTreeEntry = {
|
|
|
18
18
|
path: string;
|
|
19
19
|
type: 'tree' | 'blob' | 'commit';
|
|
20
20
|
children_omitted?: true;
|
|
21
|
+
children_omitted_reason?: 'ignored' | 'internal';
|
|
21
22
|
};
|
|
22
23
|
|
|
24
|
+
const WORKTREE_RECURSIVE_DEPTH_LIMIT = 32;
|
|
25
|
+
|
|
23
26
|
function run(args: string[], cwd: string): { code: number; stdout: string; stderr: string } {
|
|
24
27
|
const proc = Bun.spawnSync(args, { cwd, stdout: 'pipe', stderr: 'pipe' });
|
|
25
28
|
return {
|
|
@@ -156,24 +159,85 @@ function sortTreeEntries(entries: GitTreeEntry[]): GitTreeEntry[] {
|
|
|
156
159
|
});
|
|
157
160
|
}
|
|
158
161
|
|
|
159
|
-
function
|
|
162
|
+
function ignoredWorktreeDirectories(cwd: string, path: string): Set<string> {
|
|
160
163
|
const base = normalizeTreePath(path);
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
164
|
+
const args = ['git', '-c', 'core.quotepath=false', 'ls-files', '--others', '--ignored', '--exclude-standard', '--directory', '-z'];
|
|
165
|
+
if (base) args.push('--', `${base}/`);
|
|
166
|
+
const proc = Bun.spawnSync(args, {
|
|
167
|
+
cwd,
|
|
168
|
+
stdout: 'pipe',
|
|
169
|
+
stderr: 'ignore',
|
|
170
|
+
});
|
|
171
|
+
if (proc.exitCode !== 0) return new Set();
|
|
172
|
+
return new Set(new TextDecoder().decode(proc.stdout)
|
|
173
|
+
.split('\0')
|
|
174
|
+
.filter(entry => entry.endsWith('/'))
|
|
175
|
+
.map(entry => entry.replace(/\/+$/g, ''))
|
|
176
|
+
.filter(entry => entry && entry !== base));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function worktreeEntryFromDirent(base: string, dir: string, name: string, isDirectory: boolean, ignoredDirectories: Set<string>): GitTreeEntry {
|
|
180
|
+
const entryPath = base ? `${base}/${name}` : name;
|
|
181
|
+
const type = isDirectory
|
|
182
|
+
? hasDotGitEntry(join(dir, name)) ? 'commit' as const : 'tree' as const
|
|
183
|
+
: 'blob' as const;
|
|
184
|
+
return type === 'tree' && (name === '.git' || ignoredDirectories.has(entryPath))
|
|
185
|
+
? {
|
|
186
|
+
name,
|
|
170
187
|
path: entryPath,
|
|
171
188
|
type,
|
|
172
|
-
|
|
173
|
-
|
|
189
|
+
children_omitted: true,
|
|
190
|
+
children_omitted_reason: name === '.git' ? 'internal' : 'ignored',
|
|
191
|
+
}
|
|
192
|
+
: { name, path: entryPath, type };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function worktreeFilesystemEntries(cwd: string, path: string, recursive: boolean): GitTreeEntry[] {
|
|
196
|
+
const base = normalizeTreePath(path);
|
|
197
|
+
const root = join(cwd, base);
|
|
198
|
+
const ignoredDirectories = ignoredWorktreeDirectories(cwd, base);
|
|
199
|
+
let directEntries: GitTreeEntry[];
|
|
200
|
+
try {
|
|
201
|
+
const dirents = readdirSync(root, { withFileTypes: true });
|
|
202
|
+
directEntries = sortTreeEntries(dirents
|
|
203
|
+
.map(entry => worktreeEntryFromDirent(base, root, entry.name, entry.isDirectory(), ignoredDirectories)));
|
|
174
204
|
} catch {
|
|
175
205
|
return [];
|
|
176
206
|
}
|
|
207
|
+
if (!recursive) return directEntries;
|
|
208
|
+
|
|
209
|
+
const fileEntries: GitTreeEntry[] = [];
|
|
210
|
+
const walk = (dir: string, prefix: string, depth: number) => {
|
|
211
|
+
if (depth >= WORKTREE_RECURSIVE_DEPTH_LIMIT) return;
|
|
212
|
+
let entries;
|
|
213
|
+
try {
|
|
214
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
215
|
+
} catch {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
for (const entry of entries) {
|
|
219
|
+
const entryPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
220
|
+
const full = join(dir, entry.name);
|
|
221
|
+
if (entry.isDirectory()) {
|
|
222
|
+
if (entry.name === '.git' || ignoredDirectories.has(entryPath)) {
|
|
223
|
+
fileEntries.push({
|
|
224
|
+
name: entry.name,
|
|
225
|
+
path: entryPath,
|
|
226
|
+
type: 'tree',
|
|
227
|
+
children_omitted: true,
|
|
228
|
+
children_omitted_reason: entry.name === '.git' ? 'internal' : 'ignored',
|
|
229
|
+
});
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (hasDotGitEntry(full)) continue;
|
|
233
|
+
walk(full, entryPath, depth + 1);
|
|
234
|
+
} else if (entry.isFile() || entry.isSymbolicLink()) {
|
|
235
|
+
fileEntries.push({ name: entry.name, path: entryPath, type: 'blob' });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
walk(root, base, 0);
|
|
240
|
+
return combineDirectAndRecursiveFiles(directEntries, fileEntries.sort((a, b) => a.path.localeCompare(b.path)));
|
|
177
241
|
}
|
|
178
242
|
|
|
179
243
|
function hasDotGitEntry(dir: string): boolean {
|
|
@@ -185,20 +249,6 @@ function hasDotGitEntry(dir: string): boolean {
|
|
|
185
249
|
}
|
|
186
250
|
}
|
|
187
251
|
|
|
188
|
-
function worktreeRecursiveFiles(cwd: string, path: string): GitTreeEntry[] {
|
|
189
|
-
const base = normalizeTreePath(path);
|
|
190
|
-
const trackedArgs = ['git', '-c', 'core.quotepath=false', 'ls-files', '-z'];
|
|
191
|
-
if (base) trackedArgs.push('--', `${base}/`);
|
|
192
|
-
const tracked = run(trackedArgs, cwd);
|
|
193
|
-
const paths = tracked.code === 0 ? tracked.stdout.split('\0').filter(Boolean) : [];
|
|
194
|
-
paths.push(...untracked(cwd, base));
|
|
195
|
-
return [...new Set(paths)].filter(Boolean).sort().map((entryPath) => ({
|
|
196
|
-
name: entryPath.split('/').pop() || entryPath,
|
|
197
|
-
path: entryPath,
|
|
198
|
-
type: 'blob' as const,
|
|
199
|
-
}));
|
|
200
|
-
}
|
|
201
|
-
|
|
202
252
|
function gitTreeEntries(ref: string, path: string, cwd: string, recursive: boolean): { code: number; entries: GitTreeEntry[]; stderr: string } {
|
|
203
253
|
const base = normalizeTreePath(path);
|
|
204
254
|
const args = ['git', '-c', 'core.quotepath=false', 'ls-tree'];
|
|
@@ -227,31 +277,6 @@ function combineDirectAndRecursiveFiles(directEntries: GitTreeEntry[], fileEntri
|
|
|
227
277
|
];
|
|
228
278
|
}
|
|
229
279
|
|
|
230
|
-
function ignoredWorktreePaths(cwd: string, path: string): Set<string> {
|
|
231
|
-
const base = normalizeTreePath(path);
|
|
232
|
-
const args = ['git', '-c', 'core.quotepath=false', 'status', '--ignored', '--porcelain=v1', '-z', '--'];
|
|
233
|
-
if (base) args.push(`${base}/`);
|
|
234
|
-
const res = run(args, cwd);
|
|
235
|
-
const ignored = new Set<string>();
|
|
236
|
-
if (res.code !== 0) return ignored;
|
|
237
|
-
const records = res.stdout.split('\0').filter(Boolean);
|
|
238
|
-
for (let i = 0; i < records.length; i++) {
|
|
239
|
-
const rec = records[i];
|
|
240
|
-
if (!rec.startsWith('!! ')) continue;
|
|
241
|
-
const path = rec.slice(3).replace(/\/+$/g, '');
|
|
242
|
-
if (path) ignored.add(path);
|
|
243
|
-
}
|
|
244
|
-
return ignored;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function annotateOmittedWorktreeChildren(entries: GitTreeEntry[], ignoredPaths: Set<string>): GitTreeEntry[] {
|
|
248
|
-
return entries.map((entry) => {
|
|
249
|
-
return entry.type === 'tree' && ignoredPaths.has(entry.path)
|
|
250
|
-
? { ...entry, children_omitted: true }
|
|
251
|
-
: entry;
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
|
|
255
280
|
export function worktreeEntries(cwd: string, path: string): GitTreeEntry[] {
|
|
256
281
|
return listTree('worktree', path, cwd).entries;
|
|
257
282
|
}
|
|
@@ -276,12 +301,7 @@ export function listTree(
|
|
|
276
301
|
): { code: number; entries: GitTreeEntry[]; stderr: string } {
|
|
277
302
|
const base = normalizeTreePath(path);
|
|
278
303
|
if (ref === 'worktree') {
|
|
279
|
-
|
|
280
|
-
const ignoredPaths = ignoredWorktreePaths(cwd, base);
|
|
281
|
-
const annotatedDirectEntries = annotateOmittedWorktreeChildren(directEntries, ignoredPaths);
|
|
282
|
-
if (!options.recursive) return { code: 0, entries: annotatedDirectEntries, stderr: '' };
|
|
283
|
-
const recursiveEntries = worktreeRecursiveFiles(cwd, base);
|
|
284
|
-
return { code: 0, entries: combineDirectAndRecursiveFiles(annotatedDirectEntries, recursiveEntries), stderr: '' };
|
|
304
|
+
return { code: 0, entries: worktreeFilesystemEntries(cwd, base, !!options.recursive), stderr: '' };
|
|
285
305
|
}
|
|
286
306
|
|
|
287
307
|
const direct = gitTreeEntries(ref, base, cwd, false);
|
|
@@ -180,6 +180,7 @@ function guessMediaKind(path: string) {
|
|
|
180
180
|
const ext = extname(path).slice(1).toLowerCase();
|
|
181
181
|
if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'avif', 'bmp', 'ico'].includes(ext)) return 'image';
|
|
182
182
|
if (['mp4', 'webm', 'mov'].includes(ext)) return 'video';
|
|
183
|
+
if (['mp3', 'wav', 'ogg', 'flac', 'm4a', 'aac', 'opus'].includes(ext)) return 'audio';
|
|
183
184
|
return null;
|
|
184
185
|
}
|
|
185
186
|
|
|
@@ -641,7 +642,9 @@ function rawFileHeaders(path: string, size: number | null = null): HeadersInit {
|
|
|
641
642
|
const mime: Record<string, string> = {
|
|
642
643
|
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif',
|
|
643
644
|
'.webp': 'image/webp', '.svg': 'image/svg+xml', '.mp4': 'video/mp4', '.webm': 'video/webm',
|
|
644
|
-
'.pdf': 'application/pdf',
|
|
645
|
+
'.mov': 'video/quicktime', '.pdf': 'application/pdf',
|
|
646
|
+
'.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.ogg': 'audio/ogg', '.flac': 'audio/flac',
|
|
647
|
+
'.m4a': 'audio/mp4', '.aac': 'audio/aac', '.opus': 'audio/ogg',
|
|
645
648
|
};
|
|
646
649
|
const headers: Record<string, string> = {
|
|
647
650
|
'Content-Type': mime[extname(path).toLowerCase()] || 'application/octet-stream',
|