@vibe-agent-toolkit/utils 0.2.0-rc.1 → 0.2.0-rc.3
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 +6 -3
- package/dist/compare-code-units.d.ts +28 -0
- package/dist/compare-code-units.d.ts.map +1 -0
- package/dist/compare-code-units.js +33 -0
- package/dist/compare-code-units.js.map +1 -0
- package/dist/crawl-timing.d.ts +680 -0
- package/dist/crawl-timing.d.ts.map +1 -0
- package/dist/crawl-timing.js +809 -0
- package/dist/crawl-timing.js.map +1 -0
- package/dist/file-crawler.d.ts +22 -0
- package/dist/file-crawler.d.ts.map +1 -1
- package/dist/file-crawler.js +31 -7
- package/dist/file-crawler.js.map +1 -1
- package/dist/fs-utils.d.ts +17 -0
- package/dist/fs-utils.d.ts.map +1 -1
- package/dist/fs-utils.js +69 -0
- package/dist/fs-utils.js.map +1 -1
- package/dist/fs.d.ts +3 -1
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +5 -1
- package/dist/fs.js.map +1 -1
- package/dist/git-run.d.ts +167 -0
- package/dist/git-run.d.ts.map +1 -0
- package/dist/git-run.js +154 -0
- package/dist/git-run.js.map +1 -0
- package/dist/git-snapshot.d.ts +181 -0
- package/dist/git-snapshot.d.ts.map +1 -0
- package/dist/git-snapshot.js +232 -0
- package/dist/git-snapshot.js.map +1 -0
- package/dist/git-tracker.d.ts +94 -1
- package/dist/git-tracker.d.ts.map +1 -1
- package/dist/git-tracker.js +135 -3
- package/dist/git-tracker.js.map +1 -1
- package/dist/git-utils.d.ts +38 -0
- package/dist/git-utils.d.ts.map +1 -1
- package/dist/git-utils.js +103 -80
- package/dist/git-utils.js.map +1 -1
- package/dist/git.d.ts +2 -1
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +2 -1
- package/dist/git.js.map +1 -1
- package/dist/gitignore-checker.d.ts.map +1 -1
- package/dist/gitignore-checker.js +6 -3
- package/dist/gitignore-checker.js.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +42 -1
- package/dist/index.js.map +1 -1
- package/dist/link-auth/expand-macro.d.ts.map +1 -1
- package/dist/link-auth/expand-macro.js +7 -1
- package/dist/link-auth/expand-macro.js.map +1 -1
- package/dist/link-auth/resolve-token.d.ts.map +1 -1
- package/dist/link-auth/resolve-token.js +15 -1
- package/dist/link-auth/resolve-token.js.map +1 -1
- package/dist/path-core.d.ts +1 -1
- package/dist/path-core.js +1 -1
- package/dist/project-utils.d.ts.map +1 -1
- package/dist/project-utils.js +6 -3
- package/dist/project-utils.js.map +1 -1
- package/dist/safe-exec.d.ts +38 -10
- package/dist/safe-exec.d.ts.map +1 -1
- package/dist/safe-exec.js +88 -19
- package/dist/safe-exec.js.map +1 -1
- package/dist/skill-test/index.d.ts +1 -1
- package/dist/skill-test/index.d.ts.map +1 -1
- package/dist/skill-test/index.js +1 -1
- package/dist/skill-test/index.js.map +1 -1
- package/dist/skill-test/spawn-claude.d.ts +28 -0
- package/dist/skill-test/spawn-claude.d.ts.map +1 -1
- package/dist/skill-test/spawn-claude.js +51 -2
- package/dist/skill-test/spawn-claude.js.map +1 -1
- package/dist/skill-test/transcript.d.ts +41 -2
- package/dist/skill-test/transcript.d.ts.map +1 -1
- package/dist/skill-test/transcript.js +47 -3
- package/dist/skill-test/transcript.js.map +1 -1
- package/dist/test-helpers.d.ts +185 -13
- package/dist/test-helpers.d.ts.map +1 -1
- package/dist/test-helpers.js +274 -27
- package/dist/test-helpers.js.map +1 -1
- package/dist/text-content.d.ts +197 -0
- package/dist/text-content.d.ts.map +1 -0
- package/dist/text-content.js +348 -0
- package/dist/text-content.js.map +1 -0
- package/dist/text-file.d.ts +45 -0
- package/dist/text-file.d.ts.map +1 -0
- package/dist/text-file.js +53 -0
- package/dist/text-file.js.map +1 -0
- package/dist/text.d.ts +16 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/text.js +16 -0
- package/dist/text.js.map +1 -0
- package/dist/timing-dump.d.ts +119 -0
- package/dist/timing-dump.d.ts.map +1 -0
- package/dist/timing-dump.js +164 -0
- package/dist/timing-dump.js.map +1 -0
- package/eslint/README.md +105 -3
- package/eslint/index.cjs +41 -0
- package/eslint/index.d.cts +7 -4
- package/eslint/rules/no-bare-symlink-in-tests.cjs +200 -0
- package/eslint/rules/no-raw-text-decode.cjs +296 -0
- package/eslint/rules/no-self-package-import.cjs +157 -0
- package/package.json +9 -2
package/dist/git-run.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one way to run `git` — safe by construction, for VAT and for anyone
|
|
3
|
+
* building on `@vibe-agent-toolkit/utils`.
|
|
4
|
+
*
|
|
5
|
+
* ## Why this exists rather than "remember to pass the right options"
|
|
6
|
+
*
|
|
7
|
+
* A `git` child inherits `GIT_DIR`, `GIT_INDEX_FILE`, `GIT_PREFIX` and friends
|
|
8
|
+
* from any ancestor `git` process, and **those override the `cwd` you passed**.
|
|
9
|
+
* The result is not an error — it is a well-formed answer about a different
|
|
10
|
+
* repository, at exit 0. Measured 2026-08-16 against real pre-commit hooks:
|
|
11
|
+
*
|
|
12
|
+
* - `git status --porcelain` at a caller-supplied path reported the *committing*
|
|
13
|
+
* repository's status, exit 0.
|
|
14
|
+
* - `git init` in a fresh directory silently re-initialized the inherited
|
|
15
|
+
* `GIT_DIR` — no `.git` appeared where it was asked for — and the following
|
|
16
|
+
* `add`/`commit` landed in a **bystander repository**, switching its branch
|
|
17
|
+
* and rewriting its index, while every step reported success.
|
|
18
|
+
* - `git rev-parse --show-toplevel` answered **correctly** throughout, so the
|
|
19
|
+
* obvious "am I in the right repository?" guard cannot detect any of it.
|
|
20
|
+
*
|
|
21
|
+
* The hazard is therefore invisible at every call site, which is exactly the
|
|
22
|
+
* kind of thing that must be handled by the default rather than by discipline.
|
|
23
|
+
*
|
|
24
|
+
* ## The default is the whole design
|
|
25
|
+
*
|
|
26
|
+
* **The environment is scrubbed unless you say `ambient: true`.** Almost every
|
|
27
|
+
* git command in a tool is about a path the caller was *handed* — a project
|
|
28
|
+
* root, a temp staging repo, a clone destination — and for those the inherited
|
|
29
|
+
* environment is always wrong. The rare command that genuinely means "the
|
|
30
|
+
* repository I am standing in" (`git remote get-url` inside the user's own
|
|
31
|
+
* checkout, say) opts out explicitly, and has to name itself when it does.
|
|
32
|
+
*
|
|
33
|
+
* ## What this is, mechanically: a set of defaults over `@vibe-validate/git`
|
|
34
|
+
*
|
|
35
|
+
* The spawn itself, the environment scrub and the treatment of a spawn-level
|
|
36
|
+
* error all live in `executeGitCommand`, which is that package's single
|
|
37
|
+
* chokepoint and is exercised by vibe-validate on every commit it gates. This
|
|
38
|
+
* module supplies only the three defaults VAT needs to differ on, and a result
|
|
39
|
+
* shape that never throws:
|
|
40
|
+
*
|
|
41
|
+
* - **`scrubGitEnv` is on unless `ambient: true`** — the inverse of
|
|
42
|
+
* `executeGitCommand`'s `false`. That default is right for vibe-validate,
|
|
43
|
+
* which mostly operates on the repository it was invoked in; it is wrong here,
|
|
44
|
+
* because VAT and its adopters are handed a root. Consolidating without this
|
|
45
|
+
* wrapper would relocate the opt-in rather than remove the hazard.
|
|
46
|
+
* - **`maxBuffer` is 64 MiB, never Node's 1 MiB** (`executeGitCommand` defaults
|
|
47
|
+
* to 10 MiB). `git ls-files -s -z` emits ~104 bytes per path, so an
|
|
48
|
+
* 8,500-file tree reaches 84% of Node's default and a larger one truncates.
|
|
49
|
+
* - **A spawn-level failure is a failure regardless of exit status**, which
|
|
50
|
+
* `executeGitCommand` now enforces itself; `ok` simply forwards its `success`.
|
|
51
|
+
*
|
|
52
|
+
* ⚠️ **One property was given up in the consolidation:** this used to resolve the
|
|
53
|
+
* binary with `which.sync('git')` and spawn an absolute path. `executeGitCommand`
|
|
54
|
+
* spawns bare `'git'` and lets the OS search `PATH`, so a "git is not on PATH"
|
|
55
|
+
* condition now arrives as an `ENOENT` in `error` rather than as a distinct
|
|
56
|
+
* pre-spawn message. Both are reported through {@link GitRunResult}, so no caller
|
|
57
|
+
* can mistake either for success.
|
|
58
|
+
*/
|
|
59
|
+
import { executeGitCommand } from '@vibe-validate/git';
|
|
60
|
+
/**
|
|
61
|
+
* 64 MiB. Chosen against `git ls-files -s -z`, the widest-output command here:
|
|
62
|
+
* ~104 bytes per path means this covers roughly 640,000 paths, while Node's
|
|
63
|
+
* 1 MiB default covers about 10,000 — a ceiling ordinary monorepos reach.
|
|
64
|
+
*/
|
|
65
|
+
const DEFAULT_MAX_BUFFER = 64 * 1024 * 1024;
|
|
66
|
+
/** Wall-clock ceiling on one invocation, so a hung remote cannot wedge a build. */
|
|
67
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
68
|
+
/**
|
|
69
|
+
* Run one `git` command.
|
|
70
|
+
*
|
|
71
|
+
* Never throws: a missing binary, a timeout and a non-zero exit are all reported
|
|
72
|
+
* through {@link GitRunResult} so a caller can tell them apart and choose. Check
|
|
73
|
+
* `ok`, not `status`.
|
|
74
|
+
*
|
|
75
|
+
* @param args - Arguments after the `git` executable, e.g. `['status', '--porcelain']`
|
|
76
|
+
* @param options - See {@link GitRunOptions}; the environment is scrubbed unless
|
|
77
|
+
* `ambient` is set
|
|
78
|
+
* @returns The outcome, including any spawn-level failure
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```typescript
|
|
82
|
+
* // About a path you were handed — the ordinary case, safe by default.
|
|
83
|
+
* const r = runGit(['status', '--porcelain'], { cwd: projectRoot });
|
|
84
|
+
* if (!r.ok) throw new Error(`git status failed: ${r.stderr}`);
|
|
85
|
+
* ```
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```typescript
|
|
89
|
+
* // About the repository this process is standing in — opt out explicitly.
|
|
90
|
+
* const url = runGit(['remote', 'get-url', 'origin'], { ambient: true });
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
export function runGit(args, options = {}) {
|
|
94
|
+
if (args.length === 0) {
|
|
95
|
+
// Handled here rather than by `executeGitCommand`, which THROWS on an empty
|
|
96
|
+
// argv. This function's whole contract is that it never throws.
|
|
97
|
+
return {
|
|
98
|
+
ok: false,
|
|
99
|
+
status: 1,
|
|
100
|
+
stdout: '',
|
|
101
|
+
stderr: 'runGit() needs at least one argument',
|
|
102
|
+
error: new Error('runGit() needs at least one argument'),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// `ignoreErrors` is what makes this non-throwing, and it is safe to set
|
|
106
|
+
// unconditionally only because `executeGitCommand` now reports `error` on that
|
|
107
|
+
// path too. Before that fix it dropped the spawn cause entirely, and a caller
|
|
108
|
+
// reading the result could not tell "git is not installed" from "exit 1 is the
|
|
109
|
+
// answer" from "your listing was truncated" — which is why this wrapper could
|
|
110
|
+
// not have been written against the previous release.
|
|
111
|
+
const result = executeGitCommand([...args], {
|
|
112
|
+
...(options.cwd === undefined ? {} : { cwd: options.cwd }),
|
|
113
|
+
...(options.env === undefined ? {} : { env: options.env }),
|
|
114
|
+
...(options.input === undefined ? {} : { stdin: options.input }),
|
|
115
|
+
scrubGitEnv: options.ambient !== true,
|
|
116
|
+
suppressStderr: options.suppressStderr ?? false,
|
|
117
|
+
trimOutput: options.trim ?? true,
|
|
118
|
+
ignoreErrors: true,
|
|
119
|
+
timeout: options.timeout ?? DEFAULT_TIMEOUT_MS,
|
|
120
|
+
maxBuffer: options.maxBuffer ?? DEFAULT_MAX_BUFFER,
|
|
121
|
+
});
|
|
122
|
+
return {
|
|
123
|
+
ok: result.success,
|
|
124
|
+
status: result.exitCode,
|
|
125
|
+
stdout: result.stdout,
|
|
126
|
+
stderr: result.stderr,
|
|
127
|
+
...(result.error === undefined ? {} : { error: result.error }),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* {@link runGit}, but a failure throws instead of being reported.
|
|
132
|
+
*
|
|
133
|
+
* For callers whose next line has no meaning if the command did not work —
|
|
134
|
+
* setting up a fixture repository, or any step whose failure should stop the
|
|
135
|
+
* run rather than be interpreted. Prefer {@link runGit} wherever a non-zero
|
|
136
|
+
* exit carries information ("no commit yet", "not a repository", "no such
|
|
137
|
+
* remote"), because those are answers rather than faults.
|
|
138
|
+
*
|
|
139
|
+
* @param args - Arguments after the `git` executable
|
|
140
|
+
* @param options - See {@link GitRunOptions}
|
|
141
|
+
* @returns The trimmed stdout
|
|
142
|
+
* @throws {Error} When git could not run, or ran and exited non-zero
|
|
143
|
+
*/
|
|
144
|
+
export function runGitOrThrow(args, options = {}) {
|
|
145
|
+
const result = runGit(args, options);
|
|
146
|
+
if (result.ok)
|
|
147
|
+
return result.stdout;
|
|
148
|
+
// The subcommand only: a commit message can be hundreds of KB, and
|
|
149
|
+
// interpolating the whole argv buries the actual error in CI output.
|
|
150
|
+
const label = args[0] ?? '<no args>';
|
|
151
|
+
const detail = result.stderr || `exit ${String(result.status)}`;
|
|
152
|
+
throw new Error(`git ${label} failed: ${detail}`);
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=git-run.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-run.js","sourceRoot":"","sources":["../src/git-run.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEvD;;;;GAIG;AACH,MAAM,kBAAkB,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAE5C,mFAAmF;AACnF,MAAM,kBAAkB,GAAG,MAAM,CAAC;AA8ElC;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,UAAU,MAAM,CAAC,IAAuB,EAAE,UAAyB,EAAE;IACzE,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,4EAA4E;QAC5E,gEAAgE;QAChE,OAAO;YACL,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,CAAC;YACT,MAAM,EAAE,EAAE;YACV,MAAM,EAAE,sCAAsC;YAC9C,KAAK,EAAE,IAAI,KAAK,CAAC,sCAAsC,CAAC;SACzD,CAAC;IACJ,CAAC;IAED,wEAAwE;IACxE,+EAA+E;IAC/E,8EAA8E;IAC9E,+EAA+E;IAC/E,8EAA8E;IAC9E,sDAAsD;IACtD,MAAM,MAAM,GAAG,iBAAiB,CAAC,CAAC,GAAG,IAAI,CAAC,EAAE;QAC1C,GAAG,CAAC,OAAO,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC;QAC1D,GAAG,CAAC,OAAO,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC;QAC1D,GAAG,CAAC,OAAO,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC;QAChE,WAAW,EAAE,OAAO,CAAC,OAAO,KAAK,IAAI;QACrC,cAAc,EAAE,OAAO,CAAC,cAAc,IAAI,KAAK;QAC/C,UAAU,EAAE,OAAO,CAAC,IAAI,IAAI,IAAI;QAChC,YAAY,EAAE,IAAI;QAClB,OAAO,EAAE,OAAO,CAAC,OAAO,IAAI,kBAAkB;QAC9C,SAAS,EAAE,OAAO,CAAC,SAAS,IAAI,kBAAkB;KACnD,CAAC,CAAC;IAEH,OAAO;QACL,EAAE,EAAE,MAAM,CAAC,OAAO;QAClB,MAAM,EAAE,MAAM,CAAC,QAAQ;QACvB,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,GAAG,CAAC,MAAM,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC;KAC/D,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,aAAa,CAAC,IAAuB,EAAE,UAAyB,EAAE;IAChF,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACrC,IAAI,MAAM,CAAC,EAAE;QAAE,OAAO,MAAM,CAAC,MAAM,CAAC;IAEpC,mEAAmE;IACnE,qEAAqE;IACrE,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,WAAW,CAAC;IACrC,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,IAAI,QAAQ,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;IAChE,MAAM,IAAI,KAAK,CAAC,OAAO,KAAK,YAAY,MAAM,EAAE,CAAC,CAAC;AACpD,CAAC"}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A **dirty-corrected git tree snapshot**, rebased onto absolute paths.
|
|
3
|
+
*
|
|
4
|
+
* `@vibe-validate/git`'s `getGitTreeSnapshot` answers the question git is
|
|
5
|
+
* uniquely good at: every path git can see under a directory, each with a blob
|
|
6
|
+
* OID naming *the bytes that are actually on disk right now* — dirty working-tree
|
|
7
|
+
* edits included, not the stale committed-index SHA — plus one deterministic
|
|
8
|
+
* `write-tree` hash over the whole set. Membership is `git add --all` without
|
|
9
|
+
* `--force`, i.e. `tracked ∪ (untracked ∧ ¬ignored)`.
|
|
10
|
+
*
|
|
11
|
+
* This module is the thin layer VAT needs on top, and it exists for one reason
|
|
12
|
+
* that is easy to get wrong: **git spells those paths relative to the repository
|
|
13
|
+
* root, not to the directory you asked about.** A caller scanning
|
|
14
|
+
* `<repo>/packages/foo` gets back `packages/foo/x.md`, and a caller that joined
|
|
15
|
+
* those onto its own root would build `<repo>/packages/foo/packages/foo/x.md` —
|
|
16
|
+
* a path that does not exist, so every consumer reads it as "absent" rather than
|
|
17
|
+
* as a bug. Resolving against the repository root here makes that unrepresentable
|
|
18
|
+
* at the call site.
|
|
19
|
+
*
|
|
20
|
+
* ## ⚠️ This is not a pure read
|
|
21
|
+
*
|
|
22
|
+
* Taking a snapshot runs `git add --all` against a throwaway index, which
|
|
23
|
+
* **writes loose blob objects into the target repository's `.git/objects`** for
|
|
24
|
+
* any content git has not already stored. The real index and working tree are
|
|
25
|
+
* never touched, and the objects are ordinary unreferenced blobs that `git gc`
|
|
26
|
+
* reclaims — but a command that advertises itself as read-only is, at the byte
|
|
27
|
+
* level, not. `vibe-validate` accepts this cost on every commit it gates; a VAT
|
|
28
|
+
* lane that adopts it inherits the same trade rather than a different one.
|
|
29
|
+
*
|
|
30
|
+
* ## ⚠️ A symlink's OID names its TARGET STRING, not a file's bytes
|
|
31
|
+
*
|
|
32
|
+
* Git stores a symlink as a blob whose content is the link target, under mode
|
|
33
|
+
* `120000`. Two links with the same relative target but different resolutions
|
|
34
|
+
* therefore share an OID while a consumer that follows them reads two different
|
|
35
|
+
* documents. {@link GitSnapshotEntry.isSymlink} is computed here precisely so
|
|
36
|
+
* that a consumer keying work off `oid` can exclude them — see
|
|
37
|
+
* `packages/resources/src/content-key.ts`, whose standing rule is that a git SHA
|
|
38
|
+
* may be a *lookup hint whose miss is free* and must never be the key itself.
|
|
39
|
+
*
|
|
40
|
+
* ## ⚠️ Two consumers want the SAME snapshot — see {@link withGitSnapshotCache}
|
|
41
|
+
*
|
|
42
|
+
* One command takes this snapshot twice, of one repository, sequentially, and
|
|
43
|
+
* keeps a different half of each: the projection store keeps `hash` as its
|
|
44
|
+
* cache key, and the git crawl source keeps `entries`. Bracketing the command
|
|
45
|
+
* makes that one snapshot. Everything outside the bracket still pays per call,
|
|
46
|
+
* deliberately.
|
|
47
|
+
*/
|
|
48
|
+
/** One path in a {@link GitTreeSnapshot}, located absolutely. */
|
|
49
|
+
export interface GitSnapshotEntry {
|
|
50
|
+
/** Absolute, forward-slashed path, resolved against the repository root. */
|
|
51
|
+
absolutePath: string;
|
|
52
|
+
/**
|
|
53
|
+
* Blob OID for this path's **on-disk** bytes.
|
|
54
|
+
*
|
|
55
|
+
* Equal OIDs mean equal bytes, which is what makes this usable as a lookup
|
|
56
|
+
* hint. It is NOT usable as a content key — see the module docstring.
|
|
57
|
+
*/
|
|
58
|
+
oid: string;
|
|
59
|
+
/** Git's six-digit mode, verbatim: `100644`, `100755`, `120000`, `160000`. */
|
|
60
|
+
mode: string;
|
|
61
|
+
/** Mode `120000`. Its {@link GitSnapshotEntry.oid} is the target string. */
|
|
62
|
+
isSymlink: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Mode `160000` — a submodule.
|
|
65
|
+
*
|
|
66
|
+
* The OID is a **commit**, not a blob, so `cat-file` on it yields no file
|
|
67
|
+
* bytes, and none of the submodule's own files appear anywhere in the
|
|
68
|
+
* snapshot. A consumer that needs them takes a second snapshot rooted there.
|
|
69
|
+
*/
|
|
70
|
+
isSubmodule: boolean;
|
|
71
|
+
}
|
|
72
|
+
/** One repository's snapshot. */
|
|
73
|
+
export interface GitTreeSnapshot {
|
|
74
|
+
/**
|
|
75
|
+
* `git write-tree` over the snapshot — a deterministic key for the whole set.
|
|
76
|
+
*
|
|
77
|
+
* Byte-identical content always produces it, because a tree object carries no
|
|
78
|
+
* timestamp. (A `stash create` would not: a stash is a commit, and two calls
|
|
79
|
+
* over identical content agree only within the same wall-clock second.)
|
|
80
|
+
*/
|
|
81
|
+
hash: string;
|
|
82
|
+
/** The repository root every entry was resolved against, forward-slashed. */
|
|
83
|
+
repositoryRoot: string;
|
|
84
|
+
/** Every path git can see, in git's own order. */
|
|
85
|
+
entries: GitSnapshotEntry[];
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Run work that may snapshot the same repository more than once, and pay for
|
|
89
|
+
* each repository ONCE.
|
|
90
|
+
*
|
|
91
|
+
* ## What this buys, in both currencies
|
|
92
|
+
*
|
|
93
|
+
* A snapshot is not a read — it copies the index, runs `git add --all` into it
|
|
94
|
+
* and then `git write-tree`. One `vat` command took two of them, of the same
|
|
95
|
+
* repository, back to back: `openPopulationCache` needs `hash` to key the
|
|
96
|
+
* projection store, and `GitCrawlSource` needs `entries` to enumerate. Measured
|
|
97
|
+
* on a large monorepo, the pair cost 195.22 ms and 158.55 ms.
|
|
98
|
+
*
|
|
99
|
+
* The cost is the smaller half. Two snapshots taken 195 ms apart are two
|
|
100
|
+
* *different answers* whenever the working tree changes in between, and the
|
|
101
|
+
* command then files the second one's extent under the first one's key — a
|
|
102
|
+
* cache entry whose key does not describe its contents, produced silently and
|
|
103
|
+
* discoverable only as a wrong answer much later. One snapshot removes the race
|
|
104
|
+
* rather than merely the duplicate.
|
|
105
|
+
*
|
|
106
|
+
* ## Where to open it
|
|
107
|
+
*
|
|
108
|
+
* At the level that already brackets the whole command, so that it encloses
|
|
109
|
+
* *every* snapshotting consumer. Opened deeper than one of them, the dedupe
|
|
110
|
+
* silently does nothing and looks exactly like a dedupe that works.
|
|
111
|
+
*
|
|
112
|
+
* ## What it deliberately does NOT do
|
|
113
|
+
*
|
|
114
|
+
* Inside the bracket, a working-tree edit made between two calls is not
|
|
115
|
+
* observed by the second — the first snapshot is the answer for the whole
|
|
116
|
+
* bracket. That is the race being closed, not a limitation to work around. Work
|
|
117
|
+
* that must see edits as they land does not belong inside one bracket.
|
|
118
|
+
*
|
|
119
|
+
* Nesting is safe and inner-most wins: an inner bracket starts an empty memo,
|
|
120
|
+
* and the outer one's entries are restored when it returns.
|
|
121
|
+
*
|
|
122
|
+
* @param run - The work to run with the memo open. Sync or async — the return
|
|
123
|
+
* value is passed straight through, so an async `run` keeps the memo for the
|
|
124
|
+
* whole promise it returns
|
|
125
|
+
* @returns Whatever `run` returned
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```typescript
|
|
129
|
+
* return withGitSnapshotCache(async () => {
|
|
130
|
+
* const opened = await openPopulationCache(options); // snapshots
|
|
131
|
+
* return work(opened?.cache); // crawls, snapshots
|
|
132
|
+
* });
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
export declare function withGitSnapshotCache<T>(run: () => T): T;
|
|
136
|
+
/**
|
|
137
|
+
* Snapshot everything git can see under a directory.
|
|
138
|
+
*
|
|
139
|
+
* Inside a {@link withGitSnapshotCache} bracket the answer for a given
|
|
140
|
+
* repository is taken once and reused — including a `null`. Outside one, every
|
|
141
|
+
* call spawns git, which is what it has always done.
|
|
142
|
+
*
|
|
143
|
+
* @param options - Where to look
|
|
144
|
+
* @param options.cwd - Any directory inside the repository of interest. Git
|
|
145
|
+
* resolves upward to the worktree root, and the snapshot covers that whole
|
|
146
|
+
* root — NOT only the subtree named here. Narrowing is the caller's job, and
|
|
147
|
+
* is why {@link GitSnapshotEntry.absolutePath} is absolute
|
|
148
|
+
* @returns The snapshot, or `null` when git could not answer — no `git` on
|
|
149
|
+
* `PATH`, not a repository, a bare or unreadable one. An empty `entries` is a
|
|
150
|
+
* real answer (an initialized repository with no files) and stays
|
|
151
|
+
* distinguishable from it
|
|
152
|
+
*/
|
|
153
|
+
export declare function gitTreeSnapshot(options: {
|
|
154
|
+
cwd: string;
|
|
155
|
+
}): GitTreeSnapshot | null;
|
|
156
|
+
/**
|
|
157
|
+
* The snapshot this bracket ALREADY holds for a repository — never a new one.
|
|
158
|
+
*
|
|
159
|
+
* The difference from {@link gitTreeSnapshot} is the whole reason this exists:
|
|
160
|
+
* that function TAKES a snapshot when the memo misses, and a snapshot is three
|
|
161
|
+
* spawns and a `git add --all`. This one only ever reports what a previous
|
|
162
|
+
* caller already paid for, so a consumer can prefer it and fall back to its own
|
|
163
|
+
* cheaper question without any risk of turning a miss into the most expensive
|
|
164
|
+
* git operation in the package.
|
|
165
|
+
*
|
|
166
|
+
* That asymmetry is what makes it safe to reach for from shared code. The
|
|
167
|
+
* {@link GitTracker} does exactly this: inside the projection's git lane a
|
|
168
|
+
* snapshot is already in hand and describes precisely the set its own
|
|
169
|
+
* `git ls-files --cached --others --exclude-standard` would spawn to rebuild,
|
|
170
|
+
* while on the incumbent walk no snapshot is ever taken and the tracker must
|
|
171
|
+
* keep spawning. One consumer, two lanes, and neither pays for the other's
|
|
172
|
+
* strategy.
|
|
173
|
+
*
|
|
174
|
+
* @param cwd - Any directory inside the repository of interest
|
|
175
|
+
* @returns The memoized snapshot, or `undefined` when this bracket has none —
|
|
176
|
+
* which includes there being no bracket open at all, and includes a memoized
|
|
177
|
+
* `null` (git could not answer). Callers treat all three the same way: ask
|
|
178
|
+
* your own question instead
|
|
179
|
+
*/
|
|
180
|
+
export declare function peekGitTreeSnapshot(cwd: string): GitTreeSnapshot | undefined;
|
|
181
|
+
//# sourceMappingURL=git-snapshot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-snapshot.d.ts","sourceRoot":"","sources":["../src/git-snapshot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AASH,iEAAiE;AACjE,MAAM,WAAW,gBAAgB;IAC/B,4EAA4E;IAC5E,YAAY,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,SAAS,EAAE,OAAO,CAAC;IACnB;;;;;;OAMG;IACH,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,iCAAiC;AACjC,MAAM,WAAW,eAAe;IAC9B;;;;;;OAMG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,cAAc,EAAE,MAAM,CAAC;IACvB,kDAAkD;IAClD,OAAO,EAAE,gBAAgB,EAAE,CAAC;CAC7B;AAuBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,CAEvD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,eAAe,GAAG,IAAI,CA2BhF;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAY5E"}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A **dirty-corrected git tree snapshot**, rebased onto absolute paths.
|
|
3
|
+
*
|
|
4
|
+
* `@vibe-validate/git`'s `getGitTreeSnapshot` answers the question git is
|
|
5
|
+
* uniquely good at: every path git can see under a directory, each with a blob
|
|
6
|
+
* OID naming *the bytes that are actually on disk right now* — dirty working-tree
|
|
7
|
+
* edits included, not the stale committed-index SHA — plus one deterministic
|
|
8
|
+
* `write-tree` hash over the whole set. Membership is `git add --all` without
|
|
9
|
+
* `--force`, i.e. `tracked ∪ (untracked ∧ ¬ignored)`.
|
|
10
|
+
*
|
|
11
|
+
* This module is the thin layer VAT needs on top, and it exists for one reason
|
|
12
|
+
* that is easy to get wrong: **git spells those paths relative to the repository
|
|
13
|
+
* root, not to the directory you asked about.** A caller scanning
|
|
14
|
+
* `<repo>/packages/foo` gets back `packages/foo/x.md`, and a caller that joined
|
|
15
|
+
* those onto its own root would build `<repo>/packages/foo/packages/foo/x.md` —
|
|
16
|
+
* a path that does not exist, so every consumer reads it as "absent" rather than
|
|
17
|
+
* as a bug. Resolving against the repository root here makes that unrepresentable
|
|
18
|
+
* at the call site.
|
|
19
|
+
*
|
|
20
|
+
* ## ⚠️ This is not a pure read
|
|
21
|
+
*
|
|
22
|
+
* Taking a snapshot runs `git add --all` against a throwaway index, which
|
|
23
|
+
* **writes loose blob objects into the target repository's `.git/objects`** for
|
|
24
|
+
* any content git has not already stored. The real index and working tree are
|
|
25
|
+
* never touched, and the objects are ordinary unreferenced blobs that `git gc`
|
|
26
|
+
* reclaims — but a command that advertises itself as read-only is, at the byte
|
|
27
|
+
* level, not. `vibe-validate` accepts this cost on every commit it gates; a VAT
|
|
28
|
+
* lane that adopts it inherits the same trade rather than a different one.
|
|
29
|
+
*
|
|
30
|
+
* ## ⚠️ A symlink's OID names its TARGET STRING, not a file's bytes
|
|
31
|
+
*
|
|
32
|
+
* Git stores a symlink as a blob whose content is the link target, under mode
|
|
33
|
+
* `120000`. Two links with the same relative target but different resolutions
|
|
34
|
+
* therefore share an OID while a consumer that follows them reads two different
|
|
35
|
+
* documents. {@link GitSnapshotEntry.isSymlink} is computed here precisely so
|
|
36
|
+
* that a consumer keying work off `oid` can exclude them — see
|
|
37
|
+
* `packages/resources/src/content-key.ts`, whose standing rule is that a git SHA
|
|
38
|
+
* may be a *lookup hint whose miss is free* and must never be the key itself.
|
|
39
|
+
*
|
|
40
|
+
* ## ⚠️ Two consumers want the SAME snapshot — see {@link withGitSnapshotCache}
|
|
41
|
+
*
|
|
42
|
+
* One command takes this snapshot twice, of one repository, sequentially, and
|
|
43
|
+
* keeps a different half of each: the projection store keeps `hash` as its
|
|
44
|
+
* cache key, and the git crawl source keeps `entries`. Bracketing the command
|
|
45
|
+
* makes that one snapshot. Everything outside the bracket still pays per call,
|
|
46
|
+
* deliberately.
|
|
47
|
+
*/
|
|
48
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
49
|
+
import { getGitTreeSnapshot, GIT_MODE_GITLINK, GIT_MODE_SYMLINK } from '@vibe-validate/git';
|
|
50
|
+
import { gitFindRoot } from './git-utils.js';
|
|
51
|
+
import { safePath } from './path-utils.js';
|
|
52
|
+
/**
|
|
53
|
+
* The snapshots taken inside the bracket now open, keyed by repository root.
|
|
54
|
+
*
|
|
55
|
+
* `AsyncLocalStorage` rather than a module-level `Map`, and the distinction is
|
|
56
|
+
* the whole design. A blanket memo would dedupe the same two calls — and would
|
|
57
|
+
* then hand a stale snapshot to anything that mutates a repository and
|
|
58
|
+
* re-snapshots it in the same process, which is what a vitest worker running
|
|
59
|
+
* many suites does all day. Scoping it to a bracket makes the memo's lifetime
|
|
60
|
+
* something a caller states rather than something the module assumes; outside
|
|
61
|
+
* one, {@link gitTreeSnapshot} behaves exactly as it did before this existed.
|
|
62
|
+
*
|
|
63
|
+
* Async-scoped rather than a plain variable because the bracket spans a whole
|
|
64
|
+
* command's `await`s, and two commands may be open at once in one process —
|
|
65
|
+
* `crawl-timing.ts` reaches for the same primitive for the same reason.
|
|
66
|
+
*
|
|
67
|
+
* `null` is a stored value, not an absence: "git could not answer" costs a full
|
|
68
|
+
* `add`/`write-tree` attempt to discover, so repeating it is pure cost. Absence
|
|
69
|
+
* is `undefined`, which `Map.get` returns and this module never stores.
|
|
70
|
+
*/
|
|
71
|
+
const snapshotsInBracket = new AsyncLocalStorage();
|
|
72
|
+
/**
|
|
73
|
+
* Run work that may snapshot the same repository more than once, and pay for
|
|
74
|
+
* each repository ONCE.
|
|
75
|
+
*
|
|
76
|
+
* ## What this buys, in both currencies
|
|
77
|
+
*
|
|
78
|
+
* A snapshot is not a read — it copies the index, runs `git add --all` into it
|
|
79
|
+
* and then `git write-tree`. One `vat` command took two of them, of the same
|
|
80
|
+
* repository, back to back: `openPopulationCache` needs `hash` to key the
|
|
81
|
+
* projection store, and `GitCrawlSource` needs `entries` to enumerate. Measured
|
|
82
|
+
* on a large monorepo, the pair cost 195.22 ms and 158.55 ms.
|
|
83
|
+
*
|
|
84
|
+
* The cost is the smaller half. Two snapshots taken 195 ms apart are two
|
|
85
|
+
* *different answers* whenever the working tree changes in between, and the
|
|
86
|
+
* command then files the second one's extent under the first one's key — a
|
|
87
|
+
* cache entry whose key does not describe its contents, produced silently and
|
|
88
|
+
* discoverable only as a wrong answer much later. One snapshot removes the race
|
|
89
|
+
* rather than merely the duplicate.
|
|
90
|
+
*
|
|
91
|
+
* ## Where to open it
|
|
92
|
+
*
|
|
93
|
+
* At the level that already brackets the whole command, so that it encloses
|
|
94
|
+
* *every* snapshotting consumer. Opened deeper than one of them, the dedupe
|
|
95
|
+
* silently does nothing and looks exactly like a dedupe that works.
|
|
96
|
+
*
|
|
97
|
+
* ## What it deliberately does NOT do
|
|
98
|
+
*
|
|
99
|
+
* Inside the bracket, a working-tree edit made between two calls is not
|
|
100
|
+
* observed by the second — the first snapshot is the answer for the whole
|
|
101
|
+
* bracket. That is the race being closed, not a limitation to work around. Work
|
|
102
|
+
* that must see edits as they land does not belong inside one bracket.
|
|
103
|
+
*
|
|
104
|
+
* Nesting is safe and inner-most wins: an inner bracket starts an empty memo,
|
|
105
|
+
* and the outer one's entries are restored when it returns.
|
|
106
|
+
*
|
|
107
|
+
* @param run - The work to run with the memo open. Sync or async — the return
|
|
108
|
+
* value is passed straight through, so an async `run` keeps the memo for the
|
|
109
|
+
* whole promise it returns
|
|
110
|
+
* @returns Whatever `run` returned
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```typescript
|
|
114
|
+
* return withGitSnapshotCache(async () => {
|
|
115
|
+
* const opened = await openPopulationCache(options); // snapshots
|
|
116
|
+
* return work(opened?.cache); // crawls, snapshots
|
|
117
|
+
* });
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
export function withGitSnapshotCache(run) {
|
|
121
|
+
return snapshotsInBracket.run(new Map(), run);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Snapshot everything git can see under a directory.
|
|
125
|
+
*
|
|
126
|
+
* Inside a {@link withGitSnapshotCache} bracket the answer for a given
|
|
127
|
+
* repository is taken once and reused — including a `null`. Outside one, every
|
|
128
|
+
* call spawns git, which is what it has always done.
|
|
129
|
+
*
|
|
130
|
+
* @param options - Where to look
|
|
131
|
+
* @param options.cwd - Any directory inside the repository of interest. Git
|
|
132
|
+
* resolves upward to the worktree root, and the snapshot covers that whole
|
|
133
|
+
* root — NOT only the subtree named here. Narrowing is the caller's job, and
|
|
134
|
+
* is why {@link GitSnapshotEntry.absolutePath} is absolute
|
|
135
|
+
* @returns The snapshot, or `null` when git could not answer — no `git` on
|
|
136
|
+
* `PATH`, not a repository, a bare or unreadable one. An empty `entries` is a
|
|
137
|
+
* real answer (an initialized repository with no files) and stays
|
|
138
|
+
* distinguishable from it
|
|
139
|
+
*/
|
|
140
|
+
export function gitTreeSnapshot(options) {
|
|
141
|
+
// Resolved from the filesystem BEFORE spawning anything, and deliberately not
|
|
142
|
+
// from a `rev-parse` of our own: `getGitTreeSnapshot` has already paid for that
|
|
143
|
+
// question internally, and asking it twice invites the two answers to disagree
|
|
144
|
+
// on a worktree, where the git directory and the worktree root are different
|
|
145
|
+
// places. `gitFindRoot` is memoized, so this is free after the first call.
|
|
146
|
+
const repositoryRoot = gitFindRoot(options.cwd);
|
|
147
|
+
if (repositoryRoot === null) {
|
|
148
|
+
// Not memoized, and it does not need to be: this branch spawns nothing, and
|
|
149
|
+
// `gitFindRoot` has its own memo. There is also no key to file it under —
|
|
150
|
+
// the memo is keyed by repository root, and this is the case with none.
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
// Keyed by the RESOLVED root rather than by `options.cwd`, because a snapshot
|
|
154
|
+
// covers the whole repository however deep the directory it was asked from.
|
|
155
|
+
// The two call sites this bracket exists for pass different directories — the
|
|
156
|
+
// corpus root and the project root — so keying on `cwd` would produce two
|
|
157
|
+
// entries holding the identical answer, i.e. a dedupe that does nothing in
|
|
158
|
+
// exactly the case it was written for.
|
|
159
|
+
const memo = snapshotsInBracket.getStore();
|
|
160
|
+
const memoized = memo?.get(repositoryRoot);
|
|
161
|
+
if (memoized !== undefined)
|
|
162
|
+
return memoized;
|
|
163
|
+
const snapshot = takeSnapshot(options.cwd, repositoryRoot);
|
|
164
|
+
memo?.set(repositoryRoot, snapshot);
|
|
165
|
+
return snapshot;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* The snapshot this bracket ALREADY holds for a repository — never a new one.
|
|
169
|
+
*
|
|
170
|
+
* The difference from {@link gitTreeSnapshot} is the whole reason this exists:
|
|
171
|
+
* that function TAKES a snapshot when the memo misses, and a snapshot is three
|
|
172
|
+
* spawns and a `git add --all`. This one only ever reports what a previous
|
|
173
|
+
* caller already paid for, so a consumer can prefer it and fall back to its own
|
|
174
|
+
* cheaper question without any risk of turning a miss into the most expensive
|
|
175
|
+
* git operation in the package.
|
|
176
|
+
*
|
|
177
|
+
* That asymmetry is what makes it safe to reach for from shared code. The
|
|
178
|
+
* {@link GitTracker} does exactly this: inside the projection's git lane a
|
|
179
|
+
* snapshot is already in hand and describes precisely the set its own
|
|
180
|
+
* `git ls-files --cached --others --exclude-standard` would spawn to rebuild,
|
|
181
|
+
* while on the incumbent walk no snapshot is ever taken and the tracker must
|
|
182
|
+
* keep spawning. One consumer, two lanes, and neither pays for the other's
|
|
183
|
+
* strategy.
|
|
184
|
+
*
|
|
185
|
+
* @param cwd - Any directory inside the repository of interest
|
|
186
|
+
* @returns The memoized snapshot, or `undefined` when this bracket has none —
|
|
187
|
+
* which includes there being no bracket open at all, and includes a memoized
|
|
188
|
+
* `null` (git could not answer). Callers treat all three the same way: ask
|
|
189
|
+
* your own question instead
|
|
190
|
+
*/
|
|
191
|
+
export function peekGitTreeSnapshot(cwd) {
|
|
192
|
+
const memo = snapshotsInBracket.getStore();
|
|
193
|
+
if (memo === undefined)
|
|
194
|
+
return undefined;
|
|
195
|
+
// Resolved the same way `gitTreeSnapshot` resolves it, because the memo is
|
|
196
|
+
// keyed by repository root and not by the directory anyone asked from. A peek
|
|
197
|
+
// keyed on `cwd` would miss every time the snapshot was taken from a different
|
|
198
|
+
// depth — which is the normal case, and a miss here is silent.
|
|
199
|
+
const repositoryRoot = gitFindRoot(cwd);
|
|
200
|
+
if (repositoryRoot === null)
|
|
201
|
+
return undefined;
|
|
202
|
+
return memo.get(repositoryRoot) ?? undefined;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Ask git, and rebase the answer onto absolute paths.
|
|
206
|
+
*
|
|
207
|
+
* Split out so {@link gitTreeSnapshot} reads as "key, look up, or take one" —
|
|
208
|
+
* the memo has to sit above the spawn, and a memo interleaved with the mapping
|
|
209
|
+
* is how a later edit ends up caching the wrong half.
|
|
210
|
+
*
|
|
211
|
+
* @param cwd - The directory to ask git from
|
|
212
|
+
* @param repositoryRoot - The already-resolved root every entry is rebased onto
|
|
213
|
+
* @returns The snapshot, or `null` when git could not answer
|
|
214
|
+
*/
|
|
215
|
+
function takeSnapshot(cwd, repositoryRoot) {
|
|
216
|
+
const snapshot = getGitTreeSnapshot({ cwd });
|
|
217
|
+
if (snapshot === null) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
hash: snapshot.hash,
|
|
222
|
+
repositoryRoot,
|
|
223
|
+
entries: snapshot.entries.map((entry) => ({
|
|
224
|
+
absolutePath: safePath.resolve(repositoryRoot, entry.path),
|
|
225
|
+
oid: entry.oid,
|
|
226
|
+
mode: entry.mode,
|
|
227
|
+
isSymlink: entry.mode === GIT_MODE_SYMLINK,
|
|
228
|
+
isSubmodule: entry.mode === GIT_MODE_GITLINK,
|
|
229
|
+
})),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
//# sourceMappingURL=git-snapshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-snapshot.js","sourceRoot":"","sources":["../src/git-snapshot.ts"],"names":[],"mappings":"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"}
|