@youtyan/code-viewer 0.1.24 → 0.1.26
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 +3 -0
- package/dist/code-viewer.js +436 -47
- package/package.json +1 -1
- package/web/app.js +516 -75
- package/web/index.html +3 -1
- package/web/style.css +168 -3
package/dist/code-viewer.js
CHANGED
|
@@ -6,14 +6,17 @@ import {
|
|
|
6
6
|
constants,
|
|
7
7
|
existsSync as existsSync3,
|
|
8
8
|
lstatSync as lstatSync3,
|
|
9
|
+
mkdirSync,
|
|
9
10
|
openSync,
|
|
10
11
|
readFileSync as readFileSync2,
|
|
11
12
|
realpathSync,
|
|
13
|
+
renameSync,
|
|
12
14
|
statSync,
|
|
13
15
|
unlinkSync,
|
|
14
16
|
watch,
|
|
15
17
|
writeFileSync
|
|
16
18
|
} from "node:fs";
|
|
19
|
+
import { homedir } from "node:os";
|
|
17
20
|
import { basename as basename2, dirname as dirname2, extname, join as join4, relative } from "node:path";
|
|
18
21
|
|
|
19
22
|
// web-src/routes.ts
|
|
@@ -313,6 +316,13 @@ function objectByteSize(oid, cwd) {
|
|
|
313
316
|
stderr: res.stderr
|
|
314
317
|
};
|
|
315
318
|
}
|
|
319
|
+
function lastCommitDateForPath(ref, path, cwd) {
|
|
320
|
+
const args = ["git", "log", "-1", "--format=%cI", ref, "--", path];
|
|
321
|
+
const res = run(args, cwd);
|
|
322
|
+
if (res.code !== 0)
|
|
323
|
+
return null;
|
|
324
|
+
return res.stdout.trim() || null;
|
|
325
|
+
}
|
|
316
326
|
function objectId(ref, path, cwd) {
|
|
317
327
|
const res = run(["git", "rev-parse", "--verify", `${ref}:${path}`], cwd);
|
|
318
328
|
const oid = res.stdout.trim();
|
|
@@ -561,7 +571,13 @@ function omittedWorktreeDirectoryReason(name, omitDirNames) {
|
|
|
561
571
|
return "internal";
|
|
562
572
|
return omitDirNames.has(name) ? "heavy" : undefined;
|
|
563
573
|
}
|
|
564
|
-
function worktreeEntryFromDirent(base, dir, name, isDirectory, omitDirNames) {
|
|
574
|
+
function worktreeEntryFromDirent(base, dir, name, isDirectory, omitDirNames, excludeNames) {
|
|
575
|
+
if (excludeNames.has(name.toLowerCase()))
|
|
576
|
+
return {
|
|
577
|
+
name,
|
|
578
|
+
path: "",
|
|
579
|
+
type: isDirectory ? "tree" : "blob"
|
|
580
|
+
};
|
|
565
581
|
const entryPath = base ? `${base}/${name}` : name;
|
|
566
582
|
const type = isDirectory ? hasDotGitEntry(join2(dir, name)) ? "commit" : "tree" : "blob";
|
|
567
583
|
const omittedReason = type === "tree" ? omittedWorktreeDirectoryReason(name, omitDirNames) : undefined;
|
|
@@ -573,14 +589,15 @@ function worktreeEntryFromDirent(base, dir, name, isDirectory, omitDirNames) {
|
|
|
573
589
|
children_omitted_reason: omittedReason
|
|
574
590
|
} : { name, path: entryPath, type };
|
|
575
591
|
}
|
|
576
|
-
function worktreeFilesystemEntries(cwd, path, recursive, omitDirNames = DEFAULT_WORKTREE_OMIT_DIR_NAMES) {
|
|
592
|
+
function worktreeFilesystemEntries(cwd, path, recursive, omitDirNames = DEFAULT_WORKTREE_OMIT_DIR_NAMES, excludeNames = []) {
|
|
577
593
|
const base = normalizeTreePath(path);
|
|
578
594
|
const root = join2(cwd, base);
|
|
579
595
|
const omitDirNameSet = new Set(omitDirNames);
|
|
596
|
+
const excludeNameSet = new Set(excludeNames.map((name) => name.toLowerCase()));
|
|
580
597
|
let directEntries;
|
|
581
598
|
try {
|
|
582
599
|
const dirents = readdirSync(root, { withFileTypes: true });
|
|
583
|
-
directEntries = sortTreeEntries(dirents.map((entry) => worktreeEntryFromDirent(base, root, entry.name, entry.isDirectory(), omitDirNameSet)));
|
|
600
|
+
directEntries = sortTreeEntries(dirents.map((entry) => worktreeEntryFromDirent(base, root, entry.name, entry.isDirectory(), omitDirNameSet, excludeNameSet)).filter((entry) => entry.path));
|
|
584
601
|
} catch {
|
|
585
602
|
return [];
|
|
586
603
|
}
|
|
@@ -617,6 +634,8 @@ function worktreeFilesystemEntries(cwd, path, recursive, omitDirNames = DEFAULT_
|
|
|
617
634
|
return;
|
|
618
635
|
}
|
|
619
636
|
for (const entry of entries) {
|
|
637
|
+
if (excludeNameSet.has(entry.name.toLowerCase()))
|
|
638
|
+
continue;
|
|
620
639
|
const entryPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
621
640
|
const full = join2(dir, entry.name);
|
|
622
641
|
if (entry.isDirectory()) {
|
|
@@ -692,15 +711,12 @@ function combineDirectAndRecursiveFiles(directEntries, fileEntries) {
|
|
|
692
711
|
...fileEntries.filter((entry) => !seen.has(entry.path))
|
|
693
712
|
];
|
|
694
713
|
}
|
|
695
|
-
function worktreeFiles(cwd) {
|
|
696
|
-
return listTree("worktree", "", cwd, { recursive: true }).entries;
|
|
697
|
-
}
|
|
698
714
|
function listTree(ref, path, cwd, options = {}) {
|
|
699
715
|
const base = normalizeTreePath(path);
|
|
700
716
|
if (ref === "worktree") {
|
|
701
717
|
return {
|
|
702
718
|
code: 0,
|
|
703
|
-
entries: worktreeFilesystemEntries(cwd, base, !!options.recursive, options.omitDirNames),
|
|
719
|
+
entries: worktreeFilesystemEntries(cwd, base, !!options.recursive, options.omitDirNames, options.excludeNames),
|
|
704
720
|
stderr: ""
|
|
705
721
|
};
|
|
706
722
|
}
|
|
@@ -1092,17 +1108,19 @@ var GREP_DEFAULT_MAX = 200;
|
|
|
1092
1108
|
var GREP_ABSOLUTE_MAX = 500;
|
|
1093
1109
|
var GREP_MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
1094
1110
|
var FILE_SEARCH_ABSOLUTE_MAX = 50000;
|
|
1111
|
+
var DEFAULT_EXCLUDE_NAMES = [".DS_Store"];
|
|
1095
1112
|
function normalizeGrepMax(value) {
|
|
1096
1113
|
const parsed = Number(value || "");
|
|
1097
1114
|
if (!Number.isInteger(parsed) || parsed <= 0)
|
|
1098
1115
|
return GREP_DEFAULT_MAX;
|
|
1099
1116
|
return Math.min(parsed, GREP_ABSOLUTE_MAX);
|
|
1100
1117
|
}
|
|
1101
|
-
function isSkippableSearchPath(path, omitDirNames = []) {
|
|
1118
|
+
function isSkippableSearchPath(path, omitDirNames = [], excludeNames = []) {
|
|
1102
1119
|
const omitDirs = new Set(omitDirNames.map((name) => name.toLowerCase()));
|
|
1120
|
+
const excluded = new Set(excludeNames.map((name) => name.toLowerCase()));
|
|
1103
1121
|
return path.split(/[\\/]+/).some((part) => {
|
|
1104
1122
|
const lower = part.toLowerCase();
|
|
1105
|
-
return lower === ".git" || omitDirs.has(lower);
|
|
1123
|
+
return lower === ".git" || omitDirs.has(lower) || excluded.has(lower);
|
|
1106
1124
|
});
|
|
1107
1125
|
}
|
|
1108
1126
|
function fixedStringLineMatches(path, text, query, max) {
|
|
@@ -1135,7 +1153,7 @@ function buildFileSearchList(ref, generation, entries) {
|
|
|
1135
1153
|
truncated: entries.length > FILE_SEARCH_ABSOLUTE_MAX
|
|
1136
1154
|
};
|
|
1137
1155
|
}
|
|
1138
|
-
function buildRgArgs(query, max, paths, regex = false, omitDirNames = []) {
|
|
1156
|
+
function buildRgArgs(query, max, paths, regex = false, omitDirNames = [], excludeNames = []) {
|
|
1139
1157
|
const safePaths = paths.length ? paths : ["."];
|
|
1140
1158
|
const omitGlobs = omitDirNames.flatMap((name) => [
|
|
1141
1159
|
"--glob",
|
|
@@ -1143,6 +1161,12 @@ function buildRgArgs(query, max, paths, regex = false, omitDirNames = []) {
|
|
|
1143
1161
|
"--glob",
|
|
1144
1162
|
`!**/${name}/**`
|
|
1145
1163
|
]);
|
|
1164
|
+
const excludeGlobs = excludeNames.flatMap((name) => [
|
|
1165
|
+
"--glob",
|
|
1166
|
+
`!${name}`,
|
|
1167
|
+
"--glob",
|
|
1168
|
+
`!**/${name}`
|
|
1169
|
+
]);
|
|
1146
1170
|
const args = [
|
|
1147
1171
|
"rg",
|
|
1148
1172
|
"--no-config",
|
|
@@ -1157,6 +1181,7 @@ function buildRgArgs(query, max, paths, regex = false, omitDirNames = []) {
|
|
|
1157
1181
|
"--max-filesize",
|
|
1158
1182
|
"2M",
|
|
1159
1183
|
...omitGlobs,
|
|
1184
|
+
...excludeGlobs,
|
|
1160
1185
|
"-e",
|
|
1161
1186
|
query,
|
|
1162
1187
|
"--",
|
|
@@ -1166,7 +1191,7 @@ function buildRgArgs(query, max, paths, regex = false, omitDirNames = []) {
|
|
|
1166
1191
|
args.splice(8, 0, "--fixed-strings");
|
|
1167
1192
|
return args;
|
|
1168
1193
|
}
|
|
1169
|
-
function parseRgOutput(stdout, max, omitDirNames = []) {
|
|
1194
|
+
function parseRgOutput(stdout, max, omitDirNames = [], excludeNames = []) {
|
|
1170
1195
|
const matches = [];
|
|
1171
1196
|
for (const line of stdout.split(`
|
|
1172
1197
|
`)) {
|
|
@@ -1179,7 +1204,7 @@ function parseRgOutput(stdout, max, omitDirNames = []) {
|
|
|
1179
1204
|
const lineNo = Number(parsed[2]);
|
|
1180
1205
|
const column = Number(parsed[3]);
|
|
1181
1206
|
const preview = parsed[4];
|
|
1182
|
-
if (!path || !lineNo || !column || isSkippableSearchPath(path, omitDirNames))
|
|
1207
|
+
if (!path || !lineNo || !column || isSkippableSearchPath(path, omitDirNames, excludeNames))
|
|
1183
1208
|
continue;
|
|
1184
1209
|
matches.push({
|
|
1185
1210
|
path,
|
|
@@ -1190,12 +1215,12 @@ function parseRgOutput(stdout, max, omitDirNames = []) {
|
|
|
1190
1215
|
}
|
|
1191
1216
|
return matches;
|
|
1192
1217
|
}
|
|
1193
|
-
function parseGitGrepOutput(stdout, ref, max, omitDirNames = []) {
|
|
1218
|
+
function parseGitGrepOutput(stdout, ref, max, omitDirNames = [], excludeNames = []) {
|
|
1194
1219
|
const prefix = `${ref}:`;
|
|
1195
1220
|
const normalized = stdout.split(`
|
|
1196
1221
|
`).map((line) => line.startsWith(prefix) ? line.slice(prefix.length) : line).join(`
|
|
1197
1222
|
`);
|
|
1198
|
-
return parseRgOutput(normalized, max, omitDirNames);
|
|
1223
|
+
return parseRgOutput(normalized, max, omitDirNames, excludeNames);
|
|
1199
1224
|
}
|
|
1200
1225
|
|
|
1201
1226
|
// web-src/server/preview.ts
|
|
@@ -1248,6 +1273,7 @@ var uploadAllowedByCli = false;
|
|
|
1248
1273
|
var openAfterStart = false;
|
|
1249
1274
|
var scopeOmitDirNames = DEFAULT_WORKTREE_OMIT_DIR_NAMES;
|
|
1250
1275
|
var scopeOmitDirCliOverride = null;
|
|
1276
|
+
var scopeExcludeNames = DEFAULT_EXCLUDE_NAMES;
|
|
1251
1277
|
var rgAvailableCache = null;
|
|
1252
1278
|
var enc = new TextEncoder;
|
|
1253
1279
|
var sseClients = new Set;
|
|
@@ -1322,11 +1348,14 @@ Examples:
|
|
|
1322
1348
|
if (!uploadAllowedByCli)
|
|
1323
1349
|
allowUpload = loadProjectConfigUploadEnabled();
|
|
1324
1350
|
const configScopeOmitDirs = loadProjectConfigScopeOmitDirs();
|
|
1351
|
+
const configScopeExcludeNames = loadProjectConfigScopeExcludeNames();
|
|
1325
1352
|
if (scopeOmitDirCliOverride) {
|
|
1326
1353
|
scopeOmitDirNames = scopeOmitDirCliOverride;
|
|
1327
1354
|
} else if (configScopeOmitDirs) {
|
|
1328
1355
|
scopeOmitDirNames = configScopeOmitDirs;
|
|
1329
1356
|
}
|
|
1357
|
+
if (configScopeExcludeNames)
|
|
1358
|
+
scopeExcludeNames = configScopeExcludeNames;
|
|
1330
1359
|
}
|
|
1331
1360
|
function json(data, init = {}) {
|
|
1332
1361
|
return new Response(JSON.stringify(data), {
|
|
@@ -1599,6 +1628,13 @@ function normalizeScopeOmitDirNames(names) {
|
|
|
1599
1628
|
...new Set(names.filter((name) => typeof name === "string").map((name) => name.trim()).filter((name) => name && name.length <= 64 && !name.includes("/") && !name.includes("\\") && !name.includes("\x00") && name !== "." && name !== ".." && name !== ".git"))
|
|
1600
1629
|
].sort((a, b) => a.localeCompare(b));
|
|
1601
1630
|
}
|
|
1631
|
+
function normalizeScopeExcludeNames(names) {
|
|
1632
|
+
if (!Array.isArray(names))
|
|
1633
|
+
return [];
|
|
1634
|
+
return [
|
|
1635
|
+
...new Set(names.filter((name) => typeof name === "string").map((name) => name.trim()).filter((name) => name && name.length <= 128 && !name.includes("/") && !name.includes("\\") && !name.includes("\x00") && name !== "." && name !== ".." && name !== ".git"))
|
|
1636
|
+
].sort((a, b) => a.localeCompare(b));
|
|
1637
|
+
}
|
|
1602
1638
|
function parseScopeOmitDirNamesQuery(value) {
|
|
1603
1639
|
const names = value ? value.split(",") : [];
|
|
1604
1640
|
if (names.length > 100)
|
|
@@ -1610,6 +1646,17 @@ function parseScopeOmitDirNamesQuery(value) {
|
|
|
1610
1646
|
}
|
|
1611
1647
|
return normalizeScopeOmitDirNames(names);
|
|
1612
1648
|
}
|
|
1649
|
+
function parseScopeExcludeNamesQuery(value) {
|
|
1650
|
+
const names = value ? value.split(",") : [];
|
|
1651
|
+
if (names.length > 200)
|
|
1652
|
+
return null;
|
|
1653
|
+
for (const raw of names) {
|
|
1654
|
+
const name = raw.trim();
|
|
1655
|
+
if (!name || name.length > 128 || name.includes("/") || name.includes("\\") || name.includes("\x00") || name === "." || name === ".." || name === ".git")
|
|
1656
|
+
return null;
|
|
1657
|
+
}
|
|
1658
|
+
return normalizeScopeExcludeNames(names);
|
|
1659
|
+
}
|
|
1613
1660
|
function loadProjectConfig() {
|
|
1614
1661
|
const full = join4(cwd, ".code-viewer.json");
|
|
1615
1662
|
if (!existsSync3(full))
|
|
@@ -1643,14 +1690,31 @@ function loadProjectConfigScopeOmitDirs() {
|
|
|
1643
1690
|
return null;
|
|
1644
1691
|
return normalizeScopeOmitDirNames(config.scope.omitDirs);
|
|
1645
1692
|
}
|
|
1693
|
+
function loadProjectConfigScopeExcludeNames() {
|
|
1694
|
+
const config = loadProjectConfig();
|
|
1695
|
+
if (!config?.scope || !Array.isArray(config.scope.excludeNames))
|
|
1696
|
+
return null;
|
|
1697
|
+
return normalizeScopeExcludeNames(config.scope.excludeNames);
|
|
1698
|
+
}
|
|
1646
1699
|
function scopeOmitDirNamesFromQuery(url) {
|
|
1647
1700
|
if (!url.searchParams.has("omit_dirs"))
|
|
1648
1701
|
return scopeOmitDirNames;
|
|
1649
1702
|
return parseScopeOmitDirNamesQuery(url.searchParams.get("omit_dirs") || "") || scopeOmitDirNames;
|
|
1650
1703
|
}
|
|
1704
|
+
function scopeExcludeNamesFromQuery(url) {
|
|
1705
|
+
if (!url.searchParams.has("exclude_names"))
|
|
1706
|
+
return scopeExcludeNames;
|
|
1707
|
+
return parseScopeExcludeNamesQuery(url.searchParams.get("exclude_names") || "") || scopeExcludeNames;
|
|
1708
|
+
}
|
|
1651
1709
|
function invalidScopeOmitDirNamesQuery(url) {
|
|
1652
1710
|
return url.searchParams.has("omit_dirs") && !parseScopeOmitDirNamesQuery(url.searchParams.get("omit_dirs") || "");
|
|
1653
1711
|
}
|
|
1712
|
+
function invalidScopeExcludeNamesQuery(url) {
|
|
1713
|
+
return url.searchParams.has("exclude_names") && !parseScopeExcludeNamesQuery(url.searchParams.get("exclude_names") || "");
|
|
1714
|
+
}
|
|
1715
|
+
function isExcludedScopePath(path, excludeNames) {
|
|
1716
|
+
return path.split(/[\\/]+/).some((part) => excludeNames.some((name) => part.toLowerCase() === name.toLowerCase()));
|
|
1717
|
+
}
|
|
1654
1718
|
function isGitInternalPath(path) {
|
|
1655
1719
|
return path.split(/[\\/]+/).some((part) => part.toLowerCase() === ".git");
|
|
1656
1720
|
}
|
|
@@ -1677,6 +1741,9 @@ function safeWorktreePath(path) {
|
|
|
1677
1741
|
return null;
|
|
1678
1742
|
return realFull;
|
|
1679
1743
|
}
|
|
1744
|
+
function worktreePath(path) {
|
|
1745
|
+
return join4(cwd, path);
|
|
1746
|
+
}
|
|
1680
1747
|
function safeOpenWorktreePath(path) {
|
|
1681
1748
|
if (path === "") {
|
|
1682
1749
|
try {
|
|
@@ -1694,6 +1761,71 @@ function parentRepoPath(path) {
|
|
|
1694
1761
|
const parent = dirname2(path);
|
|
1695
1762
|
return parent === "." ? "" : parent;
|
|
1696
1763
|
}
|
|
1764
|
+
function isoDate(ms) {
|
|
1765
|
+
return ms && Number.isFinite(ms) ? new Date(ms).toISOString() : undefined;
|
|
1766
|
+
}
|
|
1767
|
+
function worktreeFileMetadata(path, knownSize) {
|
|
1768
|
+
const full = safeWorktreePath(path);
|
|
1769
|
+
if (!full)
|
|
1770
|
+
return {};
|
|
1771
|
+
try {
|
|
1772
|
+
const stat = statSync(full);
|
|
1773
|
+
return {
|
|
1774
|
+
size: knownSize ?? stat.size,
|
|
1775
|
+
created_at: isoDate(stat.birthtimeMs),
|
|
1776
|
+
updated_at: isoDate(stat.mtimeMs)
|
|
1777
|
+
};
|
|
1778
|
+
} catch {
|
|
1779
|
+
return {};
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
function gitFileMetadata(ref, path, knownSize) {
|
|
1783
|
+
const size = knownSize ?? rawFileSize(path, ref);
|
|
1784
|
+
const commitUpdatedAt = lastCommitDateForPath(ref, path, cwd) || undefined;
|
|
1785
|
+
return {
|
|
1786
|
+
size: size == null ? undefined : size,
|
|
1787
|
+
updated_at: commitUpdatedAt,
|
|
1788
|
+
commit_updated_at: commitUpdatedAt
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1791
|
+
function directoryMetadata(target, path) {
|
|
1792
|
+
if (target === "worktree" || target === "") {
|
|
1793
|
+
const full = path === "" ? safeOpenWorktreePath("") : safeWorktreePath(path);
|
|
1794
|
+
if (!full)
|
|
1795
|
+
return {};
|
|
1796
|
+
try {
|
|
1797
|
+
const stat = statSync(full);
|
|
1798
|
+
return {
|
|
1799
|
+
created_at: isoDate(stat.birthtimeMs),
|
|
1800
|
+
updated_at: isoDate(stat.mtimeMs)
|
|
1801
|
+
};
|
|
1802
|
+
} catch {
|
|
1803
|
+
return {};
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
const commitUpdatedAt = lastCommitDateForPath(target, path || ".", cwd) || undefined;
|
|
1807
|
+
return { updated_at: commitUpdatedAt, commit_updated_at: commitUpdatedAt };
|
|
1808
|
+
}
|
|
1809
|
+
function fileMetadataForTarget(target, path) {
|
|
1810
|
+
return target === "worktree" || target === "" ? worktreeFileMetadata(path) : gitFileMetadata(target, path);
|
|
1811
|
+
}
|
|
1812
|
+
function attachTreeEntryMetadata(target, entry) {
|
|
1813
|
+
if (entry.type === "tree")
|
|
1814
|
+
return { ...entry, ...directoryMetadata(target, entry.path) };
|
|
1815
|
+
if (entry.type !== "blob")
|
|
1816
|
+
return entry;
|
|
1817
|
+
return { ...entry, ...fileMetadataForTarget(target, entry.path) };
|
|
1818
|
+
}
|
|
1819
|
+
function fileMetadataHeaders(metadata) {
|
|
1820
|
+
const headers = {};
|
|
1821
|
+
if (metadata.created_at)
|
|
1822
|
+
headers["X-Code-Viewer-Created-At"] = metadata.created_at;
|
|
1823
|
+
if (metadata.updated_at)
|
|
1824
|
+
headers["X-Code-Viewer-Updated-At"] = metadata.updated_at;
|
|
1825
|
+
if (metadata.commit_updated_at)
|
|
1826
|
+
headers["X-Code-Viewer-Commit-Updated-At"] = metadata.commit_updated_at;
|
|
1827
|
+
return headers;
|
|
1828
|
+
}
|
|
1697
1829
|
function readReadme(target, dirPath) {
|
|
1698
1830
|
const candidates = ["README.md", "readme.md", "README.markdown", "README"];
|
|
1699
1831
|
for (const name of candidates) {
|
|
@@ -1726,16 +1858,20 @@ function handleTree(url) {
|
|
|
1726
1858
|
const recursive = url.searchParams.get("recursive") === "1";
|
|
1727
1859
|
if (invalidScopeOmitDirNamesQuery(url))
|
|
1728
1860
|
return text("invalid omit dirs", 400);
|
|
1861
|
+
if (invalidScopeExcludeNamesQuery(url))
|
|
1862
|
+
return text("invalid exclude names", 400);
|
|
1863
|
+
const excludeNames = scopeExcludeNamesFromQuery(url);
|
|
1729
1864
|
const entries = listTree(target, path, cwd, {
|
|
1730
1865
|
recursive,
|
|
1731
|
-
omitDirNames: scopeOmitDirNamesFromQuery(url)
|
|
1732
|
-
|
|
1866
|
+
omitDirNames: scopeOmitDirNamesFromQuery(url),
|
|
1867
|
+
excludeNames
|
|
1868
|
+
}).entries.filter((entry) => !isExcludedScopePath(entry.path, excludeNames));
|
|
1733
1869
|
return json({
|
|
1734
1870
|
ref: target,
|
|
1735
1871
|
path,
|
|
1736
1872
|
project: basename2(cwd),
|
|
1737
1873
|
branch: currentBranch(cwd) || undefined,
|
|
1738
|
-
entries,
|
|
1874
|
+
entries: recursive ? entries : entries.map((entry) => attachTreeEntryMetadata(target, entry)),
|
|
1739
1875
|
readme: readReadme(target, path),
|
|
1740
1876
|
upload_enabled: allowUpload && (target === "worktree" || target === "")
|
|
1741
1877
|
});
|
|
@@ -1746,6 +1882,8 @@ function handleSettings() {
|
|
|
1746
1882
|
scope: {
|
|
1747
1883
|
omit_dirs_effective: scopeOmitDirNames,
|
|
1748
1884
|
omit_dirs_built_in: DEFAULT_WORKTREE_OMIT_DIR_NAMES,
|
|
1885
|
+
exclude_names_effective: scopeExcludeNames,
|
|
1886
|
+
exclude_names_built_in: DEFAULT_EXCLUDE_NAMES,
|
|
1749
1887
|
max_entries: WORKTREE_RECURSIVE_ENTRY_LIMIT
|
|
1750
1888
|
}
|
|
1751
1889
|
});
|
|
@@ -1756,22 +1894,26 @@ function handleFiles(url) {
|
|
|
1756
1894
|
return text("invalid target", 400);
|
|
1757
1895
|
if (invalidScopeOmitDirNamesQuery(url))
|
|
1758
1896
|
return text("invalid omit dirs", 400);
|
|
1897
|
+
if (invalidScopeExcludeNamesQuery(url))
|
|
1898
|
+
return text("invalid exclude names", 400);
|
|
1759
1899
|
const omitDirNames = scopeOmitDirNamesFromQuery(url);
|
|
1760
|
-
const
|
|
1900
|
+
const excludeNames = scopeExcludeNamesFromQuery(url);
|
|
1901
|
+
const key = `${target || "worktree"}\x00${omitDirNames.join("\x00")}\x00${excludeNames.join("\x00")}`;
|
|
1761
1902
|
const cached = fileListCache.get(key);
|
|
1762
1903
|
if (cached && cached.generation === generation)
|
|
1763
1904
|
return json(cached.body);
|
|
1764
1905
|
const ref = target || "worktree";
|
|
1765
1906
|
const entries = listTree(ref, "", cwd, {
|
|
1766
1907
|
recursive: true,
|
|
1767
|
-
omitDirNames
|
|
1768
|
-
|
|
1908
|
+
omitDirNames,
|
|
1909
|
+
excludeNames
|
|
1910
|
+
}).entries.filter((entry) => !isExcludedScopePath(entry.path, excludeNames));
|
|
1769
1911
|
const body = buildFileSearchList(ref, generation, entries);
|
|
1770
1912
|
fileListCache.set(key, { generation, body });
|
|
1771
1913
|
return json(body);
|
|
1772
1914
|
}
|
|
1773
|
-
function parseGrepPaths(url, omitDirNames) {
|
|
1774
|
-
return url.searchParams.getAll("path").filter((path) => safePath(path) && !isGitInternalPath(path) && !isSkippableSearchPath(path, omitDirNames));
|
|
1915
|
+
function parseGrepPaths(url, omitDirNames, excludeNames) {
|
|
1916
|
+
return url.searchParams.getAll("path").filter((path) => safePath(path) && !isGitInternalPath(path) && !isSkippableSearchPath(path, omitDirNames, excludeNames));
|
|
1775
1917
|
}
|
|
1776
1918
|
function rgAvailable() {
|
|
1777
1919
|
if (rgAvailableCache !== null)
|
|
@@ -1780,13 +1922,17 @@ function rgAvailable() {
|
|
|
1780
1922
|
rgAvailableCache = proc.code === 0;
|
|
1781
1923
|
return rgAvailableCache;
|
|
1782
1924
|
}
|
|
1783
|
-
function grepWorktreeFallback(query, max, paths, omitDirNames) {
|
|
1784
|
-
const candidates = paths.length ? paths :
|
|
1925
|
+
function grepWorktreeFallback(query, max, paths, omitDirNames, excludeNames) {
|
|
1926
|
+
const candidates = paths.length ? paths : listTree("worktree", "", cwd, {
|
|
1927
|
+
recursive: true,
|
|
1928
|
+
omitDirNames,
|
|
1929
|
+
excludeNames
|
|
1930
|
+
}).entries.map((entry) => entry.path);
|
|
1785
1931
|
const matches = [];
|
|
1786
1932
|
for (const path of candidates) {
|
|
1787
1933
|
if (matches.length >= max)
|
|
1788
1934
|
break;
|
|
1789
|
-
if (!safePath(path) || isGitInternalPath(path) || isSkippableSearchPath(path, omitDirNames))
|
|
1935
|
+
if (!safePath(path) || isGitInternalPath(path) || isSkippableSearchPath(path, omitDirNames, excludeNames))
|
|
1790
1936
|
continue;
|
|
1791
1937
|
const full = safeWorktreePath(path);
|
|
1792
1938
|
if (!full)
|
|
@@ -1811,13 +1957,13 @@ function grepWorktreeFallback(query, max, paths, omitDirNames) {
|
|
|
1811
1957
|
}
|
|
1812
1958
|
return matches;
|
|
1813
1959
|
}
|
|
1814
|
-
function grepWorktree(query, max, paths, regex, omitDirNames) {
|
|
1960
|
+
function grepWorktree(query, max, paths, regex, omitDirNames, excludeNames) {
|
|
1815
1961
|
if (rgAvailable()) {
|
|
1816
|
-
const safePaths = paths.filter((path) => safePath(path) && !isGitInternalPath(path) && !isSkippableSearchPath(path, omitDirNames) && safeWorktreePath(path));
|
|
1817
|
-
const args = buildRgArgs(query, max, safePaths, regex, omitDirNames);
|
|
1962
|
+
const safePaths = paths.filter((path) => safePath(path) && !isGitInternalPath(path) && !isSkippableSearchPath(path, omitDirNames, excludeNames) && safeWorktreePath(path));
|
|
1963
|
+
const args = buildRgArgs(query, max, safePaths, regex, omitDirNames, excludeNames);
|
|
1818
1964
|
const proc = runSync(args, cwd, { timeout: 5000 });
|
|
1819
1965
|
const stdout = proc.stdout;
|
|
1820
|
-
const matches2 = parseRgOutput(stdout, max, omitDirNames).filter((match) => safePath(match.path) && !isGitInternalPath(match.path) && !isSkippableSearchPath(match.path, omitDirNames) && !!safeWorktreePath(match.path));
|
|
1966
|
+
const matches2 = parseRgOutput(stdout, max, omitDirNames, excludeNames).filter((match) => safePath(match.path) && !isGitInternalPath(match.path) && !isSkippableSearchPath(match.path, omitDirNames, excludeNames) && !!safeWorktreePath(match.path));
|
|
1821
1967
|
return {
|
|
1822
1968
|
ref: "worktree",
|
|
1823
1969
|
engine: "rg",
|
|
@@ -1832,7 +1978,7 @@ function grepWorktree(query, max, paths, regex, omitDirNames) {
|
|
|
1832
1978
|
truncated: false,
|
|
1833
1979
|
matches: []
|
|
1834
1980
|
};
|
|
1835
|
-
const matches = grepWorktreeFallback(query, max, paths, omitDirNames);
|
|
1981
|
+
const matches = grepWorktreeFallback(query, max, paths, omitDirNames, excludeNames);
|
|
1836
1982
|
return {
|
|
1837
1983
|
ref: "worktree",
|
|
1838
1984
|
engine: "fallback",
|
|
@@ -1840,8 +1986,8 @@ function grepWorktree(query, max, paths, regex, omitDirNames) {
|
|
|
1840
1986
|
matches
|
|
1841
1987
|
};
|
|
1842
1988
|
}
|
|
1843
|
-
function grepTreeRef(ref, query, max, paths, regex, omitDirNames) {
|
|
1844
|
-
const safePaths = paths.filter((path) => safePath(path) && !isGitInternalPath(path) && !isSkippableSearchPath(path, omitDirNames));
|
|
1989
|
+
function grepTreeRef(ref, query, max, paths, regex, omitDirNames, excludeNames) {
|
|
1990
|
+
const safePaths = paths.filter((path) => safePath(path) && !isGitInternalPath(path) && !isSkippableSearchPath(path, omitDirNames, excludeNames));
|
|
1845
1991
|
const args = [
|
|
1846
1992
|
"git",
|
|
1847
1993
|
"-c",
|
|
@@ -1860,7 +2006,7 @@ function grepTreeRef(ref, query, max, paths, regex, omitDirNames) {
|
|
|
1860
2006
|
];
|
|
1861
2007
|
const proc = runSync(args, cwd, { timeout: 5000 });
|
|
1862
2008
|
const stdout = proc.stdout;
|
|
1863
|
-
const matches = parseGitGrepOutput(stdout, ref, max, omitDirNames).slice(0, max);
|
|
2009
|
+
const matches = parseGitGrepOutput(stdout, ref, max, omitDirNames, excludeNames).slice(0, max);
|
|
1864
2010
|
return { ref, engine: "git", truncated: matches.length >= max, matches };
|
|
1865
2011
|
}
|
|
1866
2012
|
function handleGrep(url) {
|
|
@@ -1869,8 +2015,11 @@ function handleGrep(url) {
|
|
|
1869
2015
|
const max = normalizeGrepMax(url.searchParams.get("max"));
|
|
1870
2016
|
if (invalidScopeOmitDirNamesQuery(url))
|
|
1871
2017
|
return text("invalid omit dirs", 400);
|
|
2018
|
+
if (invalidScopeExcludeNamesQuery(url))
|
|
2019
|
+
return text("invalid exclude names", 400);
|
|
1872
2020
|
const omitDirNames = scopeOmitDirNamesFromQuery(url);
|
|
1873
|
-
const
|
|
2021
|
+
const excludeNames = scopeExcludeNamesFromQuery(url);
|
|
2022
|
+
const paths = parseGrepPaths(url, omitDirNames, excludeNames);
|
|
1874
2023
|
const regex = url.searchParams.get("regex") === "1";
|
|
1875
2024
|
if (!query.trim())
|
|
1876
2025
|
return json({
|
|
@@ -1880,10 +2029,10 @@ function handleGrep(url) {
|
|
|
1880
2029
|
matches: []
|
|
1881
2030
|
});
|
|
1882
2031
|
if (ref === "worktree" || ref === "")
|
|
1883
|
-
return json(grepWorktree(query, max, paths, regex, omitDirNames));
|
|
2032
|
+
return json(grepWorktree(query, max, paths, regex, omitDirNames, excludeNames));
|
|
1884
2033
|
if (!verifyTreeRef(ref, cwd))
|
|
1885
2034
|
return text("invalid target", 400);
|
|
1886
|
-
return json(grepTreeRef(ref, query, max, paths, regex, omitDirNames));
|
|
2035
|
+
return json(grepTreeRef(ref, query, max, paths, regex, omitDirNames, excludeNames));
|
|
1887
2036
|
}
|
|
1888
2037
|
function handleRefCommits(url) {
|
|
1889
2038
|
const query = url.searchParams.get("q") || "";
|
|
@@ -2169,13 +2318,18 @@ function handleRawFile(req, url) {
|
|
|
2169
2318
|
const size = rawFileSize(path, ref);
|
|
2170
2319
|
if (size == null)
|
|
2171
2320
|
return text("not in ref", 404);
|
|
2321
|
+
const metadata = gitFileMetadata(ref, path, size);
|
|
2172
2322
|
if (req.method === "HEAD")
|
|
2173
|
-
return new Response(null, {
|
|
2323
|
+
return new Response(null, {
|
|
2324
|
+
headers: rawFileHeaders(path, size, undefined, metadata)
|
|
2325
|
+
});
|
|
2174
2326
|
const res = showBytes(ref, path, cwd);
|
|
2175
2327
|
if (res.code !== 0)
|
|
2176
2328
|
return text("not in ref", 404);
|
|
2177
2329
|
body = res.stdout.buffer.slice(res.stdout.byteOffset, res.stdout.byteOffset + res.stdout.byteLength);
|
|
2178
|
-
return new Response(body, {
|
|
2330
|
+
return new Response(body, {
|
|
2331
|
+
headers: rawFileHeaders(path, size, undefined, metadata)
|
|
2332
|
+
});
|
|
2179
2333
|
} else {
|
|
2180
2334
|
const full = safeWorktreePath(path);
|
|
2181
2335
|
if (!full)
|
|
@@ -2183,12 +2337,13 @@ function handleRawFile(req, url) {
|
|
|
2183
2337
|
const size = rawFileSize(path, ref);
|
|
2184
2338
|
if (size == null)
|
|
2185
2339
|
return text("not found", 404);
|
|
2340
|
+
const metadata = worktreeFileMetadata(path, size);
|
|
2186
2341
|
const rangeResult = req.headers.get("range") ? parseHttpByteRange(req.headers.get("range"), size) : null;
|
|
2187
2342
|
if (rangeResult?.kind === "unsatisfiable") {
|
|
2188
2343
|
return new Response(null, {
|
|
2189
2344
|
status: 416,
|
|
2190
2345
|
headers: {
|
|
2191
|
-
...rawFileHeaders(path, size),
|
|
2346
|
+
...rawFileHeaders(path, size, undefined, metadata),
|
|
2192
2347
|
"Content-Range": `bytes */${size}`,
|
|
2193
2348
|
"Content-Length": "0"
|
|
2194
2349
|
}
|
|
@@ -2199,18 +2354,20 @@ function handleRawFile(req, url) {
|
|
|
2199
2354
|
if (req.method === "HEAD") {
|
|
2200
2355
|
return new Response(null, {
|
|
2201
2356
|
status: 206,
|
|
2202
|
-
headers: rawFileHeaders(path, size, range)
|
|
2357
|
+
headers: rawFileHeaders(path, size, range, metadata)
|
|
2203
2358
|
});
|
|
2204
2359
|
}
|
|
2205
2360
|
return new Response(fileByteRangeResponseBody(full, range.start, range.end), {
|
|
2206
2361
|
status: 206,
|
|
2207
|
-
headers: rawFileHeaders(path, size, range)
|
|
2362
|
+
headers: rawFileHeaders(path, size, range, metadata)
|
|
2208
2363
|
});
|
|
2209
2364
|
}
|
|
2210
2365
|
if (req.method === "HEAD")
|
|
2211
|
-
return new Response(null, {
|
|
2366
|
+
return new Response(null, {
|
|
2367
|
+
headers: rawFileHeaders(path, size, undefined, metadata)
|
|
2368
|
+
});
|
|
2212
2369
|
return new Response(fileReadableStream(full), {
|
|
2213
|
-
headers: rawFileHeaders(path, size)
|
|
2370
|
+
headers: rawFileHeaders(path, size, undefined, metadata)
|
|
2214
2371
|
});
|
|
2215
2372
|
}
|
|
2216
2373
|
}
|
|
@@ -2230,7 +2387,7 @@ function rawFileSize(path, ref) {
|
|
|
2230
2387
|
return null;
|
|
2231
2388
|
}
|
|
2232
2389
|
}
|
|
2233
|
-
function rawFileHeaders(path, size = null, range) {
|
|
2390
|
+
function rawFileHeaders(path, size = null, range, metadata = {}) {
|
|
2234
2391
|
const mime = {
|
|
2235
2392
|
".png": "image/png",
|
|
2236
2393
|
".jpg": "image/jpeg",
|
|
@@ -2263,6 +2420,9 @@ function rawFileHeaders(path, size = null, range) {
|
|
|
2263
2420
|
} else if (size != null) {
|
|
2264
2421
|
headers["Content-Length"] = String(size);
|
|
2265
2422
|
}
|
|
2423
|
+
for (const [key, value] of Object.entries(fileMetadataHeaders(metadata))) {
|
|
2424
|
+
headers[key] = value;
|
|
2425
|
+
}
|
|
2266
2426
|
return headers;
|
|
2267
2427
|
}
|
|
2268
2428
|
function isForbiddenUploadName(name) {
|
|
@@ -2385,6 +2545,152 @@ function openOsPath(path) {
|
|
|
2385
2545
|
const cmd = process.platform === "darwin" ? ["open", "--", path] : process.platform === "win32" ? ["explorer.exe", path] : ["xdg-open", path];
|
|
2386
2546
|
spawnDetached(cmd);
|
|
2387
2547
|
}
|
|
2548
|
+
function windowsTrashScript(path) {
|
|
2549
|
+
const quotedPath = path.replace(/'/g, "''");
|
|
2550
|
+
return [
|
|
2551
|
+
"$ErrorActionPreference = 'Stop';",
|
|
2552
|
+
`$path = '${quotedPath}';`,
|
|
2553
|
+
"Add-Type -TypeDefinition @'",
|
|
2554
|
+
"using System;",
|
|
2555
|
+
"using System.Runtime.InteropServices;",
|
|
2556
|
+
"public static class CodeViewerRecycleBin {",
|
|
2557
|
+
" [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]",
|
|
2558
|
+
" public struct SHFILEOPSTRUCT {",
|
|
2559
|
+
" public IntPtr hwnd;",
|
|
2560
|
+
" public uint wFunc;",
|
|
2561
|
+
" public string pFrom;",
|
|
2562
|
+
" public string pTo;",
|
|
2563
|
+
" public ushort fFlags;",
|
|
2564
|
+
" [MarshalAs(UnmanagedType.Bool)] public bool fAnyOperationsAborted;",
|
|
2565
|
+
" public IntPtr hNameMappings;",
|
|
2566
|
+
" public string lpszProgressTitle;",
|
|
2567
|
+
" }",
|
|
2568
|
+
' [DllImport("shell32.dll", CharSet = CharSet.Unicode)]',
|
|
2569
|
+
" private static extern int SHFileOperationW(ref SHFILEOPSTRUCT lpFileOp);",
|
|
2570
|
+
" public static void MoveToRecycleBin(string path) {",
|
|
2571
|
+
" const uint FO_DELETE = 0x0003;",
|
|
2572
|
+
" const ushort FOF_SILENT = 0x0004;",
|
|
2573
|
+
" const ushort FOF_NOCONFIRMATION = 0x0010;",
|
|
2574
|
+
" const ushort FOF_ALLOWUNDO = 0x0040;",
|
|
2575
|
+
" const ushort FOF_NOERRORUI = 0x0400;",
|
|
2576
|
+
" var op = new SHFILEOPSTRUCT {",
|
|
2577
|
+
" hwnd = IntPtr.Zero,",
|
|
2578
|
+
" wFunc = FO_DELETE,",
|
|
2579
|
+
' pFrom = path + "\\0\\0",',
|
|
2580
|
+
" pTo = null,",
|
|
2581
|
+
" fFlags = (ushort)(FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT),",
|
|
2582
|
+
" fAnyOperationsAborted = false,",
|
|
2583
|
+
" hNameMappings = IntPtr.Zero,",
|
|
2584
|
+
" lpszProgressTitle = null",
|
|
2585
|
+
" };",
|
|
2586
|
+
" int result = SHFileOperationW(ref op);",
|
|
2587
|
+
' if (result != 0) throw new InvalidOperationException("SHFileOperationW failed: " + result);',
|
|
2588
|
+
' if (op.fAnyOperationsAborted) throw new OperationCanceledException("SHFileOperationW aborted");',
|
|
2589
|
+
" }",
|
|
2590
|
+
"}",
|
|
2591
|
+
"'@;",
|
|
2592
|
+
"[CodeViewerRecycleBin]::MoveToRecycleBin($path);"
|
|
2593
|
+
].join(" ");
|
|
2594
|
+
}
|
|
2595
|
+
function windowsRestoreTrashScript(originalPath) {
|
|
2596
|
+
const quotedPath = originalPath.replace(/'/g, "''");
|
|
2597
|
+
return [
|
|
2598
|
+
"$ErrorActionPreference = 'Stop';",
|
|
2599
|
+
`$original = '${quotedPath}';`,
|
|
2600
|
+
"$parent = [System.IO.Path]::GetDirectoryName($original);",
|
|
2601
|
+
"$name = [System.IO.Path]::GetFileName($original);",
|
|
2602
|
+
"$shell = New-Object -ComObject Shell.Application;",
|
|
2603
|
+
"$bin = $shell.Namespace(10);",
|
|
2604
|
+
"$restored = $false;",
|
|
2605
|
+
"foreach ($item in $bin.Items()) {",
|
|
2606
|
+
" $deletedFrom = $item.ExtendedProperty('System.Recycle.DeletedFrom');",
|
|
2607
|
+
" if ($item.Name -eq $name -and $deletedFrom -eq $parent) {",
|
|
2608
|
+
" $item.InvokeVerb('ESTORE');",
|
|
2609
|
+
" $restored = $true;",
|
|
2610
|
+
" break;",
|
|
2611
|
+
" }",
|
|
2612
|
+
"}",
|
|
2613
|
+
"if (-not $restored) { throw 'recycle bin item not found'; }"
|
|
2614
|
+
].join(" ");
|
|
2615
|
+
}
|
|
2616
|
+
function makeUndoId() {
|
|
2617
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
2618
|
+
}
|
|
2619
|
+
function clearMutableCaches() {
|
|
2620
|
+
fileCache.clear();
|
|
2621
|
+
metaCache.clear();
|
|
2622
|
+
fileListCache.clear();
|
|
2623
|
+
}
|
|
2624
|
+
function moveMacPathIntoTrash(path) {
|
|
2625
|
+
const trashDir = join4(homedir(), ".Trash");
|
|
2626
|
+
const base = basename2(path) || "code-viewer-trash-item";
|
|
2627
|
+
const target = join4(trashDir, `${base}-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
|
|
2628
|
+
try {
|
|
2629
|
+
mkdirSync(trashDir, { recursive: true });
|
|
2630
|
+
renameSync(path, target);
|
|
2631
|
+
return { ok: true, trashPath: target };
|
|
2632
|
+
} catch (error) {
|
|
2633
|
+
return { ok: false, error: String(error) };
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
function movePathToTrash(path) {
|
|
2637
|
+
lstatSync3(path);
|
|
2638
|
+
if (process.platform === "darwin") {
|
|
2639
|
+
return moveMacPathIntoTrash(path);
|
|
2640
|
+
}
|
|
2641
|
+
if (process.platform === "win32") {
|
|
2642
|
+
const res = runSync([
|
|
2643
|
+
"powershell.exe",
|
|
2644
|
+
"-NoProfile",
|
|
2645
|
+
"-NonInteractive",
|
|
2646
|
+
"-ExecutionPolicy",
|
|
2647
|
+
"Bypass",
|
|
2648
|
+
"-Command",
|
|
2649
|
+
windowsTrashScript(path)
|
|
2650
|
+
], cwd, { timeout: 60000 });
|
|
2651
|
+
return res.code === 0 ? { ok: true } : { ok: false, error: res.stderr || res.stdout };
|
|
2652
|
+
}
|
|
2653
|
+
return { ok: false, error: "trash unsupported" };
|
|
2654
|
+
}
|
|
2655
|
+
function restoreTrashPath(originalPath, trashPath) {
|
|
2656
|
+
const parent = parentRepoPath(originalPath);
|
|
2657
|
+
const parentFullPath = safeOpenWorktreePath(parent);
|
|
2658
|
+
if (!parentFullPath)
|
|
2659
|
+
return { ok: false, error: "invalid restore target" };
|
|
2660
|
+
const original = worktreePath(originalPath);
|
|
2661
|
+
if (existsSync3(original))
|
|
2662
|
+
return { ok: false, error: "restore target exists" };
|
|
2663
|
+
if (trashPath) {
|
|
2664
|
+
if (process.platform !== "darwin")
|
|
2665
|
+
return { ok: false, error: "invalid trash handle" };
|
|
2666
|
+
if (!existsSync3(trashPath))
|
|
2667
|
+
return { ok: false, error: "trash item not found" };
|
|
2668
|
+
try {
|
|
2669
|
+
const trashRoot = join4(homedir(), ".Trash");
|
|
2670
|
+
const trashRelative = relative(trashRoot, trashPath);
|
|
2671
|
+
if (trashRelative === "" || trashRelative.startsWith("..") || trashRelative.startsWith("/") || trashRelative.startsWith("\\"))
|
|
2672
|
+
return { ok: false, error: "invalid trash handle" };
|
|
2673
|
+
mkdirSync(dirname2(original), { recursive: true });
|
|
2674
|
+
renameSync(trashPath, original);
|
|
2675
|
+
return { ok: true };
|
|
2676
|
+
} catch (error) {
|
|
2677
|
+
return { ok: false, error: String(error) };
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
if (process.platform === "win32") {
|
|
2681
|
+
const res = runSync([
|
|
2682
|
+
"powershell.exe",
|
|
2683
|
+
"-NoProfile",
|
|
2684
|
+
"-NonInteractive",
|
|
2685
|
+
"-ExecutionPolicy",
|
|
2686
|
+
"Bypass",
|
|
2687
|
+
"-Command",
|
|
2688
|
+
windowsRestoreTrashScript(original)
|
|
2689
|
+
], cwd, { timeout: 60000 });
|
|
2690
|
+
return res.code === 0 ? { ok: true } : { ok: false, error: res.stderr || res.stdout };
|
|
2691
|
+
}
|
|
2692
|
+
return { ok: false, error: "undo unavailable for this trash operation" };
|
|
2693
|
+
}
|
|
2388
2694
|
async function handleOpenPath(req) {
|
|
2389
2695
|
if (req.method !== "POST")
|
|
2390
2696
|
return text("method not allowed", 405);
|
|
@@ -2425,6 +2731,87 @@ async function handleOpenPath(req) {
|
|
|
2425
2731
|
openOsPath(target);
|
|
2426
2732
|
return json({ ok: true });
|
|
2427
2733
|
}
|
|
2734
|
+
async function handleTrashPath(req) {
|
|
2735
|
+
if (req.method !== "POST")
|
|
2736
|
+
return text("method not allowed", 405);
|
|
2737
|
+
if (!sideEffectRequestAllowed(req))
|
|
2738
|
+
return text("forbidden", 403);
|
|
2739
|
+
const contentType = req.headers.get("content-type") || "";
|
|
2740
|
+
if (!/^application\/json(?:;|$)/i.test(contentType))
|
|
2741
|
+
return text("unsupported media type", 415);
|
|
2742
|
+
const length = Number(req.headers.get("content-length") || "0");
|
|
2743
|
+
if (length > 1024)
|
|
2744
|
+
return text("payload too large", 413);
|
|
2745
|
+
let body = {};
|
|
2746
|
+
try {
|
|
2747
|
+
const raw = await req.text();
|
|
2748
|
+
if (raw.length > 1024)
|
|
2749
|
+
return text("payload too large", 413);
|
|
2750
|
+
body = JSON.parse(raw);
|
|
2751
|
+
} catch {
|
|
2752
|
+
return text("invalid json", 400);
|
|
2753
|
+
}
|
|
2754
|
+
const path = typeof body.path === "string" ? body.path.replace(/^\/+|\/+$/g, "") : "";
|
|
2755
|
+
if (!path)
|
|
2756
|
+
return text("invalid path", 400);
|
|
2757
|
+
if (!safeRepoPath(path))
|
|
2758
|
+
return text("invalid path", 400);
|
|
2759
|
+
if (isGitInternalPath(path))
|
|
2760
|
+
return text("forbidden", 403);
|
|
2761
|
+
const originalFullPath = safeWorktreePath(path);
|
|
2762
|
+
if (!originalFullPath)
|
|
2763
|
+
return text("not found", 404);
|
|
2764
|
+
const moved = movePathToTrash(worktreePath(path));
|
|
2765
|
+
if (!moved.ok)
|
|
2766
|
+
return text(moved.error || "trash failed", 500);
|
|
2767
|
+
const undo = {
|
|
2768
|
+
id: makeUndoId(),
|
|
2769
|
+
type: "trash",
|
|
2770
|
+
label: `Restore ${path}`,
|
|
2771
|
+
payload: {
|
|
2772
|
+
original_path: path,
|
|
2773
|
+
trashPath: moved.trashPath
|
|
2774
|
+
}
|
|
2775
|
+
};
|
|
2776
|
+
generation++;
|
|
2777
|
+
clearMutableCaches();
|
|
2778
|
+
sendSse("update");
|
|
2779
|
+
return json({ ok: true, generation, undo });
|
|
2780
|
+
}
|
|
2781
|
+
async function handleRestoreTrash(req) {
|
|
2782
|
+
if (req.method !== "POST")
|
|
2783
|
+
return text("method not allowed", 405);
|
|
2784
|
+
if (!sideEffectRequestAllowed(req))
|
|
2785
|
+
return text("forbidden", 403);
|
|
2786
|
+
const contentType = req.headers.get("content-type") || "";
|
|
2787
|
+
if (!/^application\/json(?:;|$)/i.test(contentType))
|
|
2788
|
+
return text("unsupported media type", 415);
|
|
2789
|
+
const length = Number(req.headers.get("content-length") || "0");
|
|
2790
|
+
if (length > 1024)
|
|
2791
|
+
return text("payload too large", 413);
|
|
2792
|
+
let body = {};
|
|
2793
|
+
try {
|
|
2794
|
+
const raw = await req.text();
|
|
2795
|
+
if (raw.length > 1024)
|
|
2796
|
+
return text("payload too large", 413);
|
|
2797
|
+
body = JSON.parse(raw);
|
|
2798
|
+
} catch {
|
|
2799
|
+
return text("invalid json", 400);
|
|
2800
|
+
}
|
|
2801
|
+
const originalPath = typeof body.original_path === "string" ? body.original_path.replace(/^\/+|\/+$/g, "") : "";
|
|
2802
|
+
const trashPath = typeof body.trashPath === "string" ? body.trashPath : "";
|
|
2803
|
+
if (!originalPath || !safeRepoPath(originalPath))
|
|
2804
|
+
return text("invalid restore target", 400);
|
|
2805
|
+
if (isGitInternalPath(originalPath))
|
|
2806
|
+
return text("forbidden", 403);
|
|
2807
|
+
const restored = restoreTrashPath(originalPath, trashPath || undefined);
|
|
2808
|
+
if (!restored.ok)
|
|
2809
|
+
return text(restored.error || "undo failed", 409);
|
|
2810
|
+
generation++;
|
|
2811
|
+
clearMutableCaches();
|
|
2812
|
+
sendSse("update");
|
|
2813
|
+
return json({ ok: true, generation });
|
|
2814
|
+
}
|
|
2428
2815
|
function sendSse(event, data = "tick") {
|
|
2429
2816
|
const payload = enc.encode(`event: ${event}
|
|
2430
2817
|
data: ${data}
|
|
@@ -2473,6 +2860,10 @@ var server = await startServer({
|
|
|
2473
2860
|
return handleRawFile(req, url);
|
|
2474
2861
|
if (url.pathname === "/_open_path")
|
|
2475
2862
|
return handleOpenPath(req);
|
|
2863
|
+
if (url.pathname === "/_trash_path")
|
|
2864
|
+
return handleTrashPath(req);
|
|
2865
|
+
if (url.pathname === "/_restore_trash")
|
|
2866
|
+
return handleRestoreTrash(req);
|
|
2476
2867
|
if (url.pathname === "/_upload_files")
|
|
2477
2868
|
return handleUploadFiles(req);
|
|
2478
2869
|
if (url.pathname === "/_refs")
|
|
@@ -2481,9 +2872,7 @@ var server = await startServer({
|
|
|
2481
2872
|
if (!sideEffectRequestAllowed(req))
|
|
2482
2873
|
return text("forbidden", 403);
|
|
2483
2874
|
generation++;
|
|
2484
|
-
|
|
2485
|
-
metaCache.clear();
|
|
2486
|
-
fileListCache.clear();
|
|
2875
|
+
clearMutableCaches();
|
|
2487
2876
|
sendSse("update");
|
|
2488
2877
|
return json({ ok: true, generation });
|
|
2489
2878
|
}
|