@vzn/vx 0.0.645 → 0.0.648
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 +39 -50
- package/launcher.cjs +9 -2
- package/launcher.sh +2 -2
- package/package.json +5 -5
- package/schemas/cache.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/info.json +16 -2
- package/schemas/last.json +127 -5
- package/schemas/plan.json +28 -2
- package/schemas/show.json +10 -1
- package/skills/vx/SKILL.md +19 -3
- package/src/bin.ts +54 -13
- package/src/cache/archive.ts +324 -23
- package/src/cache/cache.ts +330 -65
- package/src/cache/git-inputs.ts +274 -97
- package/src/cache/index.ts +7 -2
- package/src/cache/inputs.ts +515 -83
- package/src/cache/key-fold.ts +15 -6
- package/src/cache/layer.ts +74 -11
- package/src/cache/layered-cache.ts +154 -59
- package/src/cache/linked-inputs.ts +1 -1
- package/src/cache/output-index.ts +44 -14
- package/src/cache/plugin-cache.ts +49 -23
- package/src/cache/remote-hits.ts +72 -0
- package/src/cache/remote-lane.ts +100 -15
- package/src/cache/run-history.ts +77 -23
- package/src/cache/run-store.ts +11 -10
- package/src/cache/schema.ts +42 -6
- package/src/cache/tar-stream.ts +5 -0
- package/src/cache/umask.ts +1 -2
- package/src/cli/cache.ts +58 -27
- package/src/cli/completions.ts +12 -5
- package/src/cli/core-alias.ts +5 -2
- package/src/cli/docs.ts +42 -16
- package/src/cli/help.ts +62 -25
- package/src/cli/index.ts +79 -45
- package/src/cli/info.ts +12 -6
- package/src/cli/init.ts +222 -34
- package/src/cli/last.ts +443 -47
- package/src/cli/lock.ts +197 -52
- package/src/cli/plan-format.ts +91 -49
- package/src/cli/plugin-commands.ts +160 -11
- package/src/cli/plugin-templates.ts +13 -13
- package/src/cli/registry.ts +209 -0
- package/src/cli/run.ts +111 -41
- package/src/cli/select.ts +148 -49
- package/src/cli/show.ts +59 -16
- package/src/cli/task-verb.ts +16 -6
- package/src/cli/upgrade.ts +155 -54
- package/src/cli/watch-cycle.ts +10 -5
- package/src/cli/watch-filter.ts +71 -3
- package/src/cli/watch-fs.ts +22 -6
- package/src/cli/watch-judge.ts +48 -6
- package/src/cli/watch.ts +163 -42
- package/src/cli/why.ts +211 -73
- package/src/cli/workspace-config.ts +54 -15
- package/src/config.ts +13 -1
- package/src/exec/env.ts +7 -2
- package/src/exec/executor.ts +262 -15
- package/src/exec/index.ts +8 -2
- package/src/exec/kill-tree.ts +15 -0
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/proc-sample.ts +70 -6
- package/src/exec/runner.ts +256 -53
- package/src/exec/sandbox-binds.ts +7 -3
- package/src/exec/sandbox-paths.ts +1 -1
- package/src/exec/sandbox-runtime.ts +222 -28
- package/src/exec/sandbox-violations.ts +52 -20
- package/src/graph/critical-path.ts +79 -0
- package/src/graph/default-build.ts +19 -0
- package/src/graph/definition.ts +38 -0
- package/src/graph/folds.ts +38 -5
- package/src/graph/index.ts +4 -1
- package/src/graph/priorities.ts +74 -8
- package/src/graph/scheduler.ts +326 -65
- package/src/graph/task-graph.ts +228 -35
- package/src/handoff.ts +97 -9
- package/src/history/failure-mode.ts +6 -3
- package/src/history/history.ts +268 -50
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +142 -45
- package/src/index.ts +42 -10
- package/src/orchestrator/admission.ts +10 -6
- package/src/orchestrator/affected-tasks.ts +11 -6
- package/src/orchestrator/deferred-outputs.ts +2 -0
- package/src/orchestrator/doctor.ts +134 -42
- package/src/orchestrator/execute-args.ts +6 -0
- package/src/orchestrator/execute-task.ts +155 -42
- package/src/orchestrator/fingerprint-watch.ts +1 -1
- package/src/orchestrator/graph-memo.ts +140 -0
- package/src/orchestrator/graph-stage.ts +18 -1
- package/src/orchestrator/held-servers.ts +86 -0
- package/src/orchestrator/hit-restore.ts +227 -122
- package/src/orchestrator/index.ts +20 -3
- package/src/orchestrator/local-floor.ts +198 -0
- package/src/orchestrator/local-shortcircuit.ts +51 -8
- package/src/orchestrator/lockfile-claim.ts +96 -16
- package/src/orchestrator/miss-save.ts +52 -12
- package/src/orchestrator/options.ts +8 -6
- package/src/orchestrator/output-host.ts +43 -14
- package/src/orchestrator/output-log.ts +90 -10
- package/src/orchestrator/persistent-task.ts +97 -28
- package/src/orchestrator/persistent.ts +16 -4
- package/src/orchestrator/placement.ts +257 -14
- package/src/orchestrator/plan.ts +64 -7
- package/src/orchestrator/plugin-host.ts +230 -40
- package/src/orchestrator/plugin.ts +150 -37
- package/src/orchestrator/prepare.ts +178 -32
- package/src/orchestrator/projects.ts +65 -40
- package/src/orchestrator/remote-prefetch.ts +119 -25
- package/src/orchestrator/run-artifacts.ts +95 -32
- package/src/orchestrator/run-context.ts +39 -41
- package/src/orchestrator/run-failures.ts +65 -23
- package/src/orchestrator/run-id.ts +6 -3
- package/src/orchestrator/run-keys.ts +300 -0
- package/src/orchestrator/run-live.ts +294 -0
- package/src/orchestrator/run-lock.ts +13 -7
- package/src/orchestrator/run-records.ts +35 -23
- package/src/orchestrator/run-report.ts +3 -3
- package/src/orchestrator/run-steps.ts +58 -22
- package/src/orchestrator/run-stop.ts +22 -5
- package/src/orchestrator/run.ts +409 -79
- package/src/orchestrator/sandbox-fix.ts +7 -5
- package/src/orchestrator/sandbox-request.ts +78 -22
- package/src/orchestrator/shell-verdict.ts +5 -5
- package/src/orchestrator/signals.ts +26 -1
- package/src/orchestrator/stable-keys.ts +92 -8
- package/src/orchestrator/steps.ts +2 -2
- package/src/orchestrator/task-artifacts.ts +206 -0
- package/src/orchestrator/task-hash.ts +89 -14
- package/src/orchestrator/task-log-buffer.ts +72 -36
- package/src/orchestrator/task-process.ts +22 -0
- package/src/orchestrator/telemetry-host.ts +24 -3
- package/src/orchestrator/telemetry.ts +113 -38
- package/src/orchestrator/upstream.ts +15 -0
- package/src/report/events.ts +32 -13
- package/src/report/forecast.ts +36 -35
- package/src/report/framed-output.ts +173 -46
- package/src/report/index.ts +1 -0
- package/src/report/logger.ts +217 -186
- package/src/report/path-links.ts +12 -4
- package/src/report/plain-output.ts +13 -5
- package/src/report/status-line.ts +94 -46
- package/src/report/summary.ts +174 -38
- package/src/report/terminal-signals.ts +13 -2
- package/src/settings.ts +14 -6
- package/src/util/bun-version.ts +4 -1
- package/src/util/colors.ts +20 -3
- package/src/util/config-frame.ts +41 -4
- package/src/{graph → util}/dependency-spec.ts +22 -13
- package/src/util/edit-distance.ts +13 -1
- package/src/util/env-flag.ts +9 -0
- package/src/util/errors.ts +59 -3
- package/src/util/essential-env.ts +57 -0
- package/src/util/git-ignored.ts +1 -1
- package/src/util/hash.ts +35 -0
- package/src/util/hide-env.ts +45 -5
- package/src/util/index.ts +22 -5
- package/src/util/num.ts +8 -0
- package/src/util/paths.ts +121 -21
- package/src/util/report.ts +6 -1
- package/src/util/secret-mask.ts +62 -1
- package/src/util/secrets.ts +7 -2
- package/src/util/size.ts +11 -3
- package/src/util/tail.ts +6 -1
- package/src/util/timing.ts +13 -1
- package/src/util/ui.ts +65 -4
- package/src/util/which.ts +1 -1
- package/src/util/write-file.ts +67 -0
- package/src/workspace/affected.ts +710 -107
- package/src/workspace/bracket-literal.ts +84 -0
- package/src/workspace/canonical-dir.ts +37 -0
- package/src/workspace/config-bytecode.ts +282 -0
- package/src/workspace/config-eval.ts +29 -8
- package/src/workspace/config-imports.ts +199 -58
- package/src/workspace/config-schema.ts +242 -39
- package/src/workspace/filter.ts +37 -8
- package/src/workspace/index.ts +11 -2
- package/src/workspace/json-data.ts +3 -4
- package/src/workspace/lockfile.ts +57 -8
- package/src/workspace/migration.ts +72 -49
- package/src/workspace/package-graph.ts +5 -4
- package/src/workspace/project-loader.ts +314 -47
- package/src/workspace/repo-id.ts +1 -1
- package/src/workspace/workspace.ts +176 -33
- package/src/cli/foreign-flags.ts +0 -727
package/src/cache/git-inputs.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
checkIgnoreSync,
|
|
25
25
|
executablePath,
|
|
26
26
|
gitEnv,
|
|
27
|
+
gitFailure,
|
|
27
28
|
gitSpawnRefusal,
|
|
28
29
|
isInstalledPath,
|
|
29
30
|
notAWorkTree,
|
|
@@ -178,7 +179,12 @@ function repoFactsFromDisk(dir: string): RepoFacts | undefined {
|
|
|
178
179
|
}
|
|
179
180
|
}
|
|
180
181
|
|
|
181
|
-
/**
|
|
182
|
+
/**
|
|
183
|
+
* `[extensions] objectFormat` of a repository config, sha1 when unset;
|
|
184
|
+
* undefined when unsure, or when `core.worktree` puts the work tree
|
|
185
|
+
* somewhere other than the `.git` directory's parent, which the prefix
|
|
186
|
+
* read off the disk assumes.
|
|
187
|
+
*/
|
|
182
188
|
function configObjectFormat(config: string): 'sha1' | 'sha256' | undefined {
|
|
183
189
|
let section = ''
|
|
184
190
|
let format: 'sha1' | 'sha256' = 'sha1'
|
|
@@ -191,7 +197,10 @@ function configObjectFormat(config: string): 'sha1' | 'sha256' | undefined {
|
|
|
191
197
|
if (section === 'include' || section === 'includeif') return undefined
|
|
192
198
|
continue
|
|
193
199
|
}
|
|
200
|
+
if (section === 'core' && /^worktree\b/i.test(line)) return undefined
|
|
194
201
|
if (section !== 'extensions') continue
|
|
202
|
+
// `config.worktree` may set `core.worktree` too.
|
|
203
|
+
if (/^worktreeconfig\b/i.test(line)) return undefined
|
|
195
204
|
const kv = /^([A-Za-z0-9-]+)\s*=\s*(.*)$/.exec(line)
|
|
196
205
|
if (kv === null || kv[1]!.toLowerCase() !== 'objectformat') continue
|
|
197
206
|
const value = kv[2]!.toLowerCase()
|
|
@@ -204,7 +213,8 @@ function configObjectFormat(config: string): 'sha1' | 'sha256' | undefined {
|
|
|
204
213
|
interface PartitionDigest {
|
|
205
214
|
snapshot: readonly string[]
|
|
206
215
|
oids: OidSlot | undefined
|
|
207
|
-
|
|
216
|
+
/** Built on first read: a partition with untrusted files stats them then. */
|
|
217
|
+
digest: () => string | undefined
|
|
208
218
|
}
|
|
209
219
|
|
|
210
220
|
/**
|
|
@@ -243,6 +253,12 @@ interface IgnoredListing {
|
|
|
243
253
|
}
|
|
244
254
|
|
|
245
255
|
export class GitFilesCache extends Map<string, readonly string[]> {
|
|
256
|
+
/**
|
|
257
|
+
* vx's own directories inside the workspace (the run's cache directory),
|
|
258
|
+
* spelled under the workspace root as the run spells it: no input, even
|
|
259
|
+
* where a glob walks an ignored prefix on disk.
|
|
260
|
+
*/
|
|
261
|
+
vxDirs: readonly string[] = []
|
|
246
262
|
private changed = new Map<string, string[]>()
|
|
247
263
|
/**
|
|
248
264
|
* Per-project trusted index OIDs: absolute path → git blob OID, for
|
|
@@ -303,7 +319,7 @@ export class GitFilesCache extends Map<string, readonly string[]> {
|
|
|
303
319
|
this.foldCase ??=
|
|
304
320
|
this.vars === null
|
|
305
321
|
? process.platform === 'darwin'
|
|
306
|
-
: this.vars.get('core.ignorecase')
|
|
322
|
+
: gitBool(this.vars.get('core.ignorecase')) === true
|
|
307
323
|
return this.foldCase ? abs.toLowerCase() : abs
|
|
308
324
|
}
|
|
309
325
|
|
|
@@ -327,7 +343,7 @@ export class GitFilesCache extends Map<string, readonly string[]> {
|
|
|
327
343
|
|
|
328
344
|
/**
|
|
329
345
|
* Whether git tracks a file under `dir`: committed (or `git add`ed)
|
|
330
|
-
* files are the user's, never cleaned (
|
|
346
|
+
* files are the user's, never cleaned (2026-10-14: "never
|
|
331
347
|
* delete committed file"). A directory no enumeration covered (a
|
|
332
348
|
* nested repository, a cache no enumeration filled) is listed once.
|
|
333
349
|
* git lists a submodule as its gitlink path alone, so a file under a
|
|
@@ -515,11 +531,20 @@ export class GitFilesCache extends Map<string, readonly string[]> {
|
|
|
515
531
|
const d = this.digests.get(projectDir)
|
|
516
532
|
if (d === undefined || d.snapshot !== snapshot || this.get(projectDir) !== snapshot) return
|
|
517
533
|
if (this.oids.get(projectDir) !== d.oids || this.changed.has(projectDir)) return
|
|
518
|
-
return d.digest
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
noteFilesDigest(
|
|
522
|
-
|
|
534
|
+
return d.digest()
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
noteFilesDigest(
|
|
538
|
+
projectDir: string,
|
|
539
|
+
snapshot: readonly string[],
|
|
540
|
+
digest: () => string | undefined,
|
|
541
|
+
): void {
|
|
542
|
+
let memo: { value: string | undefined } | undefined
|
|
543
|
+
this.digests.set(projectDir, {
|
|
544
|
+
snapshot,
|
|
545
|
+
oids: this.oids.get(projectDir),
|
|
546
|
+
digest: () => (memo ??= { value: digest() }).value,
|
|
547
|
+
})
|
|
523
548
|
}
|
|
524
549
|
|
|
525
550
|
/** Trusted index OIDs for a project (abs path → oid), if any survive. */
|
|
@@ -541,12 +566,34 @@ export class GitFilesCache extends Map<string, readonly string[]> {
|
|
|
541
566
|
const snap = this.get(projectDir)
|
|
542
567
|
if (snap === undefined) return undefined
|
|
543
568
|
const pending = this.changed.get(projectDir)
|
|
544
|
-
if (pending !== undefined &&
|
|
545
|
-
return undefined
|
|
546
|
-
}
|
|
569
|
+
if (pending !== undefined && this.pendingMatch(pending, inputGlobs)) return undefined
|
|
547
570
|
return snap
|
|
548
571
|
}
|
|
549
572
|
|
|
573
|
+
/**
|
|
574
|
+
* How far each glob has read a `changed` list, -1 once it matched: the
|
|
575
|
+
* list only grows until a reset replaces it, so each glob reads each path
|
|
576
|
+
* once. Every task asked again from the start: 9000 tasks writing into
|
|
577
|
+
* one project cost 10 s of reads (2026-10-25).
|
|
578
|
+
*/
|
|
579
|
+
private readonly read = new WeakMap<readonly string[], Map<Bun.Glob, number>>()
|
|
580
|
+
|
|
581
|
+
private pendingMatch(pending: readonly string[], globs: readonly Bun.Glob[]): boolean {
|
|
582
|
+
let upTo = this.read.get(pending)
|
|
583
|
+
if (upTo === undefined) this.read.set(pending, (upTo = new Map()))
|
|
584
|
+
for (const g of globs) {
|
|
585
|
+
let i = upTo.get(g) ?? 0
|
|
586
|
+
if (i < 0) return true
|
|
587
|
+
for (; i < pending.length; i++)
|
|
588
|
+
if (g.match(pending[i]!)) {
|
|
589
|
+
upTo.set(g, -1)
|
|
590
|
+
return true
|
|
591
|
+
}
|
|
592
|
+
upTo.set(g, i)
|
|
593
|
+
}
|
|
594
|
+
return false
|
|
595
|
+
}
|
|
596
|
+
|
|
550
597
|
override set(key: string, value: readonly string[]): this {
|
|
551
598
|
this.changed.delete(key)
|
|
552
599
|
this.oids.delete(key)
|
|
@@ -645,12 +692,12 @@ const LS_FILES_STAGE_RE = /^(?:([A-Za-z]) )?([0-7]{6}) ([0-9a-f]{40,64}) ([0-3])
|
|
|
645
692
|
* tree; vx requires git. `-z` survives filenames with newlines /
|
|
646
693
|
* spaces.
|
|
647
694
|
*/
|
|
648
|
-
export function runGitLsFiles(cwd: string): GitLsResult {
|
|
695
|
+
export function runGitLsFiles(cwd: string, env = gitEnv()): GitLsResult {
|
|
649
696
|
let proc
|
|
650
697
|
try {
|
|
651
698
|
proc = Bun.spawnSync({
|
|
652
699
|
cmd: [executablePath('git'), 'ls-files', '-s', '--others', '--exclude-standard', '-z', '.'],
|
|
653
|
-
env
|
|
700
|
+
env,
|
|
654
701
|
cwd,
|
|
655
702
|
stdout: 'pipe',
|
|
656
703
|
stderr: 'pipe',
|
|
@@ -659,9 +706,9 @@ export function runGitLsFiles(cwd: string): GitLsResult {
|
|
|
659
706
|
throw gitSpawnRefusal(cwd)
|
|
660
707
|
}
|
|
661
708
|
if (proc.exitCode !== 0) {
|
|
662
|
-
//
|
|
663
|
-
//
|
|
664
|
-
throw
|
|
709
|
+
// Not a work tree, or git failing in one: either way the inputs
|
|
710
|
+
// cannot be enumerated, and the two have different fixes.
|
|
711
|
+
throw gitFailure(cwd, 'ls-files', proc.exitCode, new TextDecoder().decode(proc.stderr).trim())
|
|
665
712
|
}
|
|
666
713
|
const { text, undecodable } = decodeGitZ(proc.stdout)
|
|
667
714
|
const parsed = parseLsFilesOutput(text.length === 0 ? [] : text.split('\0'), undecodable)
|
|
@@ -674,7 +721,9 @@ export function runGitLsFiles(cwd: string): GitLsResult {
|
|
|
674
721
|
/**
|
|
675
722
|
* The files git tracks under `dir`, absolute: one `ls-files` of the index,
|
|
676
723
|
* no worktree walk. Outside a work tree (an embedder's temp dir) nothing
|
|
677
|
-
* is tracked.
|
|
724
|
+
* is tracked. A git that fails otherwise refuses: read as "nothing
|
|
725
|
+
* tracked", every committed file under `dir` was the output clean's to
|
|
726
|
+
* delete (2026-10-28).
|
|
678
727
|
*/
|
|
679
728
|
function listTracked(dir: string): string[] {
|
|
680
729
|
let proc
|
|
@@ -687,9 +736,16 @@ function listTracked(dir: string): string[] {
|
|
|
687
736
|
stderr: 'pipe',
|
|
688
737
|
})
|
|
689
738
|
} catch {
|
|
690
|
-
|
|
739
|
+
throw gitSpawnRefusal(dir)
|
|
740
|
+
}
|
|
741
|
+
if (proc.exitCode !== 0) {
|
|
742
|
+
const stderr = new TextDecoder().decode(proc.stderr).trim()
|
|
743
|
+
if (/not a git repository/i.test(stderr)) return []
|
|
744
|
+
throw new UserError(
|
|
745
|
+
`git ls-files failed (exit ${proc.exitCode}) in ${dir}: ${stderr || 'no output'}. Run it there to see why.`,
|
|
746
|
+
'VX_E_GIT',
|
|
747
|
+
)
|
|
691
748
|
}
|
|
692
|
-
if (proc.exitCode !== 0) return []
|
|
693
749
|
const { text } = decodeGitZ(proc.stdout)
|
|
694
750
|
return text
|
|
695
751
|
.split('\0')
|
|
@@ -732,13 +788,67 @@ function expandNestedRepos(
|
|
|
732
788
|
for (const f of walkFiles(abs)) out.push(`${nested}/${f}`)
|
|
733
789
|
continue
|
|
734
790
|
}
|
|
735
|
-
|
|
791
|
+
let inner
|
|
792
|
+
try {
|
|
793
|
+
inner = runGitLsFiles(abs, nestedRepoEnv())
|
|
794
|
+
} catch {
|
|
795
|
+
// A `.git` git cannot open (a submodule's file naming a lost
|
|
796
|
+
// `.git/modules` entry) answers as no `.git` does: by a walk. It
|
|
797
|
+
// refused the whole run as "not inside a git work tree".
|
|
798
|
+
for (const f of walkFiles(abs)) out.push(`${nested}/${f}`)
|
|
799
|
+
continue
|
|
800
|
+
}
|
|
736
801
|
for (const f of inner.files) out.push(`${nested}/${f}`)
|
|
737
802
|
for (const f of inner.undecodable) undecodable.push(`${nested}/${f}`)
|
|
738
803
|
}
|
|
739
804
|
return { files: out ?? files, undecodable }
|
|
740
805
|
}
|
|
741
806
|
|
|
807
|
+
/**
|
|
808
|
+
* Variables that name the repository git works on (git's `local_repo_env`),
|
|
809
|
+
* which a nested repository's git must not inherit. A pre-commit hook runs
|
|
810
|
+
* vx with `GIT_INDEX_FILE` or `GIT_DIR` naming the OUTER repository, and
|
|
811
|
+
* the nested listing read the outer index: a file the nested repository
|
|
812
|
+
* tracks under an ignore rule fell out of the inputs, a stale hit. git
|
|
813
|
+
* clears the same list for a submodule's commands; config variables stay.
|
|
814
|
+
*/
|
|
815
|
+
const LOCAL_REPO_ENV = new Set([
|
|
816
|
+
'GIT_ALTERNATE_OBJECT_DIRECTORIES',
|
|
817
|
+
'GIT_COMMON_DIR',
|
|
818
|
+
'GIT_DIR',
|
|
819
|
+
'GIT_GRAFT_FILE',
|
|
820
|
+
'GIT_IMPLICIT_WORK_TREE',
|
|
821
|
+
'GIT_INDEX_FILE',
|
|
822
|
+
'GIT_INTERNAL_SUPER_PREFIX',
|
|
823
|
+
'GIT_NO_REPLACE_OBJECTS',
|
|
824
|
+
'GIT_OBJECT_DIRECTORY',
|
|
825
|
+
'GIT_PREFIX',
|
|
826
|
+
'GIT_REPLACE_REF_BASE',
|
|
827
|
+
'GIT_SHALLOW_FILE',
|
|
828
|
+
'GIT_WORK_TREE',
|
|
829
|
+
])
|
|
830
|
+
|
|
831
|
+
function nestedRepoEnv(): Record<string, string> {
|
|
832
|
+
const env = gitEnv()
|
|
833
|
+
for (const name of LOCAL_REPO_ENV) delete env[name]
|
|
834
|
+
return env
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* The environment a listing of `dir` runs in: `nestedRepoEnv` when a
|
|
839
|
+
* location variable is set and `dir` sits in a repository nested below
|
|
840
|
+
* `workspaceRoot` (a `.git` at it or between), whose git must not read the
|
|
841
|
+
* workspace repository's index; git's own environment otherwise.
|
|
842
|
+
*/
|
|
843
|
+
export function listingEnv(dir: string, workspaceRoot: string): Record<string, string> {
|
|
844
|
+
if (![...LOCAL_REPO_ENV].some((name) => process.env[name] !== undefined)) return gitEnv()
|
|
845
|
+
for (let at = dir; at !== workspaceRoot && at.startsWith(workspaceRoot); at = path.dirname(at)) {
|
|
846
|
+
if (existsSync(path.join(at, '.git'))) return nestedRepoEnv()
|
|
847
|
+
if (path.dirname(at) === at) break
|
|
848
|
+
}
|
|
849
|
+
return gitEnv()
|
|
850
|
+
}
|
|
851
|
+
|
|
742
852
|
/** Every file and symlink under `dir`, relative and `/`-separated; a `.git` is skipped, a link not followed. */
|
|
743
853
|
function walkFiles(dir: string, rel = ''): string[] {
|
|
744
854
|
let entries
|
|
@@ -1206,10 +1316,30 @@ function gitVarList(listing: string): Map<string, string> {
|
|
|
1206
1316
|
for (const line of listing.split('\n')) {
|
|
1207
1317
|
const eq = line.indexOf('=')
|
|
1208
1318
|
if (eq > 0) vars.set(line.slice(0, eq), line.slice(eq + 1))
|
|
1319
|
+
// A bare key (`[core] fileMode` with no `=`) is listed without one, and
|
|
1320
|
+
// git reads it as true.
|
|
1321
|
+
else if (eq === -1 && /^[a-z]+\./i.test(line)) vars.set(line, 'true')
|
|
1209
1322
|
}
|
|
1210
1323
|
return vars
|
|
1211
1324
|
}
|
|
1212
1325
|
|
|
1326
|
+
/**
|
|
1327
|
+
* A boolean config value as git reads it: `true`/`yes`/`on`, `false`/
|
|
1328
|
+
* `no`/`off` and the empty value, or an integer (`k`/`m`/`g` suffix
|
|
1329
|
+
* allowed) that is true when nonzero. Only `false`/`no`/`off`/`0` were
|
|
1330
|
+
* taken, so `trustctime =` or `fileMode = 00` kept index OIDs trusted
|
|
1331
|
+
* while git judged by the weaker stat. Undefined when unset or not a
|
|
1332
|
+
* boolean git takes.
|
|
1333
|
+
*/
|
|
1334
|
+
function gitBool(v: string | undefined): boolean | undefined {
|
|
1335
|
+
if (v === undefined) return undefined
|
|
1336
|
+
const t = v.trim().toLowerCase()
|
|
1337
|
+
if (t === 'true' || t === 'yes' || t === 'on') return true
|
|
1338
|
+
if (t === 'false' || t === 'no' || t === 'off' || t === '') return false
|
|
1339
|
+
const n = /^([-+]?\d+)[kmg]?$/.exec(t)
|
|
1340
|
+
return n === null ? undefined : Number(n[1]) !== 0
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1213
1343
|
/**
|
|
1214
1344
|
* `true` when git ignores the executable bit (`core.fileMode=false`, the
|
|
1215
1345
|
* default on WSL's DrvFs and what `git init` writes on a filesystem without
|
|
@@ -1217,8 +1347,7 @@ function gitVarList(listing: string): Map<string, string> {
|
|
|
1217
1347
|
* says nothing about the worktree.
|
|
1218
1348
|
*/
|
|
1219
1349
|
function fileModeIgnored(gitVars: string): boolean {
|
|
1220
|
-
|
|
1221
|
-
return v === 'false' || v === 'no' || v === 'off' || v === '0'
|
|
1350
|
+
return gitBool(gitVarList(gitVars).get('core.filemode')) === false
|
|
1222
1351
|
}
|
|
1223
1352
|
|
|
1224
1353
|
/**
|
|
@@ -1255,21 +1384,14 @@ function restampModes(trusted: Map<string, string>, workspaceRoot: string): void
|
|
|
1255
1384
|
*/
|
|
1256
1385
|
export function gitStatWeakened(gitVars: string): boolean {
|
|
1257
1386
|
const vars = gitVarList(gitVars)
|
|
1258
|
-
const trustctime = vars.get('core.trustctime')?.trim().toLowerCase()
|
|
1259
1387
|
const checkStat = vars.get('core.checkstat')?.trim().toLowerCase()
|
|
1260
|
-
return (
|
|
1261
|
-
trustctime === 'false' ||
|
|
1262
|
-
trustctime === 'no' ||
|
|
1263
|
-
trustctime === 'off' ||
|
|
1264
|
-
trustctime === '0' ||
|
|
1265
|
-
checkStat === 'minimal'
|
|
1266
|
-
)
|
|
1388
|
+
return gitBool(vars.get('core.trustctime')) === false || checkStat === 'minimal'
|
|
1267
1389
|
}
|
|
1268
1390
|
|
|
1269
1391
|
/** `true` when git may rewrite bytes for EVERY auto-detected text file. */
|
|
1270
1392
|
export function autocrlfConverts(gitVars: string): boolean {
|
|
1271
|
-
const v = gitVarList(gitVars).get('core.autocrlf')
|
|
1272
|
-
return v === '
|
|
1393
|
+
const v = gitVarList(gitVars).get('core.autocrlf')
|
|
1394
|
+
return v?.trim().toLowerCase() === 'input' || gitBool(v) === true
|
|
1273
1395
|
}
|
|
1274
1396
|
|
|
1275
1397
|
/**
|
|
@@ -1423,6 +1545,8 @@ export interface GitEnumeration {
|
|
|
1423
1545
|
scopes: readonly string[]
|
|
1424
1546
|
/** Workspace-relative path → index OID, for files whose worktree state matched the index. */
|
|
1425
1547
|
trusted: Map<string, string>
|
|
1548
|
+
/** Tracked paths git does not look at on disk (assume-unchanged, skip-worktree), workspace-relative. */
|
|
1549
|
+
flagged: readonly string[]
|
|
1426
1550
|
/** Whether the worktree had uncommitted changes; null when `git status` failed. */
|
|
1427
1551
|
dirty: boolean | null
|
|
1428
1552
|
/** `git var -l`'s listing (merged config, then git's variables); null when it failed. */
|
|
@@ -1505,6 +1629,19 @@ function hasRacyEntry(index: Uint8Array, mtimeSec: number, oidBytes: number): bo
|
|
|
1505
1629
|
/** A private copy a run older than this may still have handed to `status`. */
|
|
1506
1630
|
const STALE_INDEX_MS = 60_000
|
|
1507
1631
|
|
|
1632
|
+
/** Whether `status` should read a refreshed copy of the index (`refreshedIndex`). */
|
|
1633
|
+
function racyIndex(
|
|
1634
|
+
read: { bytes: Uint8Array; st: Stats },
|
|
1635
|
+
objectFormat: 'sha1' | 'sha256',
|
|
1636
|
+
): boolean {
|
|
1637
|
+
if (GIT_LOCATION_ENV.some((name) => process.env[name] !== undefined)) return false
|
|
1638
|
+
return hasRacyEntry(
|
|
1639
|
+
read.bytes,
|
|
1640
|
+
Math.floor(read.st.mtimeMs / 1000),
|
|
1641
|
+
objectFormat === 'sha256' ? 32 : 20,
|
|
1642
|
+
)
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1508
1645
|
/**
|
|
1509
1646
|
* The index `status` reads when the real one holds a racily clean entry: a
|
|
1510
1647
|
* copy refreshed once and kept beside it, named by the real index's bytes.
|
|
@@ -1517,18 +1654,13 @@ const STALE_INDEX_MS = 60_000
|
|
|
1517
1654
|
async function refreshedIndex(
|
|
1518
1655
|
indexFile: string,
|
|
1519
1656
|
read: { bytes: Uint8Array; hash: string; st: Stats },
|
|
1520
|
-
objectFormat: 'sha1' | 'sha256',
|
|
1521
1657
|
spawnGit: (
|
|
1522
1658
|
args: string[],
|
|
1523
1659
|
stdin?: string,
|
|
1524
1660
|
env?: Record<string, string>,
|
|
1525
1661
|
) => Promise<GitRun | null>,
|
|
1526
1662
|
): Promise<string | undefined> {
|
|
1527
|
-
if (GIT_LOCATION_ENV.some((name) => process.env[name] !== undefined)) return undefined
|
|
1528
1663
|
const { bytes, hash, st } = read
|
|
1529
|
-
if (!hasRacyEntry(bytes, Math.floor(st.mtimeMs / 1000), objectFormat === 'sha256' ? 32 : 20)) {
|
|
1530
|
-
return undefined
|
|
1531
|
-
}
|
|
1532
1664
|
const dir = path.dirname(indexFile)
|
|
1533
1665
|
const name = `vx-index-${hash}`
|
|
1534
1666
|
const copy = path.join(dir, name)
|
|
@@ -1630,15 +1762,6 @@ export async function startGitEnumeration(
|
|
|
1630
1762
|
}
|
|
1631
1763
|
}
|
|
1632
1764
|
const startedAtMs = Date.now()
|
|
1633
|
-
// Four spawns at most (the rev-parse is memoized per process), one
|
|
1634
|
-
// worktree walk. `ls-files -s -v` reads the INDEX only
|
|
1635
|
-
// (~9 ms on a 1000-project tree) and answers two questions at once: every
|
|
1636
|
-
// tracked path's OID and its cache-state flag. `status -uall` is the one
|
|
1637
|
-
// command that walks the worktree, and it answers two as well: which
|
|
1638
|
-
// tracked paths are dirty AND which files are untracked. Asking
|
|
1639
|
-
// `ls-files --others` for the untracked set walked the same tree a second
|
|
1640
|
-
// time (~50 ms of CPU, concurrent with status but contending with it).
|
|
1641
|
-
const listing = spawnGit(['ls-files', '-s', '-v', '-z', '--', ...pathspecs])
|
|
1642
1765
|
// The gate for whether a clean filter can rewrite bytes between the
|
|
1643
1766
|
// index and the worktree: git's merged config (`core.autocrlf`) and,
|
|
1644
1767
|
// from git 2.42, the attributes files it reads outside the tree
|
|
@@ -1647,7 +1770,7 @@ export async function startGitEnumeration(
|
|
|
1647
1770
|
// 1.32 ms, min of 30), which could not name the default global file.
|
|
1648
1771
|
// Not foldable into the rev-parse below: rev-parse prints no config.
|
|
1649
1772
|
const varsRun = spawnGit(['var', '-l'])
|
|
1650
|
-
// Asked while
|
|
1773
|
+
// Asked while `var` runs, from the memo the file hasher reads
|
|
1651
1774
|
// too (`repoFacts`). `prefix` is the repo→workspace path (empty when the
|
|
1652
1775
|
// workspace root IS the git root): `ls-files` prints cwd(workspace)-
|
|
1653
1776
|
// relative paths but `status` prints repo-root-relative ones, so when the
|
|
@@ -1673,15 +1796,39 @@ export async function startGitEnumeration(
|
|
|
1673
1796
|
facts === null || facts.indexFile === ''
|
|
1674
1797
|
? undefined
|
|
1675
1798
|
: path.resolve(workspaceRoot, facts.indexFile)
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1799
|
+
// `ls-files` and `status` each read the index on their own. A write
|
|
1800
|
+
// between them (an unstage, a checkout) paired one index's OIDs with the
|
|
1801
|
+
// other's verdict: a `git reset` of a staged edit made the file clean to
|
|
1802
|
+
// `status` while `ls-files` had listed the staged blob, and that blob
|
|
1803
|
+
// keyed the committed bytes on disk. The index is stamped before the
|
|
1804
|
+
// listing and after the status; an index that moved trusts no OID.
|
|
1805
|
+
const indexStamp = (): string | undefined => {
|
|
1806
|
+
if (indexFile === undefined) return undefined
|
|
1807
|
+
const st = lstatSync(indexFile, { throwIfNoEntry: false })
|
|
1808
|
+
return st === undefined ? undefined : `${st.ino}:${st.ctimeMs}:${st.size}`
|
|
1809
|
+
}
|
|
1810
|
+
const stampBefore = indexStamp()
|
|
1811
|
+
// Four spawns at most (the rev-parse is memoized per process), one
|
|
1812
|
+
// worktree walk. `ls-files -s -v` reads the INDEX only
|
|
1813
|
+
// (~9 ms on a 1000-project tree) and answers two questions at once: every
|
|
1814
|
+
// tracked path's OID and its cache-state flag. `status -uall` is the one
|
|
1815
|
+
// command that walks the worktree, and it answers two as well: which
|
|
1816
|
+
// tracked paths are dirty AND which files are untracked. Asking
|
|
1817
|
+
// `ls-files --others` for the untracked set walked the same tree a second
|
|
1818
|
+
// time (~50 ms of CPU, concurrent with status but contending with it).
|
|
1819
|
+
const listing = spawnGit(['ls-files', '-s', '-v', '-z', '--', ...pathspecs])
|
|
1820
|
+
// Read synchronously: `status` spawns once the read is in, and an async
|
|
1821
|
+
// read's answer waited for the main thread to yield, behind the workspace
|
|
1822
|
+
// config and discovery (~5 ms of a 1,200-task warm run's wait for git).
|
|
1823
|
+
const readIndex = ():
|
|
1824
|
+
| { bytes: Uint8Array; hash: string; st: Stats; key: string; stamp: string }
|
|
1825
|
+
| undefined => {
|
|
1679
1826
|
if (indexFile === undefined) return undefined
|
|
1680
1827
|
try {
|
|
1681
1828
|
// Stat before the read: a write between them leaves an mtime older
|
|
1682
1829
|
// than the bytes, which only makes more entries racy.
|
|
1683
1830
|
const st = lstatSync(indexFile)
|
|
1684
|
-
const bytes =
|
|
1831
|
+
const bytes = readFileSync(indexFile)
|
|
1685
1832
|
const hash = xxh3hex(bytes)
|
|
1686
1833
|
return {
|
|
1687
1834
|
bytes,
|
|
@@ -1694,38 +1841,33 @@ export async function startGitEnumeration(
|
|
|
1694
1841
|
return undefined
|
|
1695
1842
|
}
|
|
1696
1843
|
}
|
|
1697
|
-
const
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
)
|
|
1703
|
-
const
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
env,
|
|
1725
|
-
)
|
|
1726
|
-
}),
|
|
1727
|
-
varsRun,
|
|
1728
|
-
])
|
|
1844
|
+
const read = readIndex()
|
|
1845
|
+
// `--ignored=matching` names an ignored path without descending into an
|
|
1846
|
+
// ignored directory: git applies an ignored `.gitattributes` as it does
|
|
1847
|
+
// any other, and the filter gate below must see it (A-19).
|
|
1848
|
+
// `--no-renames`: a deletion paired as an unmerged path's rename source
|
|
1849
|
+
// prints only as `UU <path>`, and the deleted file stayed trusted (A-59).
|
|
1850
|
+
const spawnStatus = (index: string | undefined) =>
|
|
1851
|
+
spawnGit(
|
|
1852
|
+
[
|
|
1853
|
+
'status',
|
|
1854
|
+
'--porcelain',
|
|
1855
|
+
'-z',
|
|
1856
|
+
'-uall',
|
|
1857
|
+
'--ignored=matching',
|
|
1858
|
+
'--no-renames',
|
|
1859
|
+
'--',
|
|
1860
|
+
...pathspecs,
|
|
1861
|
+
],
|
|
1862
|
+
undefined,
|
|
1863
|
+
index === undefined ? undefined : { GIT_INDEX_FILE: index },
|
|
1864
|
+
)
|
|
1865
|
+
// An index with no racy entry is read as it is: `status` spawns now.
|
|
1866
|
+
const statusRun =
|
|
1867
|
+
read === undefined || indexFile === undefined || !racyIndex(read, facts!.objectFormat)
|
|
1868
|
+
? spawnStatus(undefined)
|
|
1869
|
+
: refreshedIndex(indexFile, read, spawnGit).then(spawnStatus)
|
|
1870
|
+
const running = Promise.all([listing, statusRun, varsRun])
|
|
1729
1871
|
const gitPrefix = facts?.prefix ?? ''
|
|
1730
1872
|
const gitDir = facts?.commonDir ?? ''
|
|
1731
1873
|
const ls = await listing
|
|
@@ -1740,12 +1882,12 @@ export async function startGitEnumeration(
|
|
|
1740
1882
|
? spawnGit(CHECK_ATTR, [...parsedLs.oids.keys()].join('\0'))
|
|
1741
1883
|
: undefined
|
|
1742
1884
|
const [, status, vars] = await running
|
|
1885
|
+
const indexMoved = indexStamp() !== stampBefore
|
|
1743
1886
|
// The key stands for the index `ls-files` listed only when that index
|
|
1744
1887
|
// was written before the enumeration began (git writes it by rename, so
|
|
1745
1888
|
// the stamp moves with any write) and is still in place after it: a
|
|
1746
1889
|
// `git add` between the listing and the read paired one index's verdict
|
|
1747
1890
|
// with the other's entries. Otherwise this run asks for no verdict.
|
|
1748
|
-
const read = await indexKeyRead
|
|
1749
1891
|
let indexKey: string | undefined
|
|
1750
1892
|
if (
|
|
1751
1893
|
read !== undefined &&
|
|
@@ -1761,13 +1903,8 @@ export async function startGitEnumeration(
|
|
|
1761
1903
|
if (ls === null) {
|
|
1762
1904
|
throw gitSpawnRefusal(workspaceRoot)
|
|
1763
1905
|
}
|
|
1764
|
-
if (parsedLs === undefined)
|
|
1765
|
-
|
|
1766
|
-
throw new UserError(
|
|
1767
|
-
`vx requires git: ${workspaceRoot} is not inside a git work tree. ` +
|
|
1768
|
-
`Run 'git init' in your workspace root.${stderr ? ` (git: ${stderr})` : ''}`,
|
|
1769
|
-
)
|
|
1770
|
-
}
|
|
1906
|
+
if (parsedLs === undefined)
|
|
1907
|
+
throw gitFailure(workspaceRoot, 'ls-files', ls.exitCode, ls.stderr.trim())
|
|
1771
1908
|
const { files: tracked, oids, flagged, gitlinks, undecodable } = parsedLs
|
|
1772
1909
|
// Normalize `status`'s repo-root-relative paths to workspace-relative (strip
|
|
1773
1910
|
// the `--show-prefix`) so the dirty set is keyed identically to the trusted
|
|
@@ -1798,7 +1935,7 @@ export async function startGitEnumeration(
|
|
|
1798
1935
|
// this same status spawn so `run()` needs no second `git status`.
|
|
1799
1936
|
// null when the status spawn failed (non-repo / git error).
|
|
1800
1937
|
const worktreeDirty = dirty === null ? null : dirty.size > 0
|
|
1801
|
-
const trusted = dirty === null ? new Map<string, string>() : oids
|
|
1938
|
+
const trusted = dirty === null || indexMoved ? new Map<string, string>() : oids
|
|
1802
1939
|
if (dirty !== null) {
|
|
1803
1940
|
for (const rel of dirty) trusted.delete(rel)
|
|
1804
1941
|
}
|
|
@@ -1846,6 +1983,7 @@ export async function startGitEnumeration(
|
|
|
1846
1983
|
tracked: gitlinks.size === 0 ? tracked : tracked.concat([...gitlinks]),
|
|
1847
1984
|
scopes: pathspecs,
|
|
1848
1985
|
trusted,
|
|
1986
|
+
flagged: [...flagged],
|
|
1849
1987
|
dirty: worktreeDirty,
|
|
1850
1988
|
vars: vars !== null && vars.exitCode === 0 ? vars.stdout : null,
|
|
1851
1989
|
changed: dirty === null ? null : [...dirty, ...untracked],
|
|
@@ -1861,6 +1999,10 @@ export async function startGitEnumeration(
|
|
|
1861
1999
|
async blobs() {
|
|
1862
2000
|
const run = await spawnGit(['ls-files', '-s', '-v', '-z', '--debug', '--', ...pathspecs])
|
|
1863
2001
|
if (run === null || run.exitCode !== 0) return null
|
|
2002
|
+
// Sizes from an index written since the listing judge entries the
|
|
2003
|
+
// trusted OIDs did not come from: an add there rewrote a resized
|
|
2004
|
+
// entry to one that fits, and the listed OID stood (A-60).
|
|
2005
|
+
if (indexStamp() !== stampBefore) return null
|
|
1864
2006
|
const debug = splitLsDebug(run)
|
|
1865
2007
|
const indexed = new Map<string, { oid: string; size: number }>()
|
|
1866
2008
|
parseLsFilesOutput(debug.records, debug.undecodable, { sizes: debug.sizes, indexed })
|
|
@@ -1908,6 +2050,30 @@ export function lazyGitEnumeration(workspaceRoot: string): LazyGitEnumeration {
|
|
|
1908
2050
|
}
|
|
1909
2051
|
}
|
|
1910
2052
|
|
|
2053
|
+
/**
|
|
2054
|
+
* Each loose file's stat identity, `hashFile`'s memo key: mode, size,
|
|
2055
|
+
* mtime, ctime and inode. Undefined for a symlink (what it points to
|
|
2056
|
+
* folds too, and its stat does not move with that), a file gone, or one
|
|
2057
|
+
* changed within the racy window, where the memo trusts no stat either.
|
|
2058
|
+
*/
|
|
2059
|
+
function statRecords(
|
|
2060
|
+
rels: readonly string[],
|
|
2061
|
+
abs: (rel: string) => string,
|
|
2062
|
+
prefixLength: number,
|
|
2063
|
+
): string | undefined {
|
|
2064
|
+
if (rels.length === 0) return ''
|
|
2065
|
+
const now = Date.now()
|
|
2066
|
+
let out = ''
|
|
2067
|
+
for (const rel of rels) {
|
|
2068
|
+
const st = lstatSync(abs(rel), { throwIfNoEntry: false })
|
|
2069
|
+
if (st === undefined || !st.isFile()) return undefined
|
|
2070
|
+
if (now - Math.floor(st.ctimeMs) < racyWindowMs(st.ctimeMs, FILE_HASH_RACY_MS)) return
|
|
2071
|
+
const mode = (st.mode & 0o100) !== 0 ? '100755' : '100644'
|
|
2072
|
+
out += `${rel.slice(prefixLength)}\0stat:${mode}:${st.size}:${Math.floor(st.mtimeMs)}:${Math.floor(st.ctimeMs)}:${Number(st.ino)}\n`
|
|
2073
|
+
}
|
|
2074
|
+
return out
|
|
2075
|
+
}
|
|
2076
|
+
|
|
1911
2077
|
/** The partition half of `populateGitFilesCache`: store per-project slices of one enumeration. */
|
|
1912
2078
|
export async function applyGitEnumeration(
|
|
1913
2079
|
enumeration: GitEnumeration,
|
|
@@ -2006,15 +2172,20 @@ export async function applyGitEnumeration(
|
|
|
2006
2172
|
}
|
|
2007
2173
|
// One hash of the records joined: one per record was ~25 ms of 125,000.
|
|
2008
2174
|
let records: string | undefined = ''
|
|
2175
|
+
// A file git vouches for no OID (untracked, or changed since the
|
|
2176
|
+
// index) is named by its stat instead, the identity `hashFile`'s memo
|
|
2177
|
+
// trusts: a just-added config or an edited file turned the fold memo
|
|
2178
|
+
// off for its whole project (+3-5 ms of solid's warm run, 2026-10-22).
|
|
2179
|
+
const loose: string[] = []
|
|
2009
2180
|
for (let i = start; i < end; i++) {
|
|
2010
2181
|
const rel = sorted[i]!
|
|
2011
2182
|
const own = rel.slice(prefix.length)
|
|
2012
2183
|
matches.push(own)
|
|
2013
2184
|
if (records === undefined) continue
|
|
2014
2185
|
const oid = trusted.get(rel)
|
|
2015
|
-
if (
|
|
2016
|
-
|
|
2017
|
-
|
|
2186
|
+
if (undecodable?.has(rel) === true || oid?.startsWith('120000:') === true) records = undefined
|
|
2187
|
+
else if (oid === undefined) loose.push(rel)
|
|
2188
|
+
else records += `${own}\0${oid}\n`
|
|
2018
2189
|
}
|
|
2019
2190
|
// An empty slice is a directory git did not see, not an empty project:
|
|
2020
2191
|
// a project has at least its package.json, tracked or untracked. A
|
|
@@ -2033,7 +2204,13 @@ export async function applyGitEnumeration(
|
|
|
2033
2204
|
// setLazyOids AFTER set — set() drops the project's OID slot.
|
|
2034
2205
|
cache.set(projectDir, matches)
|
|
2035
2206
|
cache.setLazyOids(projectDir, projOids)
|
|
2036
|
-
if (records !== undefined)
|
|
2207
|
+
if (records !== undefined) {
|
|
2208
|
+
const known = records
|
|
2209
|
+
cache.noteFilesDigest(projectDir, matches, () => {
|
|
2210
|
+
const statted = statRecords(loose, projAbs, prefix.length)
|
|
2211
|
+
return statted === undefined ? undefined : xxh3hex(`${known}\0${statted}`)
|
|
2212
|
+
})
|
|
2213
|
+
}
|
|
2037
2214
|
}
|
|
2038
2215
|
if (workspaceWide) {
|
|
2039
2216
|
cache.set(workspaceRoot, all)
|
package/src/cache/index.ts
CHANGED
|
@@ -29,6 +29,7 @@ export {
|
|
|
29
29
|
WORKSPACE_OUTPUT_PREFIX,
|
|
30
30
|
type OutputDirRow,
|
|
31
31
|
OUTPUT_DIRS_CAP,
|
|
32
|
+
OUTPUT_DIRS_OVER_CAP,
|
|
32
33
|
FILE_HASH_RACY_MS,
|
|
33
34
|
OUTPUT_DIRS_RACY_MS,
|
|
34
35
|
racyWindowMs,
|
|
@@ -39,7 +40,7 @@ export {
|
|
|
39
40
|
export { ABSENT_INPUT, absentOr, CACHE_VERSION, foldHead } from './key-fold.js'
|
|
40
41
|
export type { FoldMemo } from './fold-memo.js'
|
|
41
42
|
export { measureFsClockLag, setFsClockLag } from './fs-clock.js'
|
|
42
|
-
export { filesBehind } from './linked-inputs.js'
|
|
43
|
+
export { behindLink, filesBehind } from './linked-inputs.js'
|
|
43
44
|
export {
|
|
44
45
|
addedInput,
|
|
45
46
|
type InputListing,
|
|
@@ -54,7 +55,9 @@ export {
|
|
|
54
55
|
type ResolveInputsArgs,
|
|
55
56
|
resolveOutputTree,
|
|
56
57
|
resolveOutputs,
|
|
58
|
+
outputWalkTakes,
|
|
57
59
|
resolveWorkspaceOutputs,
|
|
60
|
+
resolveWorkspaceOutputTree,
|
|
58
61
|
sweepRestoreTemps,
|
|
59
62
|
sweepStaging,
|
|
60
63
|
asTrees,
|
|
@@ -66,6 +69,7 @@ export {
|
|
|
66
69
|
stampOutputs,
|
|
67
70
|
stampWorkspaceOutputs,
|
|
68
71
|
stopRuntimeProbes,
|
|
72
|
+
answerRuntime,
|
|
69
73
|
type OutputStamp,
|
|
70
74
|
} from './inputs.js'
|
|
71
75
|
export {
|
|
@@ -84,10 +88,11 @@ export {
|
|
|
84
88
|
export {
|
|
85
89
|
type ArtifactInfo,
|
|
86
90
|
LayeredCache,
|
|
91
|
+
type LentAccess,
|
|
87
92
|
type RemoteCacheLayer,
|
|
88
93
|
type RemoteOpenContext,
|
|
89
94
|
} from './layered-cache.js'
|
|
90
|
-
export { networkConcurrency } from './remote-lane.js'
|
|
95
|
+
export { networkConcurrency, sayRemoteFault } from './remote-lane.js'
|
|
91
96
|
export type { ResolvedRemote } from './remote-lane.js'
|
|
92
97
|
export {
|
|
93
98
|
absentPluginCache,
|