@cmflow/source-pack 0.0.0-stage → 3.19.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 +183 -2
- package/dist/GitRepository-VTAWT6j4.mjs +151 -0
- package/dist/GitRepository-VTAWT6j4.mjs.map +1 -0
- package/dist/bin/source-pack.d.mts +1 -0
- package/dist/bin/source-pack.mjs +3038 -0
- package/dist/bin/source-pack.mjs.map +1 -0
- package/dist/buildSourcePack-CaaMZ_tw.mjs +11280 -0
- package/dist/buildSourcePack-CaaMZ_tw.mjs.map +1 -0
- package/dist/buildSourcePack-DIp8G_uL.mjs +3 -0
- package/dist/constants-DkiLCuIK.mjs +25 -0
- package/dist/constants-DkiLCuIK.mjs.map +1 -0
- package/dist/defaults-BJs7a37e.mjs +52 -0
- package/dist/defaults-BJs7a37e.mjs.map +1 -0
- package/dist/index.d.mts +397 -0
- package/dist/index.mjs +73 -0
- package/dist/index.mjs.map +1 -0
- package/dist/initConfig-ByGsbShR.mjs +185 -0
- package/dist/initConfig-ByGsbShR.mjs.map +1 -0
- package/dist/loadConfig-CNrL90dq.mjs +224 -0
- package/dist/loadConfig-CNrL90dq.mjs.map +1 -0
- package/dist/logger-D1XGzzjT.mjs +15 -0
- package/dist/logger-D1XGzzjT.mjs.map +1 -0
- package/dist/packageVersion-B0749hF6.mjs +4646 -0
- package/dist/packageVersion-B0749hF6.mjs.map +1 -0
- package/dist/projectIdentity-F97vs1H-.mjs +3331 -0
- package/dist/projectIdentity-F97vs1H-.mjs.map +1 -0
- package/dist/publishSourcePack-Dj1cdWfT.mjs +3 -0
- package/dist/publishSourcePack-xNr9RBgv.mjs +296 -0
- package/dist/publishSourcePack-xNr9RBgv.mjs.map +1 -0
- package/dist/token-DMtao22Y.mjs +59 -0
- package/dist/token-DMtao22Y.mjs.map +1 -0
- package/dist/token-util-C3JYrW4-.mjs +498 -0
- package/dist/token-util-C3JYrW4-.mjs.map +1 -0
- package/dist/token-util-CbjlbIno.mjs +5 -0
- package/package.json +44 -3
- package/templates/circleci-job.yml +39 -0
- package/templates/gitlab-ci.yml +30 -0
package/README.md
CHANGED
|
@@ -1,3 +1,184 @@
|
|
|
1
|
-
|
|
1
|
+
<div style="text-align: center" align="center">
|
|
2
|
+
<img src="https://ns.clubmed.com/fbs/RWD/branding2023/Logo/MicrosoftTeams-image%20(9).png" width="200" alt="Club Med"/>
|
|
3
|
+
</div>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<div align="center">
|
|
6
|
+
<h1>@cmflow/source-pack</h1>
|
|
7
|
+
<hr />
|
|
8
|
+
|
|
9
|
+
[](https://badge.fury.io/js/%40cmflow%2Fsource-pack)
|
|
10
|
+
[](https://oxc.rs/docs/guide/usage/formatter)
|
|
11
|
+
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
`source-pack` builds, at each production release of a project hosted on GitHub or GitLab, a **versioned index of its code**
|
|
15
|
+
(chunks + embeddings + optional attachments + manifest) and publishes it on the release of the tag. The Dataviz loads it with
|
|
16
|
+
`readSourcePack()` to answer `source_search` on any production version, without a local checkout.
|
|
17
|
+
|
|
18
|
+
The package is generic: everything that depends on a project lives in its `source-pack.config.ts`.
|
|
19
|
+
|
|
20
|
+
## Commands
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx @cmflow/source-pack init # create ./source-pack.config.ts
|
|
24
|
+
npx @cmflow/source-pack build # index HEAD, version from package.json
|
|
25
|
+
npx @cmflow/source-pack build --tag version-1.2.3 # index the tag, version extracted from it
|
|
26
|
+
npx @cmflow/source-pack publish # upload the last build on the release of its tag
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Global option: `-c, --config <path>` (default `./source-pack.config.ts`). Paths of the config are resolved against the directory
|
|
30
|
+
the command is run from. The config may be a `.ts` file (loaded with `tsx`) or a `.mjs` file. It can import `defineConfig` from
|
|
31
|
+
`@cmflow/source-pack` even when the package is not installed in the project (`npx` usage): the import resolves to the running CLI.
|
|
32
|
+
|
|
33
|
+
| Command | Options |
|
|
34
|
+
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
35
|
+
| `init` | `--force` overwrites an existing file. `repository` is deduced from the `origin` remote; exclusions are pre-filled for the detected project type (node, python, jvm, go). |
|
|
36
|
+
| `build` | `--ref <commit-ish>` (default: `--tag`, otherwise `HEAD`), `--tag <tag>`, `--release-version <v>` (default: from `--tag` with `publish.tag`, otherwise `version` of `package.json` at the ref), `--skip-embeddings`, `--dry-run` (no LiteLLM call, nothing written, sizes estimated). |
|
|
37
|
+
| `publish` | `--archive <path>` (default: last build of `outputDir`), `--dry-run` (logs the planned upload, no HTTP call). |
|
|
38
|
+
|
|
39
|
+
Secrets are read from the environment only, never from the config, and are never logged nor written to the pack:
|
|
40
|
+
|
|
41
|
+
| Variable | Used by |
|
|
42
|
+
| ----------------- | ------------------------------------------------------------------------- |
|
|
43
|
+
| `LITELLM_API_KEY` | `build` (embeddings; the name can be changed with `embedding.apiKeyEnv`) |
|
|
44
|
+
| `GITHUB_TOKEN` | `publish` on a GitHub repository (`contents: write`) |
|
|
45
|
+
| `GITLAB_TOKEN` | `publish` on a GitLab project (`api` scope; falls back to `CI_JOB_TOKEN`) |
|
|
46
|
+
|
|
47
|
+
## Configuration
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { defineConfig } from "@cmflow/source-pack";
|
|
51
|
+
|
|
52
|
+
export default defineConfig({
|
|
53
|
+
outputDir: "./tmp/source-pack",
|
|
54
|
+
repository: { provider: "github", owner: "ClubMediterranee", name: "digital-api" },
|
|
55
|
+
// GitLab: { provider: "gitlab", projectId: "group/project" (or 1234), apiUrl: "https://gitlab.example.com/api/v4" }
|
|
56
|
+
sources: {
|
|
57
|
+
include: ["**/*.{ts,tsx,js,json,yaml,yml,md,feature}"],
|
|
58
|
+
exclude: ["mock/**", "test/functionnal/**", "package-lock.json", "coverage/**", "dist/**"],
|
|
59
|
+
maxFileBytes: 1048576
|
|
60
|
+
},
|
|
61
|
+
chunking: { lines: 100, overlap: 15 },
|
|
62
|
+
embedding: {
|
|
63
|
+
baseUrl: "https://apim-prdaiplatform-frc.azure-api.net/litellm-api",
|
|
64
|
+
model: "text-embedding-3-small-gmdt",
|
|
65
|
+
fallbackModels: ["text-embedding-3-small-swc-gmdt", "text-embedding-3-small-eus2-gmdt"],
|
|
66
|
+
referenceModel: "text-embedding-3-small",
|
|
67
|
+
dimensions: 1536,
|
|
68
|
+
batchSize: 64,
|
|
69
|
+
concurrency: 4
|
|
70
|
+
},
|
|
71
|
+
attachments: ["tools/build/cache-layers-adjacency-list-reference.json"],
|
|
72
|
+
publish: {
|
|
73
|
+
tag: "version-{version}",
|
|
74
|
+
assetName: "source-pack-{version}.tar.gz"
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
- Only files **tracked by Git at the indexed commit** are read (from the Git objects, not the working tree: the result does not depend
|
|
80
|
+
on the checkout, line endings or the OS). Binary files (NUL byte in the first 8000 bytes), symlinks, submodules and files above
|
|
81
|
+
`maxFileBytes` are skipped. Test files (`*.spec.*`, `*.test.*` and their Python, Go and JVM equivalents) are excluded by
|
|
82
|
+
default: they double the index size without describing production behavior.
|
|
83
|
+
- Globs use minimatch with dot files. Defaults when `sources` is omitted: common text extensions, excluding tests, `node_modules`, `dist`,
|
|
84
|
+
`build`, `coverage` and lockfiles.
|
|
85
|
+
- Chunks are windows of `lines` lines advancing by `lines - overlap`; blank windows are skipped;
|
|
86
|
+
`id = sha256("<path>:<startLine>:<text>")` — identical to the state-control indexer.
|
|
87
|
+
- Embeddings use the OpenAI-compatible `/v1/embeddings` endpoint of LiteLLM (`/v1` is appended to `baseUrl`), in batches of
|
|
88
|
+
`batchSize`, `concurrency` (4) batches at a time, each retried `maxRetries` (3) times before switching to the next `fallbackModels` alias. The embedded text of a chunk is
|
|
89
|
+
`"<path>\n<text>"` truncated to `maxInputChars` (12000). The manifest records the **reference model** (`referenceModel`, default:
|
|
90
|
+
`model`): set it to the public model behind the gateway aliases so the manifest never names the regional alias that answered.
|
|
91
|
+
- Omit `embedding` (or use `--skip-embeddings`) for a lexical-only pack.
|
|
92
|
+
- `attachments` are copied verbatim (they do not need to match `sources`).
|
|
93
|
+
- `publish.createRelease` (default `false`) creates the release when the tag has none; by default the release tool owns releases and a
|
|
94
|
+
missing release is an error.
|
|
95
|
+
|
|
96
|
+
An invalid config is rejected with one line per field, e.g.:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
Invalid source-pack config (/repo/source-pack.config.ts):
|
|
100
|
+
- repository.provider: expected "github" or "gitlab" (received "bitbucket")
|
|
101
|
+
- chunking.overlap: expected a value lower than chunking.lines
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Pack format (`source-pack-<version>.tar.gz`)
|
|
105
|
+
|
|
106
|
+
| Entry | Content |
|
|
107
|
+
| -------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
108
|
+
| `manifest.json` | Project, version, tag, ref, SHA, dates, chunking version + config fingerprint, embedding model, counts, attachments |
|
|
109
|
+
| `chunks.jsonl.gz` | One JSON object per line: `{ id, path, startLine, endLine, text }` (lines are 1-based and inclusive) |
|
|
110
|
+
| `embeddings.f32` | `chunks.count × dimensions` little-endian float32, in chunk order (absent without embeddings) |
|
|
111
|
+
| `attachments/<path>` | Files declared in `attachments` |
|
|
112
|
+
|
|
113
|
+
`manifest.project.id` (`github:<owner>/<repo>` or `gitlab:<project>`), `sha`, `config.fingerprint` (sha256 of `sources` +
|
|
114
|
+
`chunking` + chunking format version) and `embedding.model`/`dimensions` uniquely identify the pack content.
|
|
115
|
+
|
|
116
|
+
### Publishing
|
|
117
|
+
|
|
118
|
+
The destination follows `repository.provider`:
|
|
119
|
+
|
|
120
|
+
- GitHub: asset of the GitHub release of the tag; an asset with the same name is deleted first (idempotent).
|
|
121
|
+
- GitLab: `PUT /projects/:id/packages/generic/<packageName>/<version>/<assetName>` (Generic Package Registry, `packageName`
|
|
122
|
+
defaults to `source-pack`), older files with the same name in that package version are deleted, then a `package` link with the same
|
|
123
|
+
name is created or updated on the GitLab release of the tag.
|
|
124
|
+
|
|
125
|
+
## Reading a pack (Dataviz)
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import { embeddingInput, readSourcePack } from "@cmflow/source-pack";
|
|
129
|
+
|
|
130
|
+
const pack = await readSourcePack("./source-pack-0.3503.0.tar.gz"); // or the downloaded bytes (Uint8Array)
|
|
131
|
+
|
|
132
|
+
pack.manifest.sha;
|
|
133
|
+
pack.chunks[0]; // { id, path, startLine, endLine, text }
|
|
134
|
+
pack.getEmbedding(0); // Float32Array(1536) | null
|
|
135
|
+
pack.attachments.get("tools/build/cache-layers-adjacency-list-reference.json");
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`readSourcePack` checks the format version, the chunk count and the embeddings size. Embed queries with the same
|
|
139
|
+
`manifest.embedding.model`/`dimensions`; use `embeddingInput()` to re-embed chunks consistently.
|
|
140
|
+
|
|
141
|
+
## Onboarding a project
|
|
142
|
+
|
|
143
|
+
1. In the project root: `npx @cmflow/source-pack init`, then adjust `sources.exclude` (mocks, fixtures, generated data, vendored
|
|
144
|
+
code…) and `attachments`.
|
|
145
|
+
2. Check the result locally: `npx @cmflow/source-pack build --dry-run` (chunk count and estimated size), then
|
|
146
|
+
`LITELLM_API_KEY=… npx @cmflow/source-pack build` and `npx @cmflow/source-pack publish --dry-run`.
|
|
147
|
+
3. Commit `source-pack.config.ts` and add `tmp/source-pack` to `.gitignore`.
|
|
148
|
+
4. Add the job to the **production merge pipeline only**, after the job that creates the tag and its release (snippets below). It must
|
|
149
|
+
not be required by any other job so that a failure never blocks the release; rerunning it republishes the pack.
|
|
150
|
+
5. Provide the secrets: `LITELLM_API_KEY` and `GITHUB_TOKEN` (CircleCI shared context `source-pack`) or `LITELLM_API_KEY` and
|
|
151
|
+
`GITLAB_TOKEN` (masked GitLab CI/CD variables).
|
|
152
|
+
6. Enable the project in the « Source pack » tab of the Dataviz.
|
|
153
|
+
|
|
154
|
+
### CircleCI
|
|
155
|
+
|
|
156
|
+
Copy [`templates/circleci-job.yml`](./templates/circleci-job.yml) under `jobs:` and add to the production workflow:
|
|
157
|
+
|
|
158
|
+
```yaml
|
|
159
|
+
- source-pack:
|
|
160
|
+
requires: [cm-release-prod]
|
|
161
|
+
filters:
|
|
162
|
+
branches:
|
|
163
|
+
only: /^production$/
|
|
164
|
+
context: source-pack
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
The job resolves the first `version-*` tag containing the pipeline commit (the release commit created by `cmrelease`), then runs
|
|
168
|
+
`build --tag "$TAG"` and `publish`.
|
|
169
|
+
|
|
170
|
+
### GitLab CI
|
|
171
|
+
|
|
172
|
+
```yaml
|
|
173
|
+
include:
|
|
174
|
+
- remote: "https://raw.githubusercontent.com/ClubMediterranee/cmflow/master/packages/source-pack/templates/gitlab-ci.yml"
|
|
175
|
+
|
|
176
|
+
source-pack:
|
|
177
|
+
extends: .source-pack
|
|
178
|
+
needs: ["release"]
|
|
179
|
+
rules:
|
|
180
|
+
- if: $CI_COMMIT_BRANCH == "production"
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The `.source-pack` template ([`templates/gitlab-ci.yml`](./templates/gitlab-ci.yml)) is `allow_failure: true` and accepts
|
|
184
|
+
`SOURCE_PACK_VERSION` and `SOURCE_PACK_TAG_PATTERN` variables.
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
|
|
4
|
+
//#region src/git/parseLsTree.ts
|
|
5
|
+
/** Parses `git ls-tree -r -z --long` output, keeping regular files only (no symlinks, no submodules). */
|
|
6
|
+
function parseLsTree(output) {
|
|
7
|
+
return output.split("\0").filter(Boolean).flatMap((record) => {
|
|
8
|
+
const tab = record.indexOf(" ");
|
|
9
|
+
const [mode, type, object, size] = record.slice(0, tab).trim().split(/\s+/);
|
|
10
|
+
if (type !== "blob" || !(mode === "100644" || mode === "100755")) return [];
|
|
11
|
+
return [{
|
|
12
|
+
mode,
|
|
13
|
+
object,
|
|
14
|
+
size: Number(size),
|
|
15
|
+
path: record.slice(tab + 1)
|
|
16
|
+
}];
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/git/GitRepository.ts
|
|
22
|
+
const execFileAsync = promisify(execFile);
|
|
23
|
+
/**
|
|
24
|
+
* Read-only access to the Git objects of a repository: everything is read at a commit, never from the working tree,
|
|
25
|
+
* so the result does not depend on the checkout state, line-ending conversion or the OS.
|
|
26
|
+
*/
|
|
27
|
+
var GitRepository = class GitRepository {
|
|
28
|
+
#trees = /* @__PURE__ */ new Map();
|
|
29
|
+
constructor(root) {
|
|
30
|
+
this.root = root;
|
|
31
|
+
}
|
|
32
|
+
static async open(cwd) {
|
|
33
|
+
try {
|
|
34
|
+
const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"], { cwd });
|
|
35
|
+
return new GitRepository(stdout.trim());
|
|
36
|
+
} catch {
|
|
37
|
+
throw new Error(`${cwd} is not inside a Git repository`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async git(args) {
|
|
41
|
+
const { stdout } = await execFileAsync("git", args, {
|
|
42
|
+
cwd: this.root,
|
|
43
|
+
maxBuffer: 536870912
|
|
44
|
+
});
|
|
45
|
+
return stdout;
|
|
46
|
+
}
|
|
47
|
+
async resolveCommit(ref) {
|
|
48
|
+
try {
|
|
49
|
+
return (await this.git([
|
|
50
|
+
"rev-parse",
|
|
51
|
+
"--verify",
|
|
52
|
+
"--end-of-options",
|
|
53
|
+
`${ref}^{commit}`
|
|
54
|
+
])).trim();
|
|
55
|
+
} catch {
|
|
56
|
+
throw new Error(`Git ref "${ref}" not found (fetch the tag or branch first)`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async commitDate(sha) {
|
|
60
|
+
return new Date((await this.git([
|
|
61
|
+
"show",
|
|
62
|
+
"-s",
|
|
63
|
+
"--format=%cI",
|
|
64
|
+
sha
|
|
65
|
+
])).trim()).toISOString();
|
|
66
|
+
}
|
|
67
|
+
/** Regular files tracked at `sha` (cached per commit). */
|
|
68
|
+
listFiles(sha) {
|
|
69
|
+
let tree = this.#trees.get(sha);
|
|
70
|
+
if (!tree) {
|
|
71
|
+
tree = this.git([
|
|
72
|
+
"ls-tree",
|
|
73
|
+
"-r",
|
|
74
|
+
"-z",
|
|
75
|
+
"--long",
|
|
76
|
+
"--full-tree",
|
|
77
|
+
sha
|
|
78
|
+
]).then(parseLsTree);
|
|
79
|
+
this.#trees.set(sha, tree);
|
|
80
|
+
}
|
|
81
|
+
return tree;
|
|
82
|
+
}
|
|
83
|
+
async remoteUrl(remote = "origin") {
|
|
84
|
+
try {
|
|
85
|
+
return (await this.git([
|
|
86
|
+
"remote",
|
|
87
|
+
"get-url",
|
|
88
|
+
remote
|
|
89
|
+
])).trim() || void 0;
|
|
90
|
+
} catch {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** Reads `path` at `sha`; undefined when the file is not tracked at that commit. */
|
|
95
|
+
async readFile(sha, path) {
|
|
96
|
+
const entry = (await this.listFiles(sha)).find((file) => file.path === path);
|
|
97
|
+
if (!entry) return;
|
|
98
|
+
return (await this.readBlobs([entry.object])).get(entry.object);
|
|
99
|
+
}
|
|
100
|
+
/** Reads many blobs through a single `git cat-file --batch` process. */
|
|
101
|
+
readBlobs(objects) {
|
|
102
|
+
const unique = [...new Set(objects)];
|
|
103
|
+
const blobs = /* @__PURE__ */ new Map();
|
|
104
|
+
if (!unique.length) return Promise.resolve(blobs);
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
const child = spawn("git", ["cat-file", "--batch"], {
|
|
107
|
+
cwd: this.root,
|
|
108
|
+
stdio: [
|
|
109
|
+
"pipe",
|
|
110
|
+
"pipe",
|
|
111
|
+
"pipe"
|
|
112
|
+
]
|
|
113
|
+
});
|
|
114
|
+
let buffer = Buffer.alloc(0);
|
|
115
|
+
let stderr = "";
|
|
116
|
+
child.stdout.on("data", (data) => {
|
|
117
|
+
buffer = buffer.length ? Buffer.concat([buffer, data]) : data;
|
|
118
|
+
for (;;) {
|
|
119
|
+
const newline = buffer.indexOf(10);
|
|
120
|
+
if (newline < 0) return;
|
|
121
|
+
const [object, type, size] = buffer.subarray(0, newline).toString("utf8").split(" ");
|
|
122
|
+
if (type === "missing" || size === void 0) {
|
|
123
|
+
child.kill();
|
|
124
|
+
reject(/* @__PURE__ */ new Error(`Git object ${object} is missing`));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const length = Number(size);
|
|
128
|
+
if (buffer.length < newline + 1 + length + 1) return;
|
|
129
|
+
blobs.set(object, Buffer.from(buffer.subarray(newline + 1, newline + 1 + length)));
|
|
130
|
+
buffer = buffer.subarray(newline + 1 + length + 1);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
child.stderr.on("data", (data) => {
|
|
134
|
+
stderr += data.toString("utf8");
|
|
135
|
+
});
|
|
136
|
+
child.on("error", reject);
|
|
137
|
+
child.on("close", (code) => {
|
|
138
|
+
if (code !== 0 && blobs.size !== unique.length) {
|
|
139
|
+
reject(/* @__PURE__ */ new Error(`git cat-file failed (${code}): ${stderr.trim()}`));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
resolve(blobs);
|
|
143
|
+
});
|
|
144
|
+
child.stdin.end(`${unique.join("\n")}\n`);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
//#endregion
|
|
150
|
+
export { GitRepository as t };
|
|
151
|
+
//# sourceMappingURL=GitRepository-VTAWT6j4.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GitRepository-VTAWT6j4.mjs","names":["#trees"],"sources":["../src/git/parseLsTree.ts","../src/git/GitRepository.ts"],"sourcesContent":["import type { GitTreeEntry } from \"./interfaces\";\n\n/** Parses `git ls-tree -r -z --long` output, keeping regular files only (no symlinks, no submodules). */\nexport function parseLsTree(output: string): GitTreeEntry[] {\n return output\n .split(\"\\0\")\n .filter(Boolean)\n .flatMap((record) => {\n const tab = record.indexOf(\"\\t\");\n const [mode, type, object, size] = record.slice(0, tab).trim().split(/\\s+/) as [string, string, string, string];\n\n if (type !== \"blob\" || !(mode === \"100644\" || mode === \"100755\")) {\n return [];\n }\n\n return [{ mode, object, size: Number(size), path: record.slice(tab + 1) }];\n });\n}\n","import { execFile, spawn } from \"node:child_process\";\nimport { promisify } from \"node:util\";\nimport type { GitTreeEntry } from \"./interfaces\";\nimport { parseLsTree } from \"./parseLsTree\";\n\nconst execFileAsync = promisify(execFile);\n\n/**\n * Read-only access to the Git objects of a repository: everything is read at a commit, never from the working tree,\n * so the result does not depend on the checkout state, line-ending conversion or the OS.\n */\nexport class GitRepository {\n readonly #trees = new Map<string, Promise<GitTreeEntry[]>>();\n\n private constructor(readonly root: string) {}\n\n static async open(cwd: string): Promise<GitRepository> {\n try {\n const { stdout } = await execFileAsync(\"git\", [\"rev-parse\", \"--show-toplevel\"], { cwd });\n\n return new GitRepository(stdout.trim());\n } catch {\n throw new Error(`${cwd} is not inside a Git repository`);\n }\n }\n\n async git(args: string[]): Promise<string> {\n const { stdout } = await execFileAsync(\"git\", args, { cwd: this.root, maxBuffer: 512 * 1024 * 1024 });\n\n return stdout;\n }\n\n async resolveCommit(ref: string): Promise<string> {\n try {\n return (await this.git([\"rev-parse\", \"--verify\", \"--end-of-options\", `${ref}^{commit}`])).trim();\n } catch {\n throw new Error(`Git ref \"${ref}\" not found (fetch the tag or branch first)`);\n }\n }\n\n async commitDate(sha: string): Promise<string> {\n return new Date((await this.git([\"show\", \"-s\", \"--format=%cI\", sha])).trim()).toISOString();\n }\n\n /** Regular files tracked at `sha` (cached per commit). */\n listFiles(sha: string): Promise<GitTreeEntry[]> {\n let tree = this.#trees.get(sha);\n\n if (!tree) {\n tree = this.git([\"ls-tree\", \"-r\", \"-z\", \"--long\", \"--full-tree\", sha]).then(parseLsTree);\n this.#trees.set(sha, tree);\n }\n\n return tree;\n }\n\n async remoteUrl(remote = \"origin\"): Promise<string | undefined> {\n try {\n return (await this.git([\"remote\", \"get-url\", remote])).trim() || undefined;\n } catch {\n return undefined;\n }\n }\n\n /** Reads `path` at `sha`; undefined when the file is not tracked at that commit. */\n async readFile(sha: string, path: string): Promise<Buffer | undefined> {\n const entry = (await this.listFiles(sha)).find((file) => file.path === path);\n\n if (!entry) {\n return undefined;\n }\n\n return (await this.readBlobs([entry.object])).get(entry.object);\n }\n\n /** Reads many blobs through a single `git cat-file --batch` process. */\n readBlobs(objects: string[]): Promise<Map<string, Buffer>> {\n const unique = [...new Set(objects)];\n const blobs = new Map<string, Buffer>();\n\n if (!unique.length) {\n return Promise.resolve(blobs);\n }\n\n return new Promise((resolve, reject) => {\n const child = spawn(\"git\", [\"cat-file\", \"--batch\"], { cwd: this.root, stdio: [\"pipe\", \"pipe\", \"pipe\"] });\n let buffer: Buffer = Buffer.alloc(0);\n let stderr = \"\";\n\n child.stdout.on(\"data\", (data: Buffer) => {\n buffer = buffer.length ? Buffer.concat([buffer, data]) : data;\n\n for (;;) {\n const newline = buffer.indexOf(10);\n\n if (newline < 0) {\n return;\n }\n\n const header = buffer.subarray(0, newline).toString(\"utf8\");\n const [object, type, size] = header.split(\" \");\n\n if (type === \"missing\" || size === undefined) {\n child.kill();\n reject(new Error(`Git object ${object} is missing`));\n return;\n }\n\n const length = Number(size);\n\n if (buffer.length < newline + 1 + length + 1) {\n return;\n }\n\n blobs.set(object!, Buffer.from(buffer.subarray(newline + 1, newline + 1 + length)));\n buffer = buffer.subarray(newline + 1 + length + 1);\n }\n });\n child.stderr.on(\"data\", (data: Buffer) => {\n stderr += data.toString(\"utf8\");\n });\n child.on(\"error\", reject);\n child.on(\"close\", (code) => {\n if (code !== 0 && blobs.size !== unique.length) {\n reject(new Error(`git cat-file failed (${code}): ${stderr.trim()}`));\n return;\n }\n\n resolve(blobs);\n });\n\n child.stdin.end(`${unique.join(\"\\n\")}\\n`);\n });\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|