@youtyan/code-viewer 0.1.13 → 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 +105 -17
- package/web/style.css +37 -11
- package/web-src/server/git.ts +77 -57
- package/web-src/server/preview.ts +4 -1
- package/web-src/types.ts +1 -0
package/package.json
CHANGED
package/web/app.js
CHANGED
|
@@ -6764,6 +6764,8 @@
|
|
|
6764
6764
|
let REPO_SIDEBAR_REF = null;
|
|
6765
6765
|
let REPO_SIDEBAR_LOAD_REF = null;
|
|
6766
6766
|
let REPO_SIDEBAR_LOAD = null;
|
|
6767
|
+
let SIDEBAR_FILES = [];
|
|
6768
|
+
let SIDEBAR_ON_FILE_CLICK;
|
|
6767
6769
|
let PENDING_G_SCOPE = null;
|
|
6768
6770
|
let PENDING_G_UNTIL = 0;
|
|
6769
6771
|
let SOURCE_CURSOR = null;
|
|
@@ -7239,8 +7241,10 @@
|
|
|
7239
7241
|
}
|
|
7240
7242
|
if (f2.type === "tree") {
|
|
7241
7243
|
node.explicit = true;
|
|
7242
|
-
if (f2.children_omitted === true)
|
|
7244
|
+
if (f2.children_omitted === true) {
|
|
7243
7245
|
node.children_omitted = true;
|
|
7246
|
+
node.children_omitted_reason = f2.children_omitted_reason;
|
|
7247
|
+
}
|
|
7244
7248
|
continue;
|
|
7245
7249
|
}
|
|
7246
7250
|
node.files.push(f2);
|
|
@@ -7283,12 +7287,18 @@
|
|
|
7283
7287
|
li.dataset.explicit = "true";
|
|
7284
7288
|
if (dir.children_omitted) {
|
|
7285
7289
|
li.classList.add("children-omitted");
|
|
7286
|
-
li.
|
|
7290
|
+
li.classList.add(dir.children_omitted_reason === "ignored" ? "children-omitted-ignored" : "children-omitted-internal");
|
|
7291
|
+
li.title = dir.children_omitted_reason === "ignored" ? "Ignored directory: open the detail pane to browse its contents" : "Internal Git metadata is not browsed";
|
|
7287
7292
|
}
|
|
7288
7293
|
li.style.setProperty("--lvl-pad", 12 + depth * 14 + "px");
|
|
7289
7294
|
const chev = document.createElement("span");
|
|
7290
|
-
|
|
7291
|
-
|
|
7295
|
+
if (dir.children_omitted) {
|
|
7296
|
+
chev.className = "chev-spacer";
|
|
7297
|
+
chev.setAttribute("aria-hidden", "true");
|
|
7298
|
+
} else {
|
|
7299
|
+
chev.className = "chev";
|
|
7300
|
+
setChevronIcon(chev);
|
|
7301
|
+
}
|
|
7292
7302
|
li.appendChild(chev);
|
|
7293
7303
|
const dirIcon = document.createElement("span");
|
|
7294
7304
|
dirIcon.className = "dir-icon";
|
|
@@ -7302,9 +7312,9 @@
|
|
|
7302
7312
|
label.appendChild(dn);
|
|
7303
7313
|
if (dir.children_omitted) {
|
|
7304
7314
|
const omitted = document.createElement("span");
|
|
7305
|
-
omitted.className = "dir-omitted";
|
|
7306
|
-
omitted.textContent = "
|
|
7307
|
-
omitted.title = "
|
|
7315
|
+
omitted.className = "dir-omitted " + (dir.children_omitted_reason === "ignored" ? "dir-omitted-ignored" : "dir-omitted-internal");
|
|
7316
|
+
omitted.textContent = dir.children_omitted_reason === "ignored" ? "ignored" : "private";
|
|
7317
|
+
omitted.title = dir.children_omitted_reason === "ignored" ? "Tree expansion is skipped, but the directory detail can be opened" : "This directory cannot be opened from the browser";
|
|
7308
7318
|
label.appendChild(omitted);
|
|
7309
7319
|
}
|
|
7310
7320
|
li.appendChild(label);
|
|
@@ -7329,12 +7339,24 @@
|
|
|
7329
7339
|
STATE.collapsedDirs.delete(dir.path);
|
|
7330
7340
|
localStorage.setItem("gdp:collapsed-dirs", JSON.stringify([...STATE.collapsedDirs]));
|
|
7331
7341
|
};
|
|
7332
|
-
|
|
7333
|
-
|
|
7342
|
+
if (!dir.children_omitted) {
|
|
7343
|
+
chev.addEventListener("click", toggleDir);
|
|
7344
|
+
dirIcon.addEventListener("click", toggleDir);
|
|
7345
|
+
}
|
|
7334
7346
|
if (onFileClick) {
|
|
7335
7347
|
li.addEventListener("click", (e2) => {
|
|
7336
7348
|
e2.stopPropagation();
|
|
7337
|
-
|
|
7349
|
+
if (dir.children_omitted_reason === "internal")
|
|
7350
|
+
return;
|
|
7351
|
+
if (dir.children_omitted_reason !== "internal") {
|
|
7352
|
+
onFileClick({
|
|
7353
|
+
path: dir.path,
|
|
7354
|
+
display_path: dir.path,
|
|
7355
|
+
type: "tree",
|
|
7356
|
+
children_omitted: dir.children_omitted,
|
|
7357
|
+
children_omitted_reason: dir.children_omitted_reason
|
|
7358
|
+
});
|
|
7359
|
+
}
|
|
7338
7360
|
focusSidebarPanel();
|
|
7339
7361
|
});
|
|
7340
7362
|
} else {
|
|
@@ -7416,6 +7438,8 @@
|
|
|
7416
7438
|
ul.innerHTML = "";
|
|
7417
7439
|
ul.classList.toggle("tree", STATE.sbView === "tree");
|
|
7418
7440
|
STATE.files = files;
|
|
7441
|
+
SIDEBAR_FILES = files;
|
|
7442
|
+
SIDEBAR_ON_FILE_CLICK = onFileClick;
|
|
7419
7443
|
if (!onFileClick)
|
|
7420
7444
|
REPO_SIDEBAR_REF = null;
|
|
7421
7445
|
if (STATE.sbView === "tree") {
|
|
@@ -7550,13 +7574,43 @@
|
|
|
7550
7574
|
card.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
7551
7575
|
}
|
|
7552
7576
|
}
|
|
7553
|
-
function
|
|
7577
|
+
function sidebarAncestorDirs(path) {
|
|
7578
|
+
const parts = path.split("/").filter(Boolean);
|
|
7579
|
+
const dirs = [];
|
|
7580
|
+
for (let i2 = 1;i2 < parts.length; i2++)
|
|
7581
|
+
dirs.push(parts.slice(0, i2).join("/"));
|
|
7582
|
+
return dirs;
|
|
7583
|
+
}
|
|
7584
|
+
function expandSidebarAncestors(path) {
|
|
7585
|
+
if (STATE.sbView !== "tree")
|
|
7586
|
+
return;
|
|
7587
|
+
let changed = false;
|
|
7588
|
+
for (const dir of sidebarAncestorDirs(path)) {
|
|
7589
|
+
if (STATE.collapsedDirs.delete(dir))
|
|
7590
|
+
changed = true;
|
|
7591
|
+
const row = document.querySelector('#filelist .tree-dir[data-dirpath="' + CSS.escape(dir) + '"]');
|
|
7592
|
+
row?.classList.remove("collapsed");
|
|
7593
|
+
const icon = row?.querySelector(".dir-icon");
|
|
7594
|
+
if (icon)
|
|
7595
|
+
setFolderIcon(icon, false);
|
|
7596
|
+
}
|
|
7597
|
+
if (changed)
|
|
7598
|
+
localStorage.setItem("gdp:collapsed-dirs", JSON.stringify([...STATE.collapsedDirs]));
|
|
7599
|
+
}
|
|
7600
|
+
function markActive(path, options = {}) {
|
|
7554
7601
|
STATE.activeFile = path;
|
|
7602
|
+
if (options.reveal && STATE.sbView === "tree")
|
|
7603
|
+
expandSidebarAncestors(path);
|
|
7555
7604
|
$$("#filelist li").forEach((li) => {
|
|
7556
7605
|
const itemPath = li.dataset.path || li.dataset.dirpath;
|
|
7557
7606
|
if (itemPath)
|
|
7558
7607
|
li.classList.toggle("active", itemPath === path);
|
|
7559
7608
|
});
|
|
7609
|
+
if (options.reveal) {
|
|
7610
|
+
const active = document.querySelector("#filelist li.active[data-path], #filelist .tree-dir.active[data-dirpath]");
|
|
7611
|
+
if (active)
|
|
7612
|
+
requestAnimationFrame(() => scrollSidebarItemIntoView(active));
|
|
7613
|
+
}
|
|
7560
7614
|
}
|
|
7561
7615
|
function applyViewedState() {
|
|
7562
7616
|
$$("#filelist li[data-path]").forEach((li) => {
|
|
@@ -8239,7 +8293,8 @@
|
|
|
8239
8293
|
path: entry.path,
|
|
8240
8294
|
display_path: entry.path,
|
|
8241
8295
|
type: entry.type,
|
|
8242
|
-
children_omitted: entry.children_omitted
|
|
8296
|
+
children_omitted: entry.children_omitted,
|
|
8297
|
+
children_omitted_reason: entry.children_omitted_reason
|
|
8243
8298
|
}));
|
|
8244
8299
|
renderSidebar(files, (file) => {
|
|
8245
8300
|
if (file.type === "tree") {
|
|
@@ -8266,7 +8321,7 @@
|
|
|
8266
8321
|
return load2;
|
|
8267
8322
|
}
|
|
8268
8323
|
function activateRepoSidebarPath(currentPath) {
|
|
8269
|
-
markActive(currentPath);
|
|
8324
|
+
markActive(currentPath, { reveal: true });
|
|
8270
8325
|
applyFilter();
|
|
8271
8326
|
}
|
|
8272
8327
|
function createPlaceholder(f2) {
|
|
@@ -9213,6 +9268,8 @@
|
|
|
9213
9268
|
function sourceDisplayKind(path) {
|
|
9214
9269
|
if (isVideo(path))
|
|
9215
9270
|
return "video";
|
|
9271
|
+
if (isAudio(path))
|
|
9272
|
+
return "audio";
|
|
9216
9273
|
if (isImage(path))
|
|
9217
9274
|
return "image";
|
|
9218
9275
|
if (/\.pdf$/i.test(path))
|
|
@@ -9259,10 +9316,28 @@
|
|
|
9259
9316
|
return "MP4 video";
|
|
9260
9317
|
if (ext === "webm")
|
|
9261
9318
|
return "WebM video";
|
|
9319
|
+
if (ext === "mp3")
|
|
9320
|
+
return "MP3 audio";
|
|
9321
|
+
if (ext === "wav")
|
|
9322
|
+
return "WAV audio";
|
|
9323
|
+
if (ext === "ogg")
|
|
9324
|
+
return "Ogg audio";
|
|
9325
|
+
if (ext === "flac")
|
|
9326
|
+
return "FLAC audio";
|
|
9327
|
+
if (ext === "m4a")
|
|
9328
|
+
return "M4A audio";
|
|
9329
|
+
if (ext === "aac")
|
|
9330
|
+
return "AAC audio";
|
|
9331
|
+
if (ext === "opus")
|
|
9332
|
+
return "Opus audio";
|
|
9333
|
+
if (ext === "mid" || ext === "midi")
|
|
9334
|
+
return "MIDI file";
|
|
9262
9335
|
if (mime?.startsWith("image/"))
|
|
9263
9336
|
return "Image";
|
|
9264
9337
|
if (mime?.startsWith("video/"))
|
|
9265
9338
|
return "Video";
|
|
9339
|
+
if (mime?.startsWith("audio/"))
|
|
9340
|
+
return "Audio";
|
|
9266
9341
|
if (mime === "application/pdf")
|
|
9267
9342
|
return "PDF document";
|
|
9268
9343
|
if (fallback === "unsupported file")
|
|
@@ -9734,6 +9809,12 @@
|
|
|
9734
9809
|
video.controls = true;
|
|
9735
9810
|
video.preload = "metadata";
|
|
9736
9811
|
view.appendChild(video);
|
|
9812
|
+
} else if (mediaKind === "audio") {
|
|
9813
|
+
const audio = document.createElement("audio");
|
|
9814
|
+
audio.src = url;
|
|
9815
|
+
audio.controls = true;
|
|
9816
|
+
audio.preload = "metadata";
|
|
9817
|
+
view.appendChild(audio);
|
|
9737
9818
|
} else if (mediaKind === "pdf") {
|
|
9738
9819
|
const frame = document.createElement("iframe");
|
|
9739
9820
|
frame.src = url;
|
|
@@ -9900,7 +9981,7 @@
|
|
|
9900
9981
|
renderSourceUnsupported(card, target);
|
|
9901
9982
|
return;
|
|
9902
9983
|
}
|
|
9903
|
-
if (displayKind === "image" || displayKind === "video" || displayKind === "pdf") {
|
|
9984
|
+
if (displayKind === "image" || displayKind === "video" || displayKind === "audio" || displayKind === "pdf") {
|
|
9904
9985
|
if (req !== SOURCE_REQ_SEQ || !sourceTargetsEqual(sourceTargetFromRoute(), target))
|
|
9905
9986
|
return;
|
|
9906
9987
|
finishSourceLoad(req);
|
|
@@ -10296,9 +10377,10 @@
|
|
|
10296
10377
|
b2.addEventListener("scroll", () => mirror(b2, a2), { passive: true });
|
|
10297
10378
|
});
|
|
10298
10379
|
}
|
|
10299
|
-
const MEDIA_RE = /\.(png|jpe?g|gif|webp|svg|avif|bmp|ico|mp4|webm|mov)(\?.*)?$/i;
|
|
10380
|
+
const MEDIA_RE = /\.(png|jpe?g|gif|webp|svg|avif|bmp|ico|mp4|webm|mov|mp3|wav|ogg|flac|m4a|aac|opus)(\?.*)?$/i;
|
|
10300
10381
|
const IMAGE_RE = /\.(png|jpe?g|gif|webp|svg|avif|bmp|ico)(\?.*)?$/i;
|
|
10301
10382
|
const VIDEO_RE = /\.(mp4|webm|mov)$/i;
|
|
10383
|
+
const AUDIO_RE = /\.(mp3|wav|ogg|flac|m4a|aac|opus)$/i;
|
|
10302
10384
|
function isMedia(p2) {
|
|
10303
10385
|
return MEDIA_RE.test(p2);
|
|
10304
10386
|
}
|
|
@@ -10308,6 +10390,9 @@
|
|
|
10308
10390
|
function isVideo(p2) {
|
|
10309
10391
|
return VIDEO_RE.test(p2);
|
|
10310
10392
|
}
|
|
10393
|
+
function isAudio(p2) {
|
|
10394
|
+
return AUDIO_RE.test(p2);
|
|
10395
|
+
}
|
|
10311
10396
|
function fileURL(path, ref) {
|
|
10312
10397
|
return "/_file?path=" + encodeURIComponent(path) + "&ref=" + ref;
|
|
10313
10398
|
}
|
|
@@ -10316,6 +10401,9 @@
|
|
|
10316
10401
|
if (isVideo(path)) {
|
|
10317
10402
|
return '<video src="' + url + '" controls preload="metadata"></video>';
|
|
10318
10403
|
}
|
|
10404
|
+
if (isAudio(path)) {
|
|
10405
|
+
return '<audio src="' + url + '" controls preload="metadata"></audio>';
|
|
10406
|
+
}
|
|
10319
10407
|
return '<img src="' + url + '" alt="" loading="lazy">';
|
|
10320
10408
|
}
|
|
10321
10409
|
function enhanceMediaCard(file, card) {
|
|
@@ -10409,8 +10497,8 @@
|
|
|
10409
10497
|
b2.addEventListener("click", () => {
|
|
10410
10498
|
STATE.sbView = b2.dataset.view || "tree";
|
|
10411
10499
|
localStorage.setItem("gdp:sbview", STATE.sbView);
|
|
10412
|
-
if (
|
|
10413
|
-
renderSidebar(
|
|
10500
|
+
if (SIDEBAR_FILES.length)
|
|
10501
|
+
renderSidebar(SIDEBAR_FILES, SIDEBAR_ON_FILE_CLICK);
|
|
10414
10502
|
});
|
|
10415
10503
|
});
|
|
10416
10504
|
$("#sb-expand-all").addEventListener("click", () => setAllSidebarDirsCollapsed(false));
|
package/web/style.css
CHANGED
|
@@ -807,16 +807,14 @@ html, body {
|
|
|
807
807
|
z-index: 30;
|
|
808
808
|
}
|
|
809
809
|
body[data-focus-scope="sidebar"] #sidebar {
|
|
810
|
-
background:
|
|
811
|
-
outline:
|
|
812
|
-
|
|
813
|
-
box-shadow: inset -3px 0 0 var(--scope-accent);
|
|
810
|
+
background: var(--bg-soft);
|
|
811
|
+
outline: none;
|
|
812
|
+
box-shadow: none;
|
|
814
813
|
}
|
|
815
814
|
body[data-focus-scope="main"] #content {
|
|
816
|
-
background:
|
|
817
|
-
outline:
|
|
818
|
-
|
|
819
|
-
box-shadow: inset 3px 0 0 var(--scope-accent);
|
|
815
|
+
background: var(--bg);
|
|
816
|
+
outline: none;
|
|
817
|
+
box-shadow: none;
|
|
820
818
|
}
|
|
821
819
|
.sb-head {
|
|
822
820
|
display: flex; justify-content: space-between; align-items: center;
|
|
@@ -1102,6 +1100,12 @@ body.gdp-help-page #content {
|
|
|
1102
1100
|
#filelist.tree .tree-dir.children-omitted .dir-label {
|
|
1103
1101
|
border-bottom: 1px dashed var(--border-strong);
|
|
1104
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
|
+
}
|
|
1105
1109
|
#filelist.tree .tree-dir .chev {
|
|
1106
1110
|
display: flex;
|
|
1107
1111
|
width: 16px;
|
|
@@ -1120,6 +1124,11 @@ body.gdp-help-page #content {
|
|
|
1120
1124
|
width: 12px;
|
|
1121
1125
|
height: 12px;
|
|
1122
1126
|
}
|
|
1127
|
+
#filelist.tree .tree-dir .chev-spacer {
|
|
1128
|
+
width: 16px;
|
|
1129
|
+
height: 16px;
|
|
1130
|
+
display: inline-block;
|
|
1131
|
+
}
|
|
1123
1132
|
#filelist.tree .tree-dir .dir-label {
|
|
1124
1133
|
min-width: 0;
|
|
1125
1134
|
display: flex;
|
|
@@ -1140,7 +1149,7 @@ body.gdp-help-page #content {
|
|
|
1140
1149
|
}
|
|
1141
1150
|
#filelist.tree .tree-dir .dir-omitted {
|
|
1142
1151
|
flex: 0 0 auto;
|
|
1143
|
-
max-width:
|
|
1152
|
+
max-width: 62px;
|
|
1144
1153
|
overflow: hidden;
|
|
1145
1154
|
text-overflow: ellipsis;
|
|
1146
1155
|
border: 1px solid var(--border-muted);
|
|
@@ -1151,6 +1160,16 @@ body.gdp-help-page #content {
|
|
|
1151
1160
|
line-height: 15px;
|
|
1152
1161
|
text-transform: uppercase;
|
|
1153
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
|
+
}
|
|
1154
1173
|
#filelist.tree .tree-dir .dir-icon {
|
|
1155
1174
|
color: #54aeff;
|
|
1156
1175
|
flex-shrink: 0;
|
|
@@ -3244,7 +3263,7 @@ body.gdp-file-detail-page #empty {
|
|
|
3244
3263
|
font-style: italic;
|
|
3245
3264
|
}
|
|
3246
3265
|
|
|
3247
|
-
/* ===== Media (binary image/video) ===== */
|
|
3266
|
+
/* ===== Media (binary image/video/audio) ===== */
|
|
3248
3267
|
.gdp-media {
|
|
3249
3268
|
display: flex;
|
|
3250
3269
|
gap: 16px;
|
|
@@ -3280,7 +3299,8 @@ body.gdp-file-detail-page #empty {
|
|
|
3280
3299
|
color: var(--danger);
|
|
3281
3300
|
}
|
|
3282
3301
|
.gdp-media img,
|
|
3283
|
-
.gdp-media video
|
|
3302
|
+
.gdp-media video,
|
|
3303
|
+
.gdp-media audio {
|
|
3284
3304
|
max-width: 100%;
|
|
3285
3305
|
max-height: 70vh;
|
|
3286
3306
|
border: 1px solid var(--border);
|
|
@@ -3295,6 +3315,12 @@ body.gdp-file-detail-page #empty {
|
|
|
3295
3315
|
background-position: 0 0, 0 8px, 8px -8px, -8px 0;
|
|
3296
3316
|
object-fit: contain;
|
|
3297
3317
|
}
|
|
3318
|
+
.gdp-media audio {
|
|
3319
|
+
width: min(100%, 480px);
|
|
3320
|
+
max-height: none;
|
|
3321
|
+
padding: 8px;
|
|
3322
|
+
background-image: none;
|
|
3323
|
+
}
|
|
3298
3324
|
.gdp-media .media-empty {
|
|
3299
3325
|
width: 100%;
|
|
3300
3326
|
min-height: 120px;
|
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',
|