@animalabs/agent-framework 0.27.0 → 0.28.0
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 +56 -15
- package/dist/src/agent.d.ts +29 -2
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +69 -11
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/server.d.ts.map +1 -1
- package/dist/src/api/server.js +4 -2
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/auto-continue.d.ts +3 -2
- package/dist/src/auto-continue.d.ts.map +1 -1
- package/dist/src/auto-continue.js +4 -3
- package/dist/src/auto-continue.js.map +1 -1
- package/dist/src/cut-off-round.d.ts +24 -0
- package/dist/src/cut-off-round.d.ts.map +1 -0
- package/dist/src/cut-off-round.js +87 -0
- package/dist/src/cut-off-round.js.map +1 -0
- package/dist/src/framework.d.ts +304 -89
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +2577 -678
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +99 -3
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +538 -13
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/index.d.ts +2 -1
- package/dist/src/gate/index.d.ts.map +1 -1
- package/dist/src/gate/index.js +1 -0
- package/dist/src/gate/index.js.map +1 -1
- package/dist/src/gate/reply-judge.d.ts +116 -0
- package/dist/src/gate/reply-judge.d.ts.map +1 -0
- package/dist/src/gate/reply-judge.js +467 -0
- package/dist/src/gate/reply-judge.js.map +1 -0
- package/dist/src/gate/types.d.ts +161 -0
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/heartbeat/heartbeat.d.ts +184 -0
- package/dist/src/heartbeat/heartbeat.d.ts.map +1 -0
- package/dist/src/heartbeat/heartbeat.js +752 -0
- package/dist/src/heartbeat/heartbeat.js.map +1 -0
- package/dist/src/heartbeat/reminder-time.d.ts +5 -0
- package/dist/src/heartbeat/reminder-time.d.ts.map +1 -0
- package/dist/src/heartbeat/reminder-time.js +107 -0
- package/dist/src/heartbeat/reminder-time.js.map +1 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/capability-mask.d.ts.map +1 -1
- package/dist/src/mcpl/capability-mask.js +3 -2
- package/dist/src/mcpl/capability-mask.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +2 -1
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +26 -10
- package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
- package/dist/src/mcpl/inbound-source.js +40 -69
- package/dist/src/mcpl/inbound-source.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +18 -8
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +50 -10
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/tags.d.ts +9 -2
- package/dist/src/mcpl/tags.d.ts.map +1 -1
- package/dist/src/mcpl/tags.js +12 -2
- package/dist/src/mcpl/tags.js.map +1 -1
- package/dist/src/mcpl/tool-presence.d.ts +66 -14
- package/dist/src/mcpl/tool-presence.d.ts.map +1 -1
- package/dist/src/mcpl/tool-presence.js +134 -65
- package/dist/src/mcpl/tool-presence.js.map +1 -1
- package/dist/src/mcpl/tool-signature.d.ts +10 -3
- package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
- package/dist/src/mcpl/tool-signature.js +47 -18
- package/dist/src/mcpl/tool-signature.js.map +1 -1
- package/dist/src/mcpl/toolset-host.d.ts +88 -37
- package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
- package/dist/src/mcpl/toolset-host.js +207 -108
- package/dist/src/mcpl/toolset-host.js.map +1 -1
- package/dist/src/mcpl/toolsets-module.d.ts +14 -11
- package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
- package/dist/src/mcpl/toolsets-module.js +141 -49
- package/dist/src/mcpl/toolsets-module.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +3 -1
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/module-registry.d.ts +4 -1
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +9 -2
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/history/index.d.ts.map +1 -1
- package/dist/src/modules/history/index.js +22 -11
- package/dist/src/modules/history/index.js.map +1 -1
- package/dist/src/modules/workspace/answers.d.ts +22 -0
- package/dist/src/modules/workspace/answers.d.ts.map +1 -0
- package/dist/src/modules/workspace/answers.js +123 -0
- package/dist/src/modules/workspace/answers.js.map +1 -0
- package/dist/src/modules/workspace/branch-intent.d.ts +5 -3
- package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
- package/dist/src/modules/workspace/branch-intent.js +6 -2
- package/dist/src/modules/workspace/branch-intent.js.map +1 -1
- package/dist/src/modules/workspace/direct-write.d.ts +3 -0
- package/dist/src/modules/workspace/direct-write.d.ts.map +1 -1
- package/dist/src/modules/workspace/direct-write.js +57 -24
- package/dist/src/modules/workspace/direct-write.js.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.d.ts +33 -11
- package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.js +48 -17
- package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
- package/dist/src/modules/workspace/edit-text.d.ts +2 -1
- package/dist/src/modules/workspace/edit-text.d.ts.map +1 -1
- package/dist/src/modules/workspace/edit-text.js +4 -2
- package/dist/src/modules/workspace/edit-text.js.map +1 -1
- package/dist/src/modules/workspace/effects.d.ts.map +1 -1
- package/dist/src/modules/workspace/effects.js +20 -13
- package/dist/src/modules/workspace/effects.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +62 -51
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +934 -531
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +75 -63
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +366 -180
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/read-page.d.ts.map +1 -1
- package/dist/src/modules/workspace/read-page.js +7 -1
- package/dist/src/modules/workspace/read-page.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +26 -16
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/modules/workspace/result-text.d.ts +1 -0
- package/dist/src/modules/workspace/result-text.d.ts.map +1 -1
- package/dist/src/modules/workspace/result-text.js.map +1 -1
- package/dist/src/modules/workspace/sync.d.ts +9 -0
- package/dist/src/modules/workspace/sync.d.ts.map +1 -1
- package/dist/src/modules/workspace/sync.js +14 -0
- package/dist/src/modules/workspace/sync.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +96 -43
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js +8 -3
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/prose-drafts.d.ts +4 -3
- package/dist/src/prose-drafts.d.ts.map +1 -1
- package/dist/src/prose-drafts.js +3 -2
- package/dist/src/prose-drafts.js.map +1 -1
- package/dist/src/reasoning-loss-notice.d.ts +23 -0
- package/dist/src/reasoning-loss-notice.d.ts.map +1 -0
- package/dist/src/reasoning-loss-notice.js +50 -0
- package/dist/src/reasoning-loss-notice.js.map +1 -0
- package/dist/src/reasoning-loss.d.ts +24 -0
- package/dist/src/reasoning-loss.d.ts.map +1 -0
- package/dist/src/reasoning-loss.js +102 -0
- package/dist/src/reasoning-loss.js.map +1 -0
- package/dist/src/send-tools.d.ts +3 -2
- package/dist/src/send-tools.d.ts.map +1 -1
- package/dist/src/send-tools.js +3 -2
- package/dist/src/send-tools.js.map +1 -1
- package/dist/src/sent-rows.js +1 -1
- package/dist/src/thinking-drops.d.ts +4 -11
- package/dist/src/thinking-drops.d.ts.map +1 -1
- package/dist/src/thinking-drops.js +7 -14
- package/dist/src/thinking-drops.js.map +1 -1
- package/dist/src/tool-args.d.ts +12 -0
- package/dist/src/tool-args.d.ts.map +1 -1
- package/dist/src/tool-args.js +35 -1
- package/dist/src/tool-args.js.map +1 -1
- package/dist/src/tool-result-guard.d.ts +8 -4
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +23 -11
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tune-out/tools.d.ts +4 -1
- package/dist/src/tune-out/tools.d.ts.map +1 -1
- package/dist/src/types/agent.d.ts +40 -9
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +18 -8
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +15 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +5 -2
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +34 -1
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agent.ts +72 -11
- package/src/api/server.ts +4 -2
- package/src/auto-continue.ts +4 -3
- package/src/cut-off-round.ts +98 -0
- package/src/framework.ts +2694 -700
- package/src/gate/event-gate.ts +579 -15
- package/src/gate/index.ts +8 -0
- package/src/gate/reply-judge.ts +512 -0
- package/src/gate/types.ts +164 -1
- package/src/heartbeat/heartbeat.ts +804 -0
- package/src/heartbeat/reminder-time.ts +107 -0
- package/src/index.ts +4 -0
- package/src/mcpl/capability-mask.ts +3 -2
- package/src/mcpl/channel-registry.ts +2 -1
- package/src/mcpl/inbound-source.ts +42 -69
- package/src/mcpl/server-connection.ts +56 -16
- package/src/mcpl/tags.ts +13 -2
- package/src/mcpl/tool-presence.ts +133 -67
- package/src/mcpl/tool-signature.ts +51 -20
- package/src/mcpl/toolset-host.ts +197 -107
- package/src/mcpl/toolsets-module.ts +143 -49
- package/src/mcpl/types.ts +3 -1
- package/src/module-registry.ts +8 -2
- package/src/modules/history/index.ts +22 -12
- package/src/modules/workspace/answers.ts +133 -0
- package/src/modules/workspace/branch-intent.ts +10 -5
- package/src/modules/workspace/direct-write.ts +58 -23
- package/src/modules/workspace/disk-agreement.ts +52 -21
- package/src/modules/workspace/edit-text.ts +7 -5
- package/src/modules/workspace/effects.ts +21 -12
- package/src/modules/workspace/index.ts +927 -541
- package/src/modules/workspace/observe.ts +381 -177
- package/src/modules/workspace/read-page.ts +7 -1
- package/src/modules/workspace/reconcile.ts +27 -15
- package/src/modules/workspace/result-text.ts +2 -0
- package/src/modules/workspace/sync.ts +16 -0
- package/src/modules/workspace/types.ts +96 -43
- package/src/modules/workspace/watcher.ts +8 -3
- package/src/prose-drafts.ts +4 -3
- package/src/reasoning-loss-notice.ts +62 -0
- package/src/reasoning-loss.ts +120 -0
- package/src/send-tools.ts +3 -2
- package/src/sent-rows.ts +1 -1
- package/src/thinking-drops.ts +10 -16
- package/src/tool-args.ts +34 -1
- package/src/tool-result-guard.ts +26 -11
- package/src/tune-out/tools.ts +4 -1
- package/src/types/agent.ts +40 -9
- package/src/types/events.ts +18 -8
- package/src/types/framework.ts +15 -0
- package/src/types/module.ts +5 -2
- package/src/types/trace.ts +36 -1
|
@@ -44,6 +44,7 @@ import { constants as fsConstants } from 'node:fs';
|
|
|
44
44
|
import { lstat, mkdir, open, readdir, readlink, realpath, stat, statfs } from 'node:fs/promises';
|
|
45
45
|
import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep } from 'node:path';
|
|
46
46
|
import { sameRootIdentity } from './disk-agreement.js';
|
|
47
|
+
import { errnoCode, systemWords } from './answers.js';
|
|
47
48
|
/**
|
|
48
49
|
* Opened with every open of a workspace path that may already be there, a
|
|
49
50
|
* read, a write in place or a directory's sync, where the platform has it:
|
|
@@ -61,10 +62,14 @@ export const HEAD_BYTES = 8192;
|
|
|
61
62
|
export const RACY_WINDOW_MS = 2000;
|
|
62
63
|
export const OUTSIDE_PARENT = 'a parent directory resolves outside the mount';
|
|
63
64
|
export const NOT_FOLLOWED = 'a symlink, which this mount does not follow';
|
|
65
|
+
export const DIRECTORY = 'a directory';
|
|
66
|
+
export const NOT_REGULAR = 'not a regular file';
|
|
67
|
+
export const LEAVES_MOUNT = 'a symlink that leaves the mount';
|
|
68
|
+
export const NOT_A_DIRECTORY = 'a parent path is not a directory';
|
|
64
69
|
export const ROOT_UNAVAILABLE = 'the mount root is unavailable';
|
|
65
70
|
export const ROOT_REPLACED = 'the mount root is not the directory disk last agreed with (an unmounted drive, or a replaced directory): ' +
|
|
66
71
|
'reconnect it; if it was replaced on purpose, sync the mount with acceptRoot to take it as it is now';
|
|
67
|
-
const CHANGED = 'it changed while it was observed';
|
|
72
|
+
export const CHANGED = 'it changed while it was observed';
|
|
68
73
|
const CHANGED_WALK = 'the directory changed while the walk ran';
|
|
69
74
|
const BENEATH_SYMLINK = 'beneath a symlink that does not lead to a directory inside the mount';
|
|
70
75
|
export function errno(err) {
|
|
@@ -166,19 +171,19 @@ export async function linkLeads(root, rootReal, link) {
|
|
|
166
171
|
* does.
|
|
167
172
|
*/
|
|
168
173
|
export function nestedHolder(target, nested) {
|
|
169
|
-
return nested
|
|
170
|
-
.filter((n) => target === n.dir || target.startsWith(`${n.dir}/`))
|
|
171
|
-
.sort((a, b) => b.dir.length - a.dir.length)[0];
|
|
174
|
+
return nestedHolders(target, nested)[0];
|
|
172
175
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
176
|
+
function nestedHolders(target, nested) {
|
|
177
|
+
const segments = target.split('/');
|
|
178
|
+
return mayHold(nested.filter((n) => beneathNested(segments, n)), (n) => n.dir.split('/').length, (n) => !n.guessed && surelyBeneath(segments, n.dir.split('/')));
|
|
179
|
+
}
|
|
180
|
+
function mayHold(holders, depth, sure) {
|
|
181
|
+
const sorted = [...holders].sort((a, b) => depth(b) - depth(a) || Number(sure(b)) - Number(sure(a)));
|
|
182
|
+
const surely = sorted.findIndex(sure);
|
|
183
|
+
return surely === -1 ? sorted : sorted.slice(0, surely + 1);
|
|
184
|
+
}
|
|
185
|
+
export function namesHolder(leads, nested) {
|
|
186
|
+
return nestedHolders(leads.target, nested).every((holder) => namesNestedPlace(restBeneath(leads.target, holder), holder.include, leads.directory));
|
|
182
187
|
}
|
|
183
188
|
/**
|
|
184
189
|
* Whether a refusal names a place in a nested mount's directory, `rest`
|
|
@@ -195,19 +200,13 @@ export function namesHolder(leads, holder) {
|
|
|
195
200
|
export function namesNestedPlace(rest, include, directory) {
|
|
196
201
|
return directory || insideMount(rest, include);
|
|
197
202
|
}
|
|
198
|
-
/**
|
|
199
|
-
* Whether a path of a mount is one its passes decide and its listings may
|
|
200
|
-
* name: inside its include, and, in a nested mount's directory, a place
|
|
201
|
-
* that mount names (namesNestedPlace), its directory included. History the
|
|
202
|
-
* mount took in before another was nested there, which that mount's
|
|
203
|
-
* include leaves out, is no mount's, so no pass decides it, and no listing
|
|
204
|
-
* names it or a directory only it implies.
|
|
205
|
-
*/
|
|
206
203
|
export function withinIncludes(view, path) {
|
|
207
204
|
if (!insideMount(path, view.include))
|
|
208
205
|
return false;
|
|
209
|
-
|
|
210
|
-
|
|
206
|
+
return nestedHolders(path, view.nested ?? []).every((holder) => {
|
|
207
|
+
const rest = restBeneath(path, holder);
|
|
208
|
+
return rest === '' || namesNestedPlace(rest, holder.include, false);
|
|
209
|
+
});
|
|
211
210
|
}
|
|
212
211
|
/**
|
|
213
212
|
* The path a mount's include judges for `given`, one of the mount's paths,
|
|
@@ -300,7 +299,7 @@ export async function coversLanding(view, rootReal, real, given) {
|
|
|
300
299
|
export function linkWords(name, found, leads, nested = [], include) {
|
|
301
300
|
const spell = (rel) => `"${rel === '' ? name : `${name}/${rel}`}"`;
|
|
302
301
|
if (!('link' in found))
|
|
303
|
-
return `${UNSURE_PREFIX}${name}/${found.unsure}" is a symlink (${found.code})`;
|
|
302
|
+
return `${UNSURE_PREFIX}${name}/${found.unsure}" is a symlink (${systemWords(errnoCode({ code: found.code }), found.code)})`;
|
|
304
303
|
const link = `${spell(found.link)} is a symlink`;
|
|
305
304
|
if (leads === null || leads.kind === 'nowhere')
|
|
306
305
|
return `${link}, which this mount does not follow`;
|
|
@@ -308,11 +307,12 @@ export function linkWords(name, found, leads, nested = [], include) {
|
|
|
308
307
|
return `${link} leading outside the mount, which this mount does not follow`;
|
|
309
308
|
const holder = nestedHolder(leads.target, nested);
|
|
310
309
|
if (holder) {
|
|
311
|
-
if (!namesHolder(leads,
|
|
310
|
+
if (!namesHolder(leads, nested))
|
|
312
311
|
return `${link}, which this mount does not follow`;
|
|
313
|
-
const rest = leads.target
|
|
314
|
-
|
|
315
|
-
|
|
312
|
+
const rest = restBeneath(leads.target, holder);
|
|
313
|
+
const guess = nestedGuess(name, leads.target.split('/'), holder);
|
|
314
|
+
return `${link} to ${spell(leads.target)}, which this mount does not follow; it ${guess === undefined ? 'lies' : 'may lie'} in mount ` +
|
|
315
|
+
`"${holder.name}", nested in mount "${name}": ${nestedStep({ name: holder.name, use: rest ? `${holder.name}/${rest}` : holder.name, guess }, { after: ` in place of ${spell(found.link)}` })}`;
|
|
316
316
|
}
|
|
317
317
|
if (!leads.directory && !insideMount(leads.target, include))
|
|
318
318
|
return `${link}, which this mount does not follow`;
|
|
@@ -337,23 +337,81 @@ export async function shownLeads(root, rootReal, found, include) {
|
|
|
337
337
|
export async function linkReason(view, rootReal, found) {
|
|
338
338
|
return linkWords(view.name, found, await shownLeads(view.root, rootReal, found, view.include), view.nested, view.include);
|
|
339
339
|
}
|
|
340
|
+
export function realSegments(path) {
|
|
341
|
+
return path.split(sep).filter((segment, i) => i === 0 || segment !== '');
|
|
342
|
+
}
|
|
343
|
+
export function beneathRoot(path, root) {
|
|
344
|
+
const segments = realSegments(path);
|
|
345
|
+
const top = realSegments(root);
|
|
346
|
+
return segments.length > top.length && beginsWith(segments, top) ? segments.slice(top.length).join('/') : undefined;
|
|
347
|
+
}
|
|
348
|
+
export function sameRoot(path, root) {
|
|
349
|
+
const segments = realSegments(path);
|
|
350
|
+
const top = realSegments(root);
|
|
351
|
+
return segments.length === top.length && beginsWith(segments, top);
|
|
352
|
+
}
|
|
353
|
+
export function surelyAtRoot(path, root) {
|
|
354
|
+
return surelyBeneath(realSegments(path), realSegments(root));
|
|
355
|
+
}
|
|
356
|
+
export function nestedRootAtReal(real, nested = []) {
|
|
357
|
+
return nestedRootsAtReal(real, nested)[0];
|
|
358
|
+
}
|
|
359
|
+
function nestedRootsAtReal(real, nested) {
|
|
360
|
+
// realpath keeps the spelling it's given where disk folds it on Linux, as
|
|
361
|
+
// from a link's target, so a real root is compared as loosely as a path.
|
|
362
|
+
const segments = realSegments(real);
|
|
363
|
+
const holders = nested.flatMap((n) => {
|
|
364
|
+
if (n.real === undefined)
|
|
365
|
+
return [];
|
|
366
|
+
const root = realSegments(n.real);
|
|
367
|
+
return beginsWith(segments, root) ? [{ mount: n, depth: root.length, sure: surelyBeneath(segments, root) }] : [];
|
|
368
|
+
});
|
|
369
|
+
return mayHold(holders, (h) => h.depth, (h) => h.sure);
|
|
370
|
+
}
|
|
340
371
|
/**
|
|
341
372
|
* The nested mount whose root holds `real`, a canonical path, the innermost,
|
|
342
|
-
* with the path to use there
|
|
343
|
-
*
|
|
373
|
+
* with the path to use there and the path beneath its root (`rest`);
|
|
374
|
+
* undefined when none does. A path the lexical checks pass
|
|
344
375
|
* (inOtherMount, the tools' holderOf) reaches another mount's directory this
|
|
345
376
|
* way through a symlink the mount follows, so each check that resolves a
|
|
346
|
-
* path canonically asks this too
|
|
347
|
-
* namesNestedPlace says the place is that mount's.
|
|
377
|
+
* path canonically asks this too.
|
|
348
378
|
*/
|
|
349
|
-
export function nestedAtReal(real, nested = []) {
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
.sort((a, b) => b.real.length - a.real.length)[0];
|
|
379
|
+
export async function nestedAtReal(real, nested = [], at) {
|
|
380
|
+
const holders = nestedRootsAtReal(real, nested);
|
|
381
|
+
const holder = holders[0];
|
|
353
382
|
if (!holder)
|
|
354
383
|
return undefined;
|
|
355
|
-
const
|
|
356
|
-
|
|
384
|
+
const segments = realSegments(real);
|
|
385
|
+
// realpath keeps a link target's spelling beneath the root too, so the
|
|
386
|
+
// rest is read as disk spells it, for each mount's include to judge.
|
|
387
|
+
const rests = await Promise.all(holders.map(async (h) => {
|
|
388
|
+
const given = segments.slice(h.depth).join('/');
|
|
389
|
+
return given === '' ? '' : await diskSpelling(h.mount.real, given);
|
|
390
|
+
}));
|
|
391
|
+
const rest = rests[0];
|
|
392
|
+
const { name } = holder.mount;
|
|
393
|
+
const root = realSegments(holder.mount.real);
|
|
394
|
+
const found = {
|
|
395
|
+
name,
|
|
396
|
+
use: rest ? `${name}/${rest}` : name,
|
|
397
|
+
rest,
|
|
398
|
+
judgedByInclude: holders.some((h) => h.mount.include !== undefined),
|
|
399
|
+
names: (directory) => holders.every((h, i) => namesNestedPlace(rests[i], h.mount.include, directory)),
|
|
400
|
+
};
|
|
401
|
+
if (surelyBeneath(segments, root))
|
|
402
|
+
return found;
|
|
403
|
+
if (at === undefined)
|
|
404
|
+
return { ...found, guess: null };
|
|
405
|
+
const top = realSegments(at.rootReal);
|
|
406
|
+
if (segments.length <= top.length || root.length <= top.length || !top.every((t, i) => segments[i] === t))
|
|
407
|
+
return { ...found, guess: null };
|
|
408
|
+
const within = segments.slice(top.length);
|
|
409
|
+
const below = root.slice(top.length);
|
|
410
|
+
const guess = guessFor(at.name, within, below);
|
|
411
|
+
const leads = guess === undefined ? [at.name, ...within].join('/') : [guess.given, ...within.slice(below.length)].join('/');
|
|
412
|
+
if (!surelyBeneath(root.slice(0, top.length), top))
|
|
413
|
+
return { ...found, guess: { ...placedGuess(at.name, within.slice(0, below.length), guess), leads } };
|
|
414
|
+
return { ...found, guess: guess === undefined ? null : { ...guess, leads } };
|
|
357
415
|
}
|
|
358
416
|
/**
|
|
359
417
|
* Why a mount-relative path that resolves into a nested mount's directory is
|
|
@@ -361,7 +419,8 @@ export function nestedAtReal(real, nested = []) {
|
|
|
361
419
|
* place. Spelled as the tools take paths, from the mount's name.
|
|
362
420
|
*/
|
|
363
421
|
export function leadsInto(name, rel, holder) {
|
|
364
|
-
|
|
422
|
+
const verb = holder.guess === undefined ? 'leads' : 'may lead';
|
|
423
|
+
return `"${rel === '' ? name : `${name}/${rel}`}" ${verb} into mount "${holder.name}", nested in mount "${name}": ${nestedStep(holder, { after: ' in its place' })}`;
|
|
365
424
|
}
|
|
366
425
|
// ===========================================================================
|
|
367
426
|
// Disk's spelling of a path
|
|
@@ -371,9 +430,8 @@ const sameFile = (a, b) => a.dev === b.dev && a.ino === b.ino;
|
|
|
371
430
|
const otherCase = (c) => (c === c.toLowerCase() ? c.toUpperCase() : c.toLowerCase());
|
|
372
431
|
/**
|
|
373
432
|
* The name with each letter's case flipped, where its other case is one
|
|
374
|
-
* letter whose own other case is the letter again
|
|
375
|
-
*
|
|
376
|
-
* and dotless `ı`, whose upper case `I` lowers to `i`, stay as they are.
|
|
433
|
+
* letter whose own other case is the letter again. `ß`, whose upper case is
|
|
434
|
+
* `SS`, and dotless `ı`, whose upper case `I` lowers to `i`, stay as they are.
|
|
377
435
|
*/
|
|
378
436
|
const flippedCase = (name) => [...name].map((c) => {
|
|
379
437
|
const other = otherCase(c);
|
|
@@ -388,6 +446,15 @@ const folded = (name) => name.normalize('NFC').toLowerCase();
|
|
|
388
446
|
* the same-file check keeps that from naming another file.
|
|
389
447
|
*/
|
|
390
448
|
export const foldedLoosely = (name) => name.normalize('NFC').toLowerCase().toUpperCase().toLowerCase();
|
|
449
|
+
/**
|
|
450
|
+
* Linux's casefold, which ext4 and f2fs set per directory, drops
|
|
451
|
+
* default-ignorable code points (on every kernel but 6.12 to 6.12.4) and
|
|
452
|
+
* compares names decomposed, in full case folding, and HFS+ passes over some
|
|
453
|
+
* of those code points too. Case mapped on the decomposed name joins marks
|
|
454
|
+
* that come in another order around a combining ypogegrammeni (U+0345),
|
|
455
|
+
* whose upper case is a letter.
|
|
456
|
+
*/
|
|
457
|
+
const foldedFully = (name) => name.replace(/\p{Default_Ignorable_Code_Point}/gu, '').normalize('NFD').toLowerCase().toUpperCase().toLowerCase().normalize('NFD');
|
|
391
458
|
/**
|
|
392
459
|
* Whether `dir` folds case, told without writing: by looking up flipped a
|
|
393
460
|
* name it lists whose flipped spelling it doesn't list too (a name with no
|
|
@@ -482,19 +549,73 @@ function entryName(paths, prefix, segment) {
|
|
|
482
549
|
}
|
|
483
550
|
return found;
|
|
484
551
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
export function
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
return
|
|
552
|
+
function sameLevel(given, name) {
|
|
553
|
+
return given === name || (given !== '' && foldedFully(given) === foldedFully(name));
|
|
554
|
+
}
|
|
555
|
+
function beginsWith(segments, dir) {
|
|
556
|
+
return segments.length >= dir.length && dir.every((d, i) => sameLevel(segments[i], d));
|
|
557
|
+
}
|
|
558
|
+
export function beneathNested(segments, nested) {
|
|
559
|
+
return beginsWith(segments, nested.dir.split('/'));
|
|
560
|
+
}
|
|
561
|
+
export function restBeneath(path, nested) {
|
|
562
|
+
// By segments, since a name that matches can be another length, as `SS`
|
|
563
|
+
// is beside `ß`.
|
|
564
|
+
return path.split('/').slice(nested.dir.split('/').length).join('/');
|
|
565
|
+
}
|
|
566
|
+
function surelyBeneath(segments, dir) {
|
|
567
|
+
return segments.length >= dir.length && dir.every((d, i) => segments[i] === d);
|
|
568
|
+
}
|
|
569
|
+
function shownByCodePoints(name) {
|
|
570
|
+
return [...name].map((c) => {
|
|
571
|
+
const code = c.codePointAt(0);
|
|
572
|
+
if (c === '\\')
|
|
573
|
+
return '\\\\';
|
|
574
|
+
return code < 0x80 ? c : code > 0xffff ? `\\u{${code.toString(16)}}` : `\\u${code.toString(16).padStart(4, '0')}`;
|
|
575
|
+
}).join('');
|
|
576
|
+
}
|
|
577
|
+
function guessFor(mount, given, dir) {
|
|
578
|
+
if (surelyBeneath(given, dir))
|
|
579
|
+
return undefined;
|
|
580
|
+
const levels = given.slice(0, dir.length);
|
|
581
|
+
const plain = (a, b) => a === b || !/[^\x00-\x7f]/.test(a + b);
|
|
582
|
+
const shown = (name, other) => (plain(name, other) ? name : shownByCodePoints(name));
|
|
583
|
+
return {
|
|
584
|
+
given: [mount, ...levels.map((g, i) => shown(g, dir[i]))].join('/'),
|
|
585
|
+
dir: [mount, ...dir.map((d, i) => shown(d, levels[i]))].join('/'),
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
export function nestedGuess(mount, segments, nested) {
|
|
589
|
+
const dir = nested.dir.split('/');
|
|
590
|
+
const guess = guessFor(mount, segments, dir);
|
|
591
|
+
return nested.guessed ? placedGuess(mount, segments.slice(0, dir.length), guess) : guess;
|
|
592
|
+
}
|
|
593
|
+
function placedGuess(mount, levels, guess) {
|
|
594
|
+
return guess === undefined ? { placed: [mount, ...levels].join('/') } : { placed: guess.given, dir: guess.dir };
|
|
595
|
+
}
|
|
596
|
+
export function nestedStep(holder, opts = {}) {
|
|
597
|
+
const { guess } = holder;
|
|
598
|
+
const after = opts.after ?? '';
|
|
599
|
+
if (guess === undefined)
|
|
600
|
+
return `use "${holder.use}"${after}`;
|
|
601
|
+
if (guess === null) {
|
|
602
|
+
return `this workspace can't tell whether where it leads is that mount's directory. ` +
|
|
603
|
+
`If you meant that mount's directory, use "${holder.use}"${after}; if not, ask whoever runs this workspace`;
|
|
604
|
+
}
|
|
605
|
+
const leads = guess.leads === undefined ? '' : `it leads to "${guess.leads}", and `;
|
|
606
|
+
if ('placed' in guess) {
|
|
607
|
+
const check = 'ask whoever runs this workspace to check where the two mounts are configured';
|
|
608
|
+
const outside = guess.dir === undefined ? `"${guess.placed}"` : `"${guess.placed}" and "${guess.dir}"`;
|
|
609
|
+
return `${leads}this workspace can't tell whether "${guess.placed}" is mount "${holder.name}"'s directory, since the two mounts' roots are configured in ` +
|
|
610
|
+
`spellings it can't tell apart. If you meant that mount's directory, use "${holder.use}"${after}. ` +
|
|
611
|
+
(opts.named ? `If you meant one of this mount's own, for a new file use a path outside ${outside}, and for one already there ${check}` : `If not, ${check}`);
|
|
612
|
+
}
|
|
613
|
+
const remedy = `rename one of "${guess.given}" and "${guess.dir}"`;
|
|
614
|
+
const own = opts.named
|
|
615
|
+
? `If you meant one of this mount's own, for a new file use a path outside "${guess.given}" and "${guess.dir}", and for one already there ask whoever runs this workspace to ${remedy}`
|
|
616
|
+
: `If not, ask whoever runs this workspace to ${remedy}`;
|
|
617
|
+
return `${leads}this workspace can't tell "${guess.given}" from "${guess.dir}", ` +
|
|
618
|
+
`mount "${holder.name}"'s directory. If you meant that mount's directory, use "${holder.use}"${after}. ${own}`;
|
|
498
619
|
}
|
|
499
620
|
/**
|
|
500
621
|
* The name `dir` lists for `segment`: `segment` itself, unless disk folds it
|
|
@@ -707,20 +828,26 @@ export class CheckedDir {
|
|
|
707
828
|
lexical;
|
|
708
829
|
dev;
|
|
709
830
|
ino;
|
|
831
|
+
born;
|
|
710
832
|
constructor(
|
|
711
833
|
/** Mount-relative path ('' for the root). */
|
|
712
834
|
rel,
|
|
713
835
|
/** Its canonical location when checked. */
|
|
714
|
-
real, lexical, dev, ino) {
|
|
836
|
+
real, lexical, dev, ino, born) {
|
|
715
837
|
this.rel = rel;
|
|
716
838
|
this.real = real;
|
|
717
839
|
this.lexical = lexical;
|
|
718
840
|
this.dev = dev;
|
|
719
841
|
this.ino = ino;
|
|
842
|
+
this.born = born;
|
|
720
843
|
}
|
|
721
844
|
/** Which directory it is. */
|
|
722
845
|
identity() {
|
|
723
|
-
return {
|
|
846
|
+
return {
|
|
847
|
+
dev: String(this.dev),
|
|
848
|
+
ino: String(this.ino),
|
|
849
|
+
...(this.born > 0n ? { born: String(this.born) } : {}),
|
|
850
|
+
};
|
|
724
851
|
}
|
|
725
852
|
/** A path that reaches `name` in this directory. */
|
|
726
853
|
at(name) {
|
|
@@ -764,12 +891,26 @@ export class CheckedDir {
|
|
|
764
891
|
}
|
|
765
892
|
}
|
|
766
893
|
}
|
|
894
|
+
/**
|
|
895
|
+
* What is at a mount root's path now, followed as the root is: for a
|
|
896
|
+
* directory, undefined; for anything else, ENOTDIR; and where the path
|
|
897
|
+
* can't be followed to anything, the errno its stat failed with, ENOENT
|
|
898
|
+
* when nothing is there.
|
|
899
|
+
*/
|
|
900
|
+
export async function rootAbsence(root) {
|
|
901
|
+
try {
|
|
902
|
+
return (await stat(root)).isDirectory() ? undefined : 'ENOTDIR';
|
|
903
|
+
}
|
|
904
|
+
catch (err) {
|
|
905
|
+
return errno(err);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
767
908
|
/** Check the directory at a canonical location, if one is there. */
|
|
768
909
|
async function checkedAt(view, rel, real) {
|
|
769
910
|
const info = await lstat(real, { bigint: true }).catch(() => null);
|
|
770
911
|
if (!info?.isDirectory())
|
|
771
912
|
return null;
|
|
772
|
-
return new CheckedDir(rel, real, lexicalOf(view, rel), info.dev, info.ino);
|
|
913
|
+
return new CheckedDir(rel, real, lexicalOf(view, rel), info.dev, info.ino, info.birthtimeNs);
|
|
773
914
|
}
|
|
774
915
|
/**
|
|
775
916
|
* Where a mount directory is now: checked; missing — nothing there, or not a
|
|
@@ -784,13 +925,13 @@ async function locateDir(view, rootReal, rel) {
|
|
|
784
925
|
catch (err) {
|
|
785
926
|
const code = errno(err);
|
|
786
927
|
if (code === 'ENOENT' || code === 'ENOTDIR')
|
|
787
|
-
return rel === '' ? { kind: 'unobserved', reason: ROOT_UNAVAILABLE } : { kind: 'missing' };
|
|
928
|
+
return rel === '' ? { kind: 'unobserved', reason: ROOT_UNAVAILABLE, errno: code } : { kind: 'missing' };
|
|
788
929
|
// A link on the way that can't be resolved, such as a loop, is still a
|
|
789
930
|
// link this mount doesn't follow.
|
|
790
931
|
const found = rel === '' ? null : await linkOnTheWay(view, rel);
|
|
791
932
|
if (found !== null)
|
|
792
933
|
return { kind: 'unobserved', reason: await linkReason(view, rootReal, found) };
|
|
793
|
-
return { kind: 'unobserved', reason: `cannot resolve the directory (${code
|
|
934
|
+
return { kind: 'unobserved', reason: `cannot resolve the directory (${systemWords(code, err)})`, ...(code !== undefined ? { errno: code } : {}) };
|
|
794
935
|
}
|
|
795
936
|
// A mount that doesn't follow symlinks goes through none on the way to a
|
|
796
937
|
// directory, inside the mount or out of it.
|
|
@@ -811,10 +952,10 @@ async function locateDir(view, rootReal, rel) {
|
|
|
811
952
|
// mount's, which the mount neither reads nor writes. A place there that
|
|
812
953
|
// mount's include leaves out is no mount's, and holds nothing beneath it,
|
|
813
954
|
// as at any file (namesNestedPlace).
|
|
814
|
-
const holder = nestedAtReal(real, view.nested);
|
|
955
|
+
const holder = await nestedAtReal(real, view.nested, { name: view.name, rootReal });
|
|
815
956
|
if (holder) {
|
|
816
|
-
const directory = holder.
|
|
817
|
-
return
|
|
957
|
+
const directory = holder.judgedByInclude && await stat(real).then((info) => info.isDirectory(), () => false);
|
|
958
|
+
return holder.names(directory) ? { kind: 'unobserved', reason: leadsInto(view.name, rel, holder) } : { kind: 'missing' };
|
|
818
959
|
}
|
|
819
960
|
}
|
|
820
961
|
const checked = await checkedAt(view, rel, real);
|
|
@@ -842,7 +983,7 @@ export async function checkRoot(view, rootReal) {
|
|
|
842
983
|
const found = await locateDir(view, rootReal, '');
|
|
843
984
|
if (found.kind === 'checked')
|
|
844
985
|
return { dir: found.dir };
|
|
845
|
-
return
|
|
986
|
+
return found.kind === 'unobserved' ? { reason: found.reason, ...(found.errno !== undefined ? { errno: found.errno } : {}) } : { reason: ROOT_UNAVAILABLE };
|
|
846
987
|
}
|
|
847
988
|
/**
|
|
848
989
|
* Check the directory at a mount-relative path. With `create`, missing
|
|
@@ -879,7 +1020,7 @@ async function createDir(view, rootReal, rel) {
|
|
|
879
1020
|
const childRel = parts.slice(0, i + 1).join('/');
|
|
880
1021
|
// Nothing is made in a nested mount's directory, its root included where
|
|
881
1022
|
// it isn't made yet, by a path that reaches it through a link.
|
|
882
|
-
const holder = nestedAtReal(base.at(parts[i]), view.nested);
|
|
1023
|
+
const holder = await nestedAtReal(base.at(parts[i]), view.nested, { name: view.name, rootReal });
|
|
883
1024
|
if (holder)
|
|
884
1025
|
return { kind: 'unobserved', reason: leadsInto(view.name, childRel, holder) };
|
|
885
1026
|
try {
|
|
@@ -888,7 +1029,7 @@ async function createDir(view, rootReal, rel) {
|
|
|
888
1029
|
catch (err) {
|
|
889
1030
|
const code = errno(err);
|
|
890
1031
|
if (code !== 'EEXIST')
|
|
891
|
-
return { kind: 'unobserved', reason: `cannot create the directory ${childRel} (${code
|
|
1032
|
+
return { kind: 'unobserved', reason: `cannot create the directory ${childRel} (${systemWords(code, err)})`, ...(code !== undefined ? { errno: code } : {}) };
|
|
892
1033
|
}
|
|
893
1034
|
// Made, or there already (made meanwhile, or not a directory): it must
|
|
894
1035
|
// be a real directory now, never a symlink or file in its place.
|
|
@@ -899,7 +1040,7 @@ async function createDir(view, rootReal, rel) {
|
|
|
899
1040
|
const there = view.followSymlinks ? null : await lstat(base.at(parts[i])).catch(() => null);
|
|
900
1041
|
if (there?.isSymbolicLink())
|
|
901
1042
|
return { kind: 'unobserved', reason: await linkReason(view, rootReal, { link: childRel }) };
|
|
902
|
-
return { kind: 'unobserved', reason:
|
|
1043
|
+
return { kind: 'unobserved', reason: NOT_A_DIRECTORY };
|
|
903
1044
|
}
|
|
904
1045
|
base = checked;
|
|
905
1046
|
}
|
|
@@ -947,14 +1088,14 @@ export async function confirmAbsent(view, rootReal, rel, target = 'entry') {
|
|
|
947
1088
|
if (code === 'ENOTDIR')
|
|
948
1089
|
return CHANGED;
|
|
949
1090
|
if (code !== 'ENOENT')
|
|
950
|
-
return `cannot stat (${code
|
|
1091
|
+
return `cannot stat (${systemWords(code, err)})`;
|
|
951
1092
|
// Absent from a directory that, like each above it, is still the one its path names.
|
|
952
1093
|
return proven();
|
|
953
1094
|
}
|
|
954
1095
|
if (info.isDirectory()) {
|
|
955
1096
|
if (last)
|
|
956
1097
|
return CHANGED; // present after all
|
|
957
|
-
cur = new CheckedDir(childRel, cur.at(name), lexicalOf(view, childRel), info.dev, info.ino);
|
|
1098
|
+
cur = new CheckedDir(childRel, cur.at(name), lexicalOf(view, childRel), info.dev, info.ino, info.birthtimeNs);
|
|
958
1099
|
walked.push(cur);
|
|
959
1100
|
continue;
|
|
960
1101
|
}
|
|
@@ -1064,13 +1205,13 @@ async function parentPlacement(view, rootReal, relativePath) {
|
|
|
1064
1205
|
return { reason: await linkReason(view, rootReal, found) };
|
|
1065
1206
|
if (!contained(rootReal, parent))
|
|
1066
1207
|
return { reason: OUTSIDE_PARENT };
|
|
1067
|
-
const holder = nestedAtReal(parent, view.nested);
|
|
1208
|
+
const holder = await nestedAtReal(parent, view.nested, { name: view.name, rootReal });
|
|
1068
1209
|
if (!holder)
|
|
1069
1210
|
return { inside: parent };
|
|
1070
1211
|
// A place there that the nested mount's include leaves out is no mount's,
|
|
1071
1212
|
// and holds nothing beneath it, as beneath any file (namesNestedPlace).
|
|
1072
|
-
const directory = holder.
|
|
1073
|
-
return
|
|
1213
|
+
const directory = holder.judgedByInclude && await stat(parent).then((info) => info.isDirectory(), () => false);
|
|
1214
|
+
return holder.names(directory) ? { reason: leadsInto(view.name, parentOf(relativePath), holder) } : 'gone';
|
|
1074
1215
|
}
|
|
1075
1216
|
catch (err) {
|
|
1076
1217
|
const code = errno(err);
|
|
@@ -1081,7 +1222,7 @@ async function parentPlacement(view, rootReal, relativePath) {
|
|
|
1081
1222
|
const found = await linkOnTheWay(view, relativePath, 'parents');
|
|
1082
1223
|
if (found !== null)
|
|
1083
1224
|
return { reason: await linkReason(view, rootReal, found) };
|
|
1084
|
-
return { reason: `cannot resolve the parent directory (${code
|
|
1225
|
+
return { reason: `cannot resolve the parent directory (${systemWords(code, err)})` };
|
|
1085
1226
|
}
|
|
1086
1227
|
}
|
|
1087
1228
|
/**
|
|
@@ -1105,7 +1246,7 @@ export async function observePath(view, rootReal, relativePath) {
|
|
|
1105
1246
|
const code = errno(err);
|
|
1106
1247
|
if (code === 'ENOENT' || code === 'ENOTDIR')
|
|
1107
1248
|
return absence(view, rootReal, relativePath);
|
|
1108
|
-
return { kind: 'unobserved', reason: `cannot stat (${code
|
|
1249
|
+
return { kind: 'unobserved', reason: `cannot stat (${systemWords(code, err)})` };
|
|
1109
1250
|
}
|
|
1110
1251
|
if (info.isSymbolicLink()) {
|
|
1111
1252
|
if (!view.followSymlinks)
|
|
@@ -1115,43 +1256,43 @@ export async function observePath(view, rootReal, relativePath) {
|
|
|
1115
1256
|
target = await stat(lexical, { bigint: true });
|
|
1116
1257
|
}
|
|
1117
1258
|
catch (err) {
|
|
1118
|
-
return { kind: 'unobserved', reason: `a symlink whose target cannot be read (${errno(err)
|
|
1259
|
+
return { kind: 'unobserved', reason: `a symlink whose target cannot be read (${systemWords(errno(err), err)})` };
|
|
1119
1260
|
}
|
|
1120
1261
|
let real;
|
|
1121
1262
|
try {
|
|
1122
1263
|
real = await realpath(lexical);
|
|
1123
1264
|
}
|
|
1124
1265
|
catch (err) {
|
|
1125
|
-
return { kind: 'unobserved', reason: `a symlink that cannot be resolved (${errno(err)
|
|
1266
|
+
return { kind: 'unobserved', reason: `a symlink that cannot be resolved (${systemWords(errno(err), err)})` };
|
|
1126
1267
|
}
|
|
1127
1268
|
if (!contained(rootReal, real))
|
|
1128
|
-
return { kind: 'unobserved', reason:
|
|
1129
|
-
const holder = nestedAtReal(real, view.nested);
|
|
1269
|
+
return { kind: 'unobserved', reason: LEAVES_MOUNT };
|
|
1270
|
+
const holder = await nestedAtReal(real, view.nested, { name: view.name, rootReal });
|
|
1130
1271
|
// A place a nested mount's include leaves out is no mount's, so the link
|
|
1131
1272
|
// leaves this one (namesNestedPlace).
|
|
1132
1273
|
if (holder) {
|
|
1133
|
-
return { kind: 'unobserved', reason:
|
|
1274
|
+
return { kind: 'unobserved', reason: holder.names(target.isDirectory()) ? leadsInto(view.name, relativePath, holder) : LEAVES_MOUNT };
|
|
1134
1275
|
}
|
|
1135
1276
|
if (!target.isFile()) {
|
|
1136
|
-
return { kind: 'unobserved', reason: target.isDirectory() ? 'a symlink to a directory, not walked' :
|
|
1277
|
+
return { kind: 'unobserved', reason: target.isDirectory() ? 'a symlink to a directory, not walked' : NOT_REGULAR };
|
|
1137
1278
|
}
|
|
1138
1279
|
// A file this mount's own include leaves out where the link lands is no
|
|
1139
1280
|
// file of this mount's (coversLanding). It's judged here, and not only
|
|
1140
1281
|
// where the file is opened, since a pass takes a copy whose fingerprint
|
|
1141
1282
|
// the stat vouches for without opening it.
|
|
1142
1283
|
if (!(await coversLanding(view, rootReal, real, relativePath)))
|
|
1143
|
-
return { kind: 'unobserved', reason:
|
|
1284
|
+
return { kind: 'unobserved', reason: LEAVES_MOUNT };
|
|
1144
1285
|
info = target;
|
|
1145
1286
|
}
|
|
1146
1287
|
else if (info.isDirectory()) {
|
|
1147
|
-
return { kind: 'unobserved', reason:
|
|
1288
|
+
return { kind: 'unobserved', reason: DIRECTORY };
|
|
1148
1289
|
}
|
|
1149
1290
|
else if (!info.isFile()) {
|
|
1150
|
-
return { kind: 'unobserved', reason:
|
|
1291
|
+
return { kind: 'unobserved', reason: NOT_REGULAR };
|
|
1151
1292
|
}
|
|
1152
1293
|
else if (!(await coversLanding(view, rootReal, join(placement.inside, basename(lexical)), relativePath))) {
|
|
1153
1294
|
// So is one reached through a link among its directories.
|
|
1154
|
-
return { kind: 'unobserved', reason:
|
|
1295
|
+
return { kind: 'unobserved', reason: LEAVES_MOUNT };
|
|
1155
1296
|
}
|
|
1156
1297
|
return { kind: 'file', size: Number(info.size), mtimeMs: Number(info.mtimeMs), mode: Number(info.mode & 511n), fp: statFingerprint(info) };
|
|
1157
1298
|
}
|
|
@@ -1187,8 +1328,8 @@ async function openContained(view, rootReal, relativePath) {
|
|
|
1187
1328
|
return { kind: 'unobserved', reason: found === null ? NOT_FOLLOWED : await linkReason(view, rootReal, found) };
|
|
1188
1329
|
}
|
|
1189
1330
|
if (code === 'EISDIR')
|
|
1190
|
-
return { kind: 'unobserved', reason:
|
|
1191
|
-
return { kind: 'unobserved', reason: `cannot open (${code
|
|
1331
|
+
return { kind: 'unobserved', reason: DIRECTORY };
|
|
1332
|
+
return { kind: 'unobserved', reason: `cannot open (${systemWords(code, err)})` };
|
|
1192
1333
|
}
|
|
1193
1334
|
let keep = false;
|
|
1194
1335
|
try {
|
|
@@ -1205,15 +1346,15 @@ async function openContained(view, rootReal, relativePath) {
|
|
|
1205
1346
|
if (found !== null)
|
|
1206
1347
|
return { kind: 'unobserved', reason: await linkReason(view, rootReal, found) };
|
|
1207
1348
|
if (!info.isFile())
|
|
1208
|
-
return { kind: 'unobserved', reason: info.isDirectory() ?
|
|
1209
|
-
const leaves = view.followSymlinks ?
|
|
1349
|
+
return { kind: 'unobserved', reason: info.isDirectory() ? DIRECTORY : NOT_REGULAR };
|
|
1350
|
+
const leaves = view.followSymlinks ? LEAVES_MOUNT : OUTSIDE_PARENT;
|
|
1210
1351
|
if (!contained(rootReal, real))
|
|
1211
1352
|
return { kind: 'unobserved', reason: leaves };
|
|
1212
|
-
const holder = nestedAtReal(real, view.nested);
|
|
1353
|
+
const holder = await nestedAtReal(real, view.nested, { name: view.name, rootReal });
|
|
1213
1354
|
// A place a nested mount's include leaves out is no mount's. Reached
|
|
1214
1355
|
// through a link this mount doesn't follow, the link is what's refused.
|
|
1215
1356
|
if (holder)
|
|
1216
|
-
return { kind: 'unobserved', reason:
|
|
1357
|
+
return { kind: 'unobserved', reason: holder.names(false) ? leadsInto(view.name, relativePath, holder) : view.followSymlinks ? leaves : NOT_FOLLOWED };
|
|
1217
1358
|
// So is a file this mount's own include leaves out (coversLanding).
|
|
1218
1359
|
if (!(await coversLanding(view, rootReal, real, relativePath)))
|
|
1219
1360
|
return { kind: 'unobserved', reason: view.followSymlinks ? leaves : NOT_FOLLOWED };
|
|
@@ -1239,7 +1380,7 @@ async function openContained(view, rootReal, relativePath) {
|
|
|
1239
1380
|
const code = errno(err);
|
|
1240
1381
|
if (code === 'ENOENT')
|
|
1241
1382
|
return absence(view, rootReal, relativePath);
|
|
1242
|
-
return { kind: 'unobserved', reason: `cannot read (${code
|
|
1383
|
+
return { kind: 'unobserved', reason: `cannot read (${systemWords(code, err)})` };
|
|
1243
1384
|
}
|
|
1244
1385
|
finally {
|
|
1245
1386
|
if (!keep)
|
|
@@ -1340,7 +1481,7 @@ export async function readPath(view, rootReal, relativePath, limit) {
|
|
|
1340
1481
|
const code = errno(err);
|
|
1341
1482
|
if (code === 'ENOENT')
|
|
1342
1483
|
return absence(view, rootReal, relativePath);
|
|
1343
|
-
return { kind: 'unobserved', reason: `cannot read (${code
|
|
1484
|
+
return { kind: 'unobserved', reason: `cannot read (${systemWords(code, err)})` };
|
|
1344
1485
|
}
|
|
1345
1486
|
finally {
|
|
1346
1487
|
await handle.close();
|
|
@@ -1382,7 +1523,7 @@ export async function readBytes(view, rootReal, relativePath, limit) {
|
|
|
1382
1523
|
return { kind: 'bytes', size: bytes.byteLength, bytes, head: bytes.subarray(0, HEAD_BYTES) };
|
|
1383
1524
|
}
|
|
1384
1525
|
catch (err) {
|
|
1385
|
-
return { kind: 'unobserved', reason: `cannot read (${errno(err)
|
|
1526
|
+
return { kind: 'unobserved', reason: `cannot read (${systemWords(errno(err), err)})` };
|
|
1386
1527
|
}
|
|
1387
1528
|
finally {
|
|
1388
1529
|
await handle.close();
|
|
@@ -1404,7 +1545,7 @@ export async function readHead(view, rootReal, relativePath) {
|
|
|
1404
1545
|
return { kind: 'head', size: Number(info.size), mtimeMs: Number(info.mtimeMs), head: head.subarray(0, bytesRead) };
|
|
1405
1546
|
}
|
|
1406
1547
|
catch (err) {
|
|
1407
|
-
return { kind: 'unobserved', reason: `cannot read (${errno(err)
|
|
1548
|
+
return { kind: 'unobserved', reason: `cannot read (${systemWords(errno(err), err)})` };
|
|
1408
1549
|
}
|
|
1409
1550
|
finally {
|
|
1410
1551
|
await handle.close();
|
|
@@ -1480,14 +1621,14 @@ function subtreeAgainst(dir, at) {
|
|
|
1480
1621
|
return 'holds';
|
|
1481
1622
|
}
|
|
1482
1623
|
/**
|
|
1483
|
-
* Whether a mount's ignore list covers a mount-relative path: the path itself
|
|
1484
|
-
* or any directory above it, as the walk's own test reads
|
|
1485
|
-
* path and
|
|
1624
|
+
* Whether a mount's ignore list covers a mount-relative path: the path itself,
|
|
1625
|
+
* a directory or not, or any directory above it, as the walk's own test reads
|
|
1626
|
+
* each entry by its path and kind.
|
|
1486
1627
|
*/
|
|
1487
|
-
export function coveredByIgnore(relativePath, patterns) {
|
|
1628
|
+
export function coveredByIgnore(relativePath, patterns, directory) {
|
|
1488
1629
|
const segments = relativePath.split('/').filter(Boolean);
|
|
1489
1630
|
for (let i = 0; i < segments.length; i++) {
|
|
1490
|
-
if (isIgnored(segments.slice(0, i + 1).join('/'),
|
|
1631
|
+
if (isIgnored(segments.slice(0, i + 1).join('/'), patterns, i < segments.length - 1 || directory))
|
|
1491
1632
|
return true;
|
|
1492
1633
|
}
|
|
1493
1634
|
return false;
|
|
@@ -1530,44 +1671,90 @@ export function insideMount(relativePath, include) {
|
|
|
1530
1671
|
return true;
|
|
1531
1672
|
return coveredByInclude(relativePath, include);
|
|
1532
1673
|
}
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1674
|
+
const readings = new Map();
|
|
1675
|
+
function readPattern(pattern) {
|
|
1676
|
+
let reading = readings.get(pattern);
|
|
1677
|
+
if (reading === undefined) {
|
|
1678
|
+
reading = parsePattern(pattern);
|
|
1679
|
+
readings.set(pattern, reading);
|
|
1680
|
+
}
|
|
1681
|
+
return reading;
|
|
1682
|
+
}
|
|
1683
|
+
function parsePattern(pattern) {
|
|
1684
|
+
if (pattern.startsWith('!'))
|
|
1685
|
+
return 'a leading "!" makes an exception in a .gitignore, and this list has none, so leave it out';
|
|
1686
|
+
if (pattern.startsWith('#'))
|
|
1687
|
+
return 'a leading "#" marks a comment in a .gitignore, so leave comments out of the list';
|
|
1688
|
+
if (pattern.includes('\\'))
|
|
1689
|
+
return '"\\" is an escape in a .gitignore and a separator on Windows, so no pattern here holds one, and "?" stands for one in a name';
|
|
1690
|
+
if (pattern.endsWith(' '))
|
|
1691
|
+
return 'a .gitignore drops trailing spaces, so remove them';
|
|
1692
|
+
const anyDepth = pattern.startsWith('**/');
|
|
1693
|
+
let body = anyDepth ? pattern.slice(3) : pattern;
|
|
1694
|
+
const beneath = body.endsWith('/**');
|
|
1695
|
+
const directory = beneath || body.endsWith('/');
|
|
1696
|
+
body = body.slice(0, beneath ? -3 : directory ? -1 : undefined);
|
|
1697
|
+
const anchored = (beneath && !anyDepth) || body.includes('/');
|
|
1698
|
+
if (body.startsWith('/') && !anyDepth)
|
|
1699
|
+
body = body.slice(1);
|
|
1700
|
+
const segments = body.split('/');
|
|
1701
|
+
if (segments.some((s) => s === '' || s === '.' || s === '..'))
|
|
1702
|
+
return 'it has an empty, "." or ".." segment, which names no entry';
|
|
1703
|
+
if (body.includes('**') || (anyDepth && anchored))
|
|
1704
|
+
return '"**" is read only as a leading "**/" before one name, or a trailing "/**"';
|
|
1705
|
+
const glob = (s) => /[*?]/.test(s)
|
|
1706
|
+
? new RegExp(`^${s.replace(/[.+^${}()|[\]*?]/g, (c) => (c === '*' ? '.*' : c === '?' ? '.' : `\\${c}`))}$`, 'su')
|
|
1707
|
+
: s;
|
|
1708
|
+
return { segments: segments.map(glob), anchored, directory };
|
|
1709
|
+
}
|
|
1710
|
+
function nameMatches(segment, name) {
|
|
1711
|
+
return typeof segment === 'string' ? segment === name : segment.test(name);
|
|
1712
|
+
}
|
|
1713
|
+
function matchesEntry(pattern, segments, directory) {
|
|
1714
|
+
if (pattern.directory && !directory)
|
|
1715
|
+
return false;
|
|
1716
|
+
if (!pattern.anchored)
|
|
1717
|
+
return nameMatches(pattern.segments[0], segments[segments.length - 1]);
|
|
1718
|
+
return segments.length === pattern.segments.length && pattern.segments.every((s, i) => nameMatches(s, segments[i]));
|
|
1719
|
+
}
|
|
1720
|
+
export function patternRefusal(pattern, list) {
|
|
1721
|
+
if (typeof pattern !== 'string')
|
|
1722
|
+
return "it isn't a string";
|
|
1723
|
+
const reading = readPattern(pattern);
|
|
1724
|
+
if (typeof reading === 'string')
|
|
1725
|
+
return reading;
|
|
1726
|
+
if (list === 'ignore' && pattern.includes('[')) {
|
|
1727
|
+
return '"[" starts a character class in a .gitignore, which this list doesn\'t read, so write each name out, with "?" for a "[" in it';
|
|
1728
|
+
}
|
|
1729
|
+
return undefined;
|
|
1730
|
+
}
|
|
1731
|
+
export function directoryAt(path, followSymlinks) {
|
|
1732
|
+
if (!followSymlinks)
|
|
1733
|
+
return lstat(path).then((s) => s.isDirectory(), () => undefined);
|
|
1734
|
+
return stat(path).then((s) => s.isDirectory(), () => lstat(path).then(() => false, () => undefined));
|
|
1548
1735
|
}
|
|
1549
1736
|
/**
|
|
1550
|
-
* Whether a mount's include covers a mount-relative path
|
|
1551
|
-
*
|
|
1552
|
-
*
|
|
1553
|
-
*
|
|
1554
|
-
*
|
|
1555
|
-
*
|
|
1556
|
-
*
|
|
1557
|
-
* no covering: where one is a scope, refusesAsOutside (index.ts) decides.
|
|
1737
|
+
* Whether a mount's include covers a mount-relative path, read as a file: a
|
|
1738
|
+
* pattern that matches only directories covers what lies beneath a directory
|
|
1739
|
+
* it matches, and any other covers a file by its own name or path, and
|
|
1740
|
+
* nothing beneath a directory it matches. So `gate.json` covers `x/gate.json`
|
|
1741
|
+
* and not `x/gate.json/run.sh`, and a subtree is named with `dir/` or
|
|
1742
|
+
* `dir/**`. A directory is no file, so it needs no covering: where one is a
|
|
1743
|
+
* scope, refusesAsOutside (index.ts) decides.
|
|
1558
1744
|
*/
|
|
1559
1745
|
function coveredByInclude(relativePath, include) {
|
|
1560
|
-
const
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1746
|
+
const segments = relativePath.split('/');
|
|
1747
|
+
return include.some((p) => {
|
|
1748
|
+
const pattern = readPattern(p);
|
|
1749
|
+
if (typeof pattern === 'string')
|
|
1750
|
+
return false;
|
|
1751
|
+
if (!pattern.directory)
|
|
1752
|
+
return matchesEntry(pattern, segments, false);
|
|
1753
|
+
for (let k = 1; k < segments.length; k++)
|
|
1754
|
+
if (matchesEntry(pattern, segments.slice(0, k), true))
|
|
1564
1755
|
return true;
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
return true;
|
|
1568
|
-
}
|
|
1569
|
-
}
|
|
1570
|
-
return false;
|
|
1756
|
+
return false;
|
|
1757
|
+
});
|
|
1571
1758
|
}
|
|
1572
1759
|
/**
|
|
1573
1760
|
* The ignore patterns a walk that names `scope` reads beneath it: a pattern
|
|
@@ -1575,7 +1762,7 @@ function coveredByInclude(relativePath, include) {
|
|
|
1575
1762
|
* it (#276); one covering only something inside it does.
|
|
1576
1763
|
*/
|
|
1577
1764
|
export function ignoreBeneath(scope, patterns) {
|
|
1578
|
-
return patterns.filter((pattern) => !coveredByIgnore(scope, [pattern]));
|
|
1765
|
+
return patterns.filter((pattern) => !coveredByIgnore(scope, [pattern], true));
|
|
1579
1766
|
}
|
|
1580
1767
|
/**
|
|
1581
1768
|
* Names editors give the files they keep beside the one being edited: vim's
|
|
@@ -1618,18 +1805,18 @@ export const OTHER_MOUNT = "another mount's directory";
|
|
|
1618
1805
|
* Every nested-mount check (a pass's, a walk's, a push's, grep's) asks this.
|
|
1619
1806
|
*/
|
|
1620
1807
|
export function inOtherMount(relativePath, others) {
|
|
1621
|
-
const
|
|
1622
|
-
return others.some((dir) =>
|
|
1808
|
+
const segments = posix.normalize(relativePath).replace(/\/+$/, '').split('/');
|
|
1809
|
+
return others.some((dir) => beneathNested(segments, { dir }));
|
|
1623
1810
|
}
|
|
1624
1811
|
/**
|
|
1625
|
-
* Why a walk passes over an entry by design, by its mount-relative path
|
|
1626
|
-
*
|
|
1627
|
-
* name. Undefined when none covers it.
|
|
1812
|
+
* Why a walk passes over an entry by design, by its mount-relative path, its
|
|
1813
|
+
* name and whether it's a directory: another mount's directory, the ignore
|
|
1814
|
+
* list, or an editor's file name. Undefined when none covers it.
|
|
1628
1815
|
*/
|
|
1629
|
-
function passedOverBecause(relativePath, name, patterns, others) {
|
|
1816
|
+
function passedOverBecause(relativePath, name, patterns, others, directory) {
|
|
1630
1817
|
if (inOtherMount(relativePath, others))
|
|
1631
1818
|
return OTHER_MOUNT;
|
|
1632
|
-
if (isIgnored(relativePath,
|
|
1819
|
+
if (isIgnored(relativePath, patterns, directory))
|
|
1633
1820
|
return IGNORED;
|
|
1634
1821
|
if (EDITOR_FILE.test(name))
|
|
1635
1822
|
return EDITOR_FILE_REASON;
|
|
@@ -1637,17 +1824,17 @@ function passedOverBecause(relativePath, name, patterns, others) {
|
|
|
1637
1824
|
}
|
|
1638
1825
|
/**
|
|
1639
1826
|
* Why a sync without a path, and the watcher, pass over a mount-relative
|
|
1640
|
-
* path: the path itself or a directory above it is passed
|
|
1641
|
-
* the walk reads each entry by its path and
|
|
1642
|
-
* directory is that mount's, whatever else covers a
|
|
1643
|
-
* Undefined when nothing covers it.
|
|
1827
|
+
* path, a directory or not: the path itself or a directory above it is passed
|
|
1828
|
+
* over by design, as the walk reads each entry by its path, name and kind. A
|
|
1829
|
+
* path in another mount's directory is that mount's, whatever else covers a
|
|
1830
|
+
* directory above it. Undefined when nothing covers it.
|
|
1644
1831
|
*/
|
|
1645
|
-
export function passedOverAt(relativePath, patterns, others) {
|
|
1832
|
+
export function passedOverAt(relativePath, patterns, others, directory) {
|
|
1646
1833
|
if (inOtherMount(relativePath, others))
|
|
1647
1834
|
return OTHER_MOUNT;
|
|
1648
1835
|
const segments = relativePath.split('/').filter(Boolean);
|
|
1649
1836
|
for (let i = 0; i < segments.length; i++) {
|
|
1650
|
-
const why = passedOverBecause(segments.slice(0, i + 1).join('/'), segments[i], patterns, others);
|
|
1837
|
+
const why = passedOverBecause(segments.slice(0, i + 1).join('/'), segments[i], patterns, others, i < segments.length - 1 || directory);
|
|
1651
1838
|
if (why !== undefined)
|
|
1652
1839
|
return why;
|
|
1653
1840
|
}
|
|
@@ -1663,28 +1850,19 @@ export function passedOverBetween(scope, relativePath, patterns, others) {
|
|
|
1663
1850
|
const segments = relativePath.split('/').filter(Boolean);
|
|
1664
1851
|
const from = scope === '' ? 0 : scope.split('/').filter(Boolean).length;
|
|
1665
1852
|
for (let i = from; i < segments.length - 1; i++) {
|
|
1666
|
-
const why = passedOverBecause(segments.slice(0, i + 1).join('/'), segments[i], patterns, others);
|
|
1853
|
+
const why = passedOverBecause(segments.slice(0, i + 1).join('/'), segments[i], patterns, others, true);
|
|
1667
1854
|
if (why !== undefined)
|
|
1668
1855
|
return why;
|
|
1669
1856
|
}
|
|
1670
1857
|
return undefined;
|
|
1671
1858
|
}
|
|
1672
|
-
/**
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
if (relativePath === prefix || relativePath.startsWith(prefix + '/'))
|
|
1680
|
-
return true;
|
|
1681
|
-
}
|
|
1682
|
-
if (!pattern.includes('/') && relativePath === pattern)
|
|
1683
|
-
return true;
|
|
1684
|
-
if (pattern.startsWith('*.') && name.endsWith(pattern.slice(1)))
|
|
1685
|
-
return true;
|
|
1686
|
-
}
|
|
1687
|
-
return false;
|
|
1859
|
+
/** Whether an ignore pattern matches the entry at a mount-relative path, a directory or not, itself. */
|
|
1860
|
+
function isIgnored(relativePath, patterns, directory) {
|
|
1861
|
+
const segments = relativePath.split('/');
|
|
1862
|
+
return patterns.some((p) => {
|
|
1863
|
+
const pattern = readPattern(p);
|
|
1864
|
+
return typeof pattern !== 'string' && matchesEntry(pattern, segments, directory);
|
|
1865
|
+
});
|
|
1688
1866
|
}
|
|
1689
1867
|
/**
|
|
1690
1868
|
* A subdirectory a checked directory's listing showed, checked in turn: still a
|
|
@@ -1699,13 +1877,13 @@ async function checkChild(view, rootReal, parent, rel) {
|
|
|
1699
1877
|
}
|
|
1700
1878
|
catch (err) {
|
|
1701
1879
|
if (errno(err) !== 'ENOENT')
|
|
1702
|
-
return { kind: 'unobserved', reason: `cannot stat (${errno(err)
|
|
1880
|
+
return { kind: 'unobserved', reason: `cannot stat (${systemWords(errno(err), err)})` };
|
|
1703
1881
|
const proof = await confirmAbsent(view, rootReal, rel, 'beneath');
|
|
1704
1882
|
return typeof proof === 'string' ? { kind: 'unobserved', reason: proof } : { kind: 'absent', by: proof.by };
|
|
1705
1883
|
}
|
|
1706
1884
|
if (!info.isDirectory())
|
|
1707
1885
|
return { kind: 'unobserved', reason: CHANGED_WALK };
|
|
1708
|
-
return { kind: 'checked', dir: new CheckedDir(rel, real, lexicalOf(view, rel), info.dev, info.ino) };
|
|
1886
|
+
return { kind: 'checked', dir: new CheckedDir(rel, real, lexicalOf(view, rel), info.dev, info.ino, info.birthtimeNs) };
|
|
1709
1887
|
}
|
|
1710
1888
|
/**
|
|
1711
1889
|
* List a directory scope ('' for the mount root), descending when recursive,
|
|
@@ -1734,12 +1912,11 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
|
|
|
1734
1912
|
};
|
|
1735
1913
|
// A walk that isn't named passes over a scope the mount passes over by
|
|
1736
1914
|
// design, at it or a directory above it, as its parent's listing would pass
|
|
1737
|
-
// over its entry
|
|
1738
|
-
//
|
|
1739
|
-
//
|
|
1740
|
-
//
|
|
1741
|
-
//
|
|
1742
|
-
// sync that names it, whose scope is walked (#276), below.
|
|
1915
|
+
// over its entry. So a listing, glob or grep there never starts tracking
|
|
1916
|
+
// what the mount leaves out, and a stored copy beneath it is unobserved, as
|
|
1917
|
+
// beneath any region passed over by design. Its region says how to take
|
|
1918
|
+
// what's there in: a sync that names it, whose scope is walked (#276),
|
|
1919
|
+
// below.
|
|
1743
1920
|
// Another mount's directory is passed over by every walk, named or not,
|
|
1744
1921
|
// and no sync of this mount takes it in, so its region gives no advice.
|
|
1745
1922
|
if (scope !== '' && inOtherMount(scope, view.others)) {
|
|
@@ -1748,7 +1925,11 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
|
|
|
1748
1925
|
return walk;
|
|
1749
1926
|
}
|
|
1750
1927
|
if (!opts.named && scope !== '') {
|
|
1751
|
-
|
|
1928
|
+
let why = passedOverAt(scope, view.ignore, view.others, true);
|
|
1929
|
+
if (why !== undefined && passedOverAt(scope, view.ignore, view.others, false) === undefined) {
|
|
1930
|
+
if ((await directoryAt(join(rootReal, scope), view.followSymlinks)) === false)
|
|
1931
|
+
why = undefined;
|
|
1932
|
+
}
|
|
1752
1933
|
if (why !== undefined) {
|
|
1753
1934
|
walk.others.set(scope, why);
|
|
1754
1935
|
walk.incomplete.push({ path: scope, reason: `${why}: a sync that names this path takes in what's there, editors' files aside`, kind: 'ignored' });
|
|
@@ -1892,9 +2073,14 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
|
|
|
1892
2073
|
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
1893
2074
|
// Another mount's directory is passed over by its real path too, so a
|
|
1894
2075
|
// walk that reaches one through a link to a directory above it lists
|
|
1895
|
-
// none of that mount's names (
|
|
1896
|
-
|
|
1897
|
-
?? (entry.
|
|
2076
|
+
// none of that mount's names (nestedRootAtReal).
|
|
2077
|
+
let passedOver = passedOverBecause(childRel, entry.name, ignore, view.others, entry.isDirectory())
|
|
2078
|
+
?? (!entry.isSymbolicLink() && nestedRootAtReal(dir.at(entry.name), view.nested) !== undefined ? OTHER_MOUNT : undefined);
|
|
2079
|
+
if (passedOver === undefined && view.followSymlinks && entry.isSymbolicLink()) {
|
|
2080
|
+
const asDirectory = passedOverBecause(childRel, entry.name, ignore, view.others, true);
|
|
2081
|
+
if (asDirectory !== undefined && (await directoryAt(dir.at(entry.name), true)) === true)
|
|
2082
|
+
passedOver = asDirectory;
|
|
2083
|
+
}
|
|
1898
2084
|
if (passedOver !== undefined) {
|
|
1899
2085
|
others.push([childRel, passedOver]);
|
|
1900
2086
|
regions.push({ path: childRel, reason: passedOver, kind: 'ignored' });
|
|
@@ -1937,7 +2123,7 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
|
|
|
1937
2123
|
else {
|
|
1938
2124
|
const symlink = entry.isSymbolicLink();
|
|
1939
2125
|
leaves.push([childRel, false]);
|
|
1940
|
-
others.push([childRel, symlink ? 'a symlink' :
|
|
2126
|
+
others.push([childRel, symlink ? 'a symlink' : NOT_REGULAR]);
|
|
1941
2127
|
if (symlink)
|
|
1942
2128
|
regions.push({ path: childRel, reason: LINK_NOT_WALKED, kind: 'symlink' });
|
|
1943
2129
|
}
|
|
@@ -1971,10 +2157,10 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
|
|
|
1971
2157
|
return at === 'holds' || at === (stretch === 'after' ? 'after' : 'before');
|
|
1972
2158
|
};
|
|
1973
2159
|
// Each nested root by which directory it is, read once, when the walk
|
|
1974
|
-
// first descends: a directory the walk reaches
|
|
1975
|
-
//
|
|
1976
|
-
// a
|
|
1977
|
-
// mount's directory.
|
|
2160
|
+
// first descends: a directory the walk reaches by a path that no
|
|
2161
|
+
// comparison of names places in the nested mount's directory, as through a
|
|
2162
|
+
// bind mount, or on a disk whose folding goes past the comparison's, is
|
|
2163
|
+
// still another mount's directory.
|
|
1978
2164
|
let nestedRoots;
|
|
1979
2165
|
const isNestedRoot = async (dir) => {
|
|
1980
2166
|
if (!view.nested?.length)
|