pi-jar 0.1.7 → 0.1.9
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 +15 -12
- package/docs/DIFF-REVIEW.md +5 -2
- package/docs/DISCUSSION_BROKER_PLAN.md +333 -0
- package/docs/INTEGRATIONS.md +1 -1
- package/docs/LONG_SESSION_PERFORMANCE_PLAN.md +731 -0
- package/docs/SUBAGENT-TOOLS.md +2 -0
- package/docs/WORKFLOWS.md +2 -2
- package/extensions/index.ts +153 -63
- package/package.json +1 -1
- package/src/activity-view.ts +10 -6
- package/src/changes.ts +147 -12
- package/src/delegate-worktree.ts +71 -9
- package/src/delegate.ts +734 -423
- package/src/democracy.ts +6 -4
- package/src/diff-inline.ts +80 -0
- package/src/diff-view.ts +196 -59
- package/src/discussion-hub.ts +418 -0
- package/src/discussion.ts +419 -149
- package/src/footer-settings.ts +3 -29
- package/src/perf.ts +419 -0
- package/src/quota.ts +77 -26
- package/src/settings.ts +2 -12
- package/src/shells.ts +134 -28
package/src/changes.ts
CHANGED
|
@@ -15,7 +15,11 @@ const MAX_DIFF_MIDDLE_LINES = 4_000;
|
|
|
15
15
|
export type ChangeStatus = "added" | "modified" | "deleted";
|
|
16
16
|
export interface FileChange { path: string; rel: string; status: ChangeStatus; before: string; after: string; added: number; removed: number; diff?: DiffOp[] | null }
|
|
17
17
|
export type DiffOp = { op: " " | "+" | "-"; text: string };
|
|
18
|
-
|
|
18
|
+
/** `hidden` counts unchanged lines skipped before a hunk; `pair` is the aligned replacement line for intraline diffs. */
|
|
19
|
+
export type DiffRow = { kind: "hunk" | "context" | "add" | "remove" | "note"; text: string; oldLine?: number; newLine?: number; hidden?: number; pair?: string };
|
|
20
|
+
export type SplitSide = { line: number; text: string; changed: boolean };
|
|
21
|
+
/** One aligned Original/Updated row: a replacement run pairs its k-th removed and k-th added line. */
|
|
22
|
+
export type SplitRow = { kind: "hunk"; text: string; hidden: number } | { kind: "pair"; left?: SplitSide; right?: SplitSide };
|
|
19
23
|
|
|
20
24
|
/** Text content, `null` when the file is missing, or `undefined` when it cannot be tracked. */
|
|
21
25
|
function readText(path: string): string | null | undefined {
|
|
@@ -104,7 +108,8 @@ export function diffHunkOffsets(ops: readonly DiffOp[], context = 3): number[] {
|
|
|
104
108
|
while (to < ops.length) {
|
|
105
109
|
let next = to + 1;
|
|
106
110
|
while (next < ops.length && ops[next]!.op === " ") next++;
|
|
107
|
-
|
|
111
|
+
// Merge when the unchanged gap fits inside both hunks' context (adjacent changes at context 0).
|
|
112
|
+
if (next < ops.length && next - to - 1 <= context * 2) { to = next; continue; }
|
|
108
113
|
break;
|
|
109
114
|
}
|
|
110
115
|
const end = Math.min(ops.length, to + context + 1);
|
|
@@ -122,14 +127,7 @@ export function diffRowCount(ops: readonly DiffOp[], context = 3): number {
|
|
|
122
127
|
return hunkCache.get(ops)!.rows;
|
|
123
128
|
}
|
|
124
129
|
|
|
125
|
-
|
|
126
|
-
export function diffRowsWindow(ops: readonly DiffOp[], context = 3, offset = 0, limit = Number.MAX_SAFE_INTEGER): DiffRow[] {
|
|
127
|
-
const rows: DiffRow[] = [];
|
|
128
|
-
const firstRow = Math.max(0, offset);
|
|
129
|
-
const lastRow = firstRow + Math.max(0, limit);
|
|
130
|
-
if (hunkCache.get(ops)?.context !== context) diffHunkOffsets(ops, context);
|
|
131
|
-
const layout = hunkCache.get(ops)!;
|
|
132
|
-
if (firstRow >= layout.rows || lastRow <= firstRow) return rows;
|
|
130
|
+
const linePositions = (ops: readonly DiffOp[]) => {
|
|
133
131
|
let positions = linePositionCache.get(ops);
|
|
134
132
|
if (!positions) {
|
|
135
133
|
const old = new Uint32Array(ops.length + 1), next = new Uint32Array(ops.length + 1);
|
|
@@ -145,6 +143,57 @@ export function diffRowsWindow(ops: readonly DiffOp[], context = 3, offset = 0,
|
|
|
145
143
|
positions = { old, next };
|
|
146
144
|
linePositionCache.set(ops, positions);
|
|
147
145
|
}
|
|
146
|
+
return positions;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Replacement runs (maximal non-context spans) as flat typed arrays, independent of context:
|
|
151
|
+
* `order[start..start+removes)` lists the run's removed ops and the rest its added ops, so the
|
|
152
|
+
* k-th removal pairs with the k-th addition. `split` prefixes aligned rows (a run takes max(r, a)).
|
|
153
|
+
*/
|
|
154
|
+
type RunLayout = { start: Uint32Array; removes: Uint32Array; length: Uint32Array; order: Uint32Array; ordinal: Uint32Array; split: Uint32Array };
|
|
155
|
+
const runCache = new WeakMap<readonly DiffOp[], RunLayout>();
|
|
156
|
+
|
|
157
|
+
const runLayout = (ops: readonly DiffOp[]): RunLayout => {
|
|
158
|
+
const cached = runCache.get(ops);
|
|
159
|
+
if (cached) return cached;
|
|
160
|
+
const n = ops.length;
|
|
161
|
+
const layout: RunLayout = { start: new Uint32Array(n), removes: new Uint32Array(n), length: new Uint32Array(n),
|
|
162
|
+
order: new Uint32Array(n), ordinal: new Uint32Array(n), split: new Uint32Array(n + 1) };
|
|
163
|
+
const { start, removes, length, order, ordinal, split } = layout;
|
|
164
|
+
for (let at = 0; at < n;) {
|
|
165
|
+
if (ops[at]!.op === " ") { order[at] = at; split[at + 1] = split[at]! + 1; at++; continue; }
|
|
166
|
+
let end = at;
|
|
167
|
+
while (end < n && ops[end]!.op !== " ") end++;
|
|
168
|
+
let r = 0, a = 0;
|
|
169
|
+
for (let i = at; i < end; i++) if (ops[i]!.op === "-") { ordinal[i] = r; order[at + r++] = i; }
|
|
170
|
+
for (let i = at; i < end; i++) if (ops[i]!.op === "+") { ordinal[i] = a; order[at + r + a++] = i; }
|
|
171
|
+
for (let i = at; i < end; i++) { start[i] = at; removes[i] = r; length[i] = end - at; split[i + 1] = split[at]!; }
|
|
172
|
+
split[at + 1] = split[at]! + Math.max(r, a);
|
|
173
|
+
for (let i = at + 1; i < end; i++) split[i + 1] = split[at + 1]!;
|
|
174
|
+
at = end;
|
|
175
|
+
}
|
|
176
|
+
runCache.set(ops, layout);
|
|
177
|
+
return layout;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/** The aligned counterpart of a changed op (k-th removal ↔ k-th addition in its run), if any. */
|
|
181
|
+
const partner = (ops: readonly DiffOp[], at: number): number | undefined => {
|
|
182
|
+
const { start, removes, length, order, ordinal } = runLayout(ops);
|
|
183
|
+
const s = start[at]!, r = removes[at]!, a = length[at]! - r, k = ordinal[at]!;
|
|
184
|
+
if (ops[at]!.op === "-") return k < a ? order[s + r + k] : undefined;
|
|
185
|
+
return k < r ? order[s + k] : undefined;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/** Compute only the requested output window; diff operations stay indexed without a second full row array. */
|
|
189
|
+
export function diffRowsWindow(ops: readonly DiffOp[], context = 3, offset = 0, limit = Number.MAX_SAFE_INTEGER): DiffRow[] {
|
|
190
|
+
const rows: DiffRow[] = [];
|
|
191
|
+
const firstRow = Math.max(0, offset);
|
|
192
|
+
const lastRow = firstRow + Math.max(0, limit);
|
|
193
|
+
if (hunkCache.get(ops)?.context !== context) diffHunkOffsets(ops, context);
|
|
194
|
+
const layout = hunkCache.get(ops)!;
|
|
195
|
+
if (firstRow >= layout.rows || lastRow <= firstRow) return rows;
|
|
196
|
+
const positions = linePositions(ops);
|
|
148
197
|
// Binary-search the first intersecting hunk, then visit only visible operations. In
|
|
149
198
|
// particular, a 100k-line replacement must not slice/filter/allocate 100k rows per frame.
|
|
150
199
|
let lo = 0, hi = layout.hunks.length;
|
|
@@ -157,13 +206,99 @@ export function diffRowsWindow(ops: readonly DiffOp[], context = 3, offset = 0,
|
|
|
157
206
|
for (let index = lo; index < layout.hunks.length; index++) {
|
|
158
207
|
const { from, end, offset: start } = layout.hunks[index]!;
|
|
159
208
|
if (start >= lastRow) break;
|
|
160
|
-
if (start >= firstRow) rows.push({ kind: "hunk", text:
|
|
209
|
+
if (start >= firstRow) rows.push({ kind: "hunk", text: hunkHeader(positions, from, end), hidden: from - (layout.hunks[index - 1]?.end ?? 0) });
|
|
161
210
|
const visibleFrom = from + Math.max(0, firstRow - start - 1);
|
|
162
211
|
const visibleEnd = Math.min(end, from + lastRow - start - 1);
|
|
163
212
|
for (let at = visibleFrom; at < visibleEnd; at++) {
|
|
164
213
|
const op = ops[at]!;
|
|
214
|
+
const pair = op.op === " " ? undefined : partner(ops, at);
|
|
165
215
|
rows.push({ kind: op.op === "+" ? "add" : op.op === "-" ? "remove" : "context", text: op.text,
|
|
166
|
-
...(op.op !== "+" ? { oldLine: positions.old[at]! } : {}), ...(op.op !== "-" ? { newLine: positions.next[at]! } : {})
|
|
216
|
+
...(op.op !== "+" ? { oldLine: positions.old[at]! } : {}), ...(op.op !== "-" ? { newLine: positions.next[at]! } : {}),
|
|
217
|
+
...(pair !== undefined ? { pair: ops[pair]!.text } : {}) });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return rows;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Git convention: an empty side (pure insertion/deletion) names the line before the change. */
|
|
224
|
+
const hunkHeader = (positions: { old: Uint32Array; next: Uint32Array }, from: number, end: number) => {
|
|
225
|
+
const range = (lines: Uint32Array) => {
|
|
226
|
+
const count = lines[end]! - lines[from]!;
|
|
227
|
+
return `${count ? lines[from]! : lines[from]! - 1},${count}`;
|
|
228
|
+
};
|
|
229
|
+
return `@@ -${range(positions.old)} +${range(positions.next)} @@`;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
type SplitHunk = DiffHunk & { split: number };
|
|
233
|
+
const splitCache = new WeakMap<readonly DiffOp[], { context: number; hunks: SplitHunk[]; offsets: number[]; rows: number }>();
|
|
234
|
+
|
|
235
|
+
const splitLayout = (ops: readonly DiffOp[], context: number) => {
|
|
236
|
+
const cached = splitCache.get(ops);
|
|
237
|
+
if (cached?.context === context) return cached;
|
|
238
|
+
if (hunkCache.get(ops)?.context !== context) diffHunkOffsets(ops, context);
|
|
239
|
+
const { split } = runLayout(ops);
|
|
240
|
+
const hunks: SplitHunk[] = [];
|
|
241
|
+
let rows = 0;
|
|
242
|
+
// Hunks start and end on context ops, so a replacement run never straddles two hunks.
|
|
243
|
+
for (const hunk of hunkCache.get(ops)!.hunks) {
|
|
244
|
+
hunks.push({ ...hunk, split: rows });
|
|
245
|
+
rows += 1 + split[hunk.end]! - split[hunk.from]!;
|
|
246
|
+
}
|
|
247
|
+
const layout = { context, hunks, offsets: hunks.map(hunk => hunk.split), rows };
|
|
248
|
+
splitCache.set(ops, layout);
|
|
249
|
+
return layout;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
/** Aligned side-by-side row offsets of each hunk header. */
|
|
253
|
+
export function diffSplitHunkOffsets(ops: readonly DiffOp[], context = 3): number[] {
|
|
254
|
+
return splitLayout(ops, context).offsets.slice();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function diffSplitRowCount(ops: readonly DiffOp[], context = 3): number {
|
|
258
|
+
return splitLayout(ops, context).rows;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Window of aligned Original/Updated rows; like `diffRowsWindow`, it visits only visible operations. */
|
|
262
|
+
export function diffSplitRowsWindow(ops: readonly DiffOp[], context = 3, offset = 0, limit = Number.MAX_SAFE_INTEGER): SplitRow[] {
|
|
263
|
+
const rows: SplitRow[] = [];
|
|
264
|
+
const firstRow = Math.max(0, offset);
|
|
265
|
+
const lastRow = firstRow + Math.max(0, limit);
|
|
266
|
+
const layout = splitLayout(ops, context);
|
|
267
|
+
if (firstRow >= layout.rows || lastRow <= firstRow) return rows;
|
|
268
|
+
const positions = linePositions(ops);
|
|
269
|
+
const { split, order, removes, length } = runLayout(ops);
|
|
270
|
+
const size = (hunk: SplitHunk) => 1 + split[hunk.end]! - split[hunk.from]!;
|
|
271
|
+
let lo = 0, hi = layout.hunks.length;
|
|
272
|
+
while (lo < hi) {
|
|
273
|
+
const mid = (lo + hi) >>> 1;
|
|
274
|
+
if (layout.hunks[mid]!.split + size(layout.hunks[mid]!) <= firstRow) lo = mid + 1;
|
|
275
|
+
else hi = mid;
|
|
276
|
+
}
|
|
277
|
+
for (let index = lo; index < layout.hunks.length; index++) {
|
|
278
|
+
const hunk = layout.hunks[index]!;
|
|
279
|
+
if (hunk.split >= lastRow) break;
|
|
280
|
+
if (hunk.split >= firstRow) rows.push({ kind: "hunk", text: hunkHeader(positions, hunk.from, hunk.end), hidden: hunk.from - (layout.hunks[index - 1]?.end ?? 0) });
|
|
281
|
+
const visibleFrom = Math.max(0, firstRow - hunk.split - 1);
|
|
282
|
+
const visibleEnd = Math.min(size(hunk) - 1, lastRow - hunk.split - 1);
|
|
283
|
+
for (let row = visibleFrom; row < visibleEnd; row++) {
|
|
284
|
+
const target = split[hunk.from]! + row;
|
|
285
|
+
// First op whose aligned-row interval contains target; only run starts carry run rows.
|
|
286
|
+
let low = hunk.from, high = hunk.end - 1;
|
|
287
|
+
while (low < high) {
|
|
288
|
+
const mid = (low + high) >>> 1;
|
|
289
|
+
if (split[mid + 1]! > target) high = mid; else low = mid + 1;
|
|
290
|
+
}
|
|
291
|
+
const op = ops[low]!;
|
|
292
|
+
if (op.op === " ") {
|
|
293
|
+
rows.push({ kind: "pair", left: { line: positions.old[low]!, text: op.text, changed: false }, right: { line: positions.next[low]!, text: op.text, changed: false } });
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
const k = target - split[low]!, r = removes[low]!, a = length[low]! - r;
|
|
297
|
+
const removed = k < r ? order[low + k]! : undefined;
|
|
298
|
+
const added = k < a ? order[low + r + k]! : undefined;
|
|
299
|
+
rows.push({ kind: "pair",
|
|
300
|
+
...(removed !== undefined ? { left: { line: positions.old[removed]!, text: ops[removed]!.text, changed: true } } : {}),
|
|
301
|
+
...(added !== undefined ? { right: { line: positions.next[added]!, text: ops[added]!.text, changed: true } } : {}) });
|
|
167
302
|
}
|
|
168
303
|
}
|
|
169
304
|
return rows;
|
package/src/delegate-worktree.ts
CHANGED
|
@@ -154,6 +154,31 @@ const writeSnapshot = async (root: string, rel: string, bytes: Buffer, stat: Sta
|
|
|
154
154
|
}
|
|
155
155
|
};
|
|
156
156
|
|
|
157
|
+
// Each git call has its own GIT_TIMEOUT_MS deadline, so bound the work per batched call.
|
|
158
|
+
const HASH_BATCH_FILES = 1000;
|
|
159
|
+
const HASH_BATCH_BYTES = 128 * 1024 * 1024;
|
|
160
|
+
|
|
161
|
+
/** Object ids for regular files, in order, from bounded `hash-object --stdin-paths` chunks. */
|
|
162
|
+
const hashPaths = async (root: string, files: readonly { path: string; size: number }[], write: boolean,
|
|
163
|
+
signal?: AbortSignal): Promise<string[]> => {
|
|
164
|
+
const oids: string[] = [];
|
|
165
|
+
for (let start = 0; start < files.length;) {
|
|
166
|
+
let end = start;
|
|
167
|
+
let bytes = 0;
|
|
168
|
+
// A file larger than the byte budget still gets its own chunk, matching per-file hashing.
|
|
169
|
+
while (end < files.length && end - start < HASH_BATCH_FILES && (end === start || bytes + files[end]!.size <= HASH_BATCH_BYTES)) {
|
|
170
|
+
bytes += files[end++]!.size;
|
|
171
|
+
}
|
|
172
|
+
const chunk = files.slice(start, end);
|
|
173
|
+
const output = (await gitText(root, ["hash-object", ...(write ? ["-w"] : []), "--no-filters", "--stdin-paths"],
|
|
174
|
+
chunk.map(file => file.path).join("\n") + "\n", signal)).split("\n").filter(Boolean);
|
|
175
|
+
if (output.length !== chunk.length) throw new Error("git hash-object returned incomplete hashes");
|
|
176
|
+
oids.push(...output);
|
|
177
|
+
start = end;
|
|
178
|
+
}
|
|
179
|
+
return oids;
|
|
180
|
+
};
|
|
181
|
+
|
|
157
182
|
/**
|
|
158
183
|
* Create a detached disposable worktree and commit a private baseline snapshot containing the
|
|
159
184
|
* parent's tracked modifications plus non-ignored untracked files. The parent index/branch is not
|
|
@@ -173,6 +198,10 @@ export async function createDelegateWorktree(cwd: string, signal?: AbortSignal):
|
|
|
173
198
|
added = true;
|
|
174
199
|
await gitText(root, ["read-tree", "--empty"], undefined, signal);
|
|
175
200
|
const paths = new Set(nulList(await gitText(repoRoot, ["ls-files", "--cached", "--others", "--exclude-standard", "-z"], undefined, signal)));
|
|
201
|
+
// Git process startup (often behind wrappers such as git-ai) dominates setup time, so hash
|
|
202
|
+
// and stage in bounded batches rather than two git processes per repository file.
|
|
203
|
+
const entries: { rel: string; mode: string; oid: string }[] = [];
|
|
204
|
+
const batched: { entry: (typeof entries)[number]; path: string; size: number }[] = [];
|
|
176
205
|
for (const rel of paths) {
|
|
177
206
|
if (!safeFilePath(repoRoot, rel)) throw new Error("unsafe snapshot path: " + rel);
|
|
178
207
|
const source = resolve(repoRoot, rel);
|
|
@@ -180,11 +209,22 @@ export async function createDelegateWorktree(cwd: string, signal?: AbortSignal):
|
|
|
180
209
|
if (!stat) continue;
|
|
181
210
|
if (!stat.isFile() && !stat.isSymbolicLink()) throw new Error("unsupported snapshot file: " + rel);
|
|
182
211
|
const bytes = stat.isSymbolicLink() ? Buffer.from(await readlink(source)) : await readFile(source);
|
|
183
|
-
const oid = (await gitText(root, ["hash-object", "-w", "--no-filters", "--stdin"], bytes, signal)).trim();
|
|
184
|
-
const mode = stat.isSymbolicLink() ? "120000" : (stat.mode & 0o111) ? "100755" : "100644";
|
|
185
|
-
await gitText(root, ["update-index", "--add", "--cacheinfo", mode, oid, rel], undefined, signal);
|
|
186
212
|
signal?.throwIfAborted();
|
|
187
213
|
await writeSnapshot(root, rel, bytes, stat);
|
|
214
|
+
const entry = { rel, mode: stat.isSymbolicLink() ? "120000" : (stat.mode & 0o111) ? "100755" : "100644", oid: "" };
|
|
215
|
+
entries.push(entry);
|
|
216
|
+
// The private copy holds exactly the bytes read above. Hashing it by path would follow a
|
|
217
|
+
// symlink, and --stdin-paths is line-based, so those entries hash their bytes directly.
|
|
218
|
+
const path = resolve(root, rel);
|
|
219
|
+
if (stat.isSymbolicLink() || path.includes("\n")) {
|
|
220
|
+
entry.oid = (await gitText(root, ["hash-object", "-w", "--no-filters", "--stdin"], bytes, signal)).trim();
|
|
221
|
+
} else batched.push({ entry, path, size: bytes.length });
|
|
222
|
+
}
|
|
223
|
+
const oids = await hashPaths(root, batched, true, signal);
|
|
224
|
+
batched.forEach((item, index) => { item.entry.oid = oids[index]!; });
|
|
225
|
+
if (entries.length) {
|
|
226
|
+
await gitText(root, ["update-index", "-z", "--index-info"],
|
|
227
|
+
entries.map(entry => `${entry.mode} ${entry.oid}\t${entry.rel}\0`).join(""), signal);
|
|
188
228
|
}
|
|
189
229
|
const tree = (await gitText(root, ["write-tree"], undefined, signal)).trim();
|
|
190
230
|
const baseline = (await gitText(root, ["-c", "user.name=pi-jar", "-c", "user.email=pi-jar@local",
|
|
@@ -205,12 +245,37 @@ export async function createDelegateWorktree(cwd: string, signal?: AbortSignal):
|
|
|
205
245
|
|
|
206
246
|
/** Files whose final worktree state differs from the snapshot, including new untracked files. */
|
|
207
247
|
export async function worktreeChangedFiles(worktree: DelegateWorktree, signal?: AbortSignal): Promise<string[]> {
|
|
208
|
-
const
|
|
248
|
+
const baseline = new Map<string, { mode: string; oid: string }>();
|
|
249
|
+
for (const record of nulList(await gitText(worktree.root, ["ls-tree", "-r", "-z", worktree.baseline], undefined, signal))) {
|
|
250
|
+
const tab = record.indexOf("\t");
|
|
251
|
+
const [mode, type, oid] = record.slice(0, tab).split(" ");
|
|
252
|
+
if (type !== "blob" || !mode || !oid) throw new Error("unsupported baseline entry: " + record.slice(tab + 1));
|
|
253
|
+
baseline.set(record.slice(tab + 1), { mode, oid });
|
|
254
|
+
}
|
|
209
255
|
const currentPaths = nulList(await gitText(worktree.root, ["ls-files", "--cached", "--others", "--exclude-standard", "-z"], undefined, signal));
|
|
210
256
|
const changed: string[] = [];
|
|
211
|
-
|
|
212
|
-
|
|
257
|
+
// Compare object ids from bounded hash-object batches instead of two git processes per file.
|
|
258
|
+
const batched: { rel: string; oid: string; path: string; size: number }[] = [];
|
|
259
|
+
const hashBytes = async (bytes: Buffer) => (await gitText(worktree.root, ["hash-object", "--no-filters", "--stdin"], bytes, signal)).trim();
|
|
260
|
+
for (const rel of new Set([...baseline.keys(), ...currentPaths])) {
|
|
261
|
+
const entry = baseline.get(rel);
|
|
262
|
+
const target = resolve(worktree.root, rel);
|
|
263
|
+
if (!safeFilePath(worktree.root, rel)) { changed.push(rel); continue; }
|
|
264
|
+
const stat = statIfPresent(target);
|
|
265
|
+
if (!entry || !stat) {
|
|
266
|
+
if (entry || stat) changed.push(rel);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (entry.mode === "120000") {
|
|
270
|
+
if (!stat.isSymbolicLink() || await hashBytes(Buffer.from(await readlink(target))) !== entry.oid) changed.push(rel);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (!stat.isFile() || (entry.mode === "100755") !== ((stat.mode & 0o111) !== 0)) changed.push(rel);
|
|
274
|
+
else if (target.includes("\n")) { if (await hashBytes(await readFile(target)) !== entry.oid) changed.push(rel); }
|
|
275
|
+
else batched.push({ rel, oid: entry.oid, path: target, size: stat.size });
|
|
213
276
|
}
|
|
277
|
+
const oids = await hashPaths(worktree.root, batched, false, signal);
|
|
278
|
+
batched.forEach((item, index) => { if (oids[index] !== item.oid) changed.push(item.rel); });
|
|
214
279
|
return changed.sort();
|
|
215
280
|
}
|
|
216
281
|
|
|
@@ -224,9 +289,6 @@ const baselineEntry = async (worktree: DelegateWorktree, rel: string, signal?: A
|
|
|
224
289
|
return { mode, content: await gitBuffer(worktree.root, ["cat-file", "blob", oid], signal) };
|
|
225
290
|
};
|
|
226
291
|
|
|
227
|
-
const parentMatchesBaseline = async (worktree: DelegateWorktree, parentRoot: string, rel: string, signal?: AbortSignal): Promise<boolean> =>
|
|
228
|
-
matchesBaseline(await baselineEntry(worktree, rel, signal), parentRoot, rel);
|
|
229
|
-
|
|
230
292
|
const matchesBaseline = (baseline: BaselineEntry, parentRoot: string, rel: string): boolean => {
|
|
231
293
|
const target = resolve(parentRoot, rel);
|
|
232
294
|
if (!safeFilePath(parentRoot, rel)) return false;
|