@omgbase/git-adapter 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +38 -2
- package/dist/src/config.d.ts +62 -0
- package/dist/src/config.js +91 -0
- package/dist/src/cursor.d.ts +24 -0
- package/dist/src/cursor.js +47 -0
- package/dist/src/exclude.d.ts +5 -0
- package/dist/src/exclude.js +41 -0
- package/dist/src/git.d.ts +157 -0
- package/dist/src/git.js +356 -0
- package/dist/src/ids.d.ts +48 -0
- package/dist/src/ids.js +84 -0
- package/dist/src/index.d.ts +11 -0
- package/dist/src/index.js +17 -0
- package/dist/src/provider.d.ts +63 -0
- package/dist/src/provider.js +113 -0
- package/dist/src/reader.d.ts +44 -0
- package/dist/src/reader.js +190 -0
- package/dist/src/remote.d.ts +113 -0
- package/dist/src/remote.js +631 -0
- package/dist/src/scheduler.d.ts +20 -0
- package/dist/src/scheduler.js +37 -0
- package/dist/src/trailers.d.ts +41 -0
- package/dist/src/trailers.js +124 -0
- package/dist/src/worker.d.ts +50 -0
- package/dist/src/worker.js +202 -0
- package/package.json +51 -3
package/dist/src/git.js
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
export class GitError extends Error {
|
|
5
|
+
args;
|
|
6
|
+
status;
|
|
7
|
+
stderr;
|
|
8
|
+
constructor(args, status, stderr) {
|
|
9
|
+
super(`git ${args.join(" ")} exited ${status}: ${stderr.trim()}`);
|
|
10
|
+
this.args = args;
|
|
11
|
+
this.status = status;
|
|
12
|
+
this.stderr = stderr;
|
|
13
|
+
this.name = "GitError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** The git executable: `git` on PATH, or what `OMGBASE_GIT_BIN` names (a host pinning one git; the tests pointing at a missing one). The Rust twin honours the same variable. */
|
|
17
|
+
export const GIT_BIN_ENV = "OMGBASE_GIT_BIN";
|
|
18
|
+
export function gitBin() {
|
|
19
|
+
const v = process.env[GIT_BIN_ENV];
|
|
20
|
+
return v !== undefined && v.trim() !== "" ? v.trim() : "git";
|
|
21
|
+
}
|
|
22
|
+
export const ZERO_SHA = "0".repeat(40);
|
|
23
|
+
/** The empty tree's sha (git's well-known constant for the SHA-1 object format). */
|
|
24
|
+
export const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
25
|
+
export const SHA_RE = /^[0-9a-f]{40}$/;
|
|
26
|
+
/** `ts` (RFC 3339) as git's raw date `@<seconds> +0000`. */
|
|
27
|
+
export function gitDate(ts) {
|
|
28
|
+
const ms = Date.parse(ts);
|
|
29
|
+
if (Number.isNaN(ms))
|
|
30
|
+
throw new Error(`not an RFC 3339 instant: ${ts}`);
|
|
31
|
+
return `@${Math.floor(ms / 1000)} +0000`;
|
|
32
|
+
}
|
|
33
|
+
/** Is `git` on PATH? The version line, or the reason it is not. Probed once per process. */
|
|
34
|
+
let probed = null;
|
|
35
|
+
export function gitAvailable() {
|
|
36
|
+
if (probed)
|
|
37
|
+
return probed;
|
|
38
|
+
const r = spawnSync(gitBin(), ["--version"], { encoding: "utf8", env: baseEnv(null) });
|
|
39
|
+
if (r.error)
|
|
40
|
+
probed = { ok: false, reason: `${gitBin()} is not on PATH (${r.error.code ?? r.error.message})` };
|
|
41
|
+
else if (r.status !== 0)
|
|
42
|
+
probed = { ok: false, reason: `git --version exited ${r.status}: ${(r.stderr ?? "").trim()}` };
|
|
43
|
+
else
|
|
44
|
+
probed = { ok: true, detail: (r.stdout ?? "").trim() };
|
|
45
|
+
return probed;
|
|
46
|
+
}
|
|
47
|
+
/** Tests: forget the probe's answer. */
|
|
48
|
+
export function resetGitProbe() {
|
|
49
|
+
probed = null;
|
|
50
|
+
}
|
|
51
|
+
function baseEnv(gitDir) {
|
|
52
|
+
const env = {
|
|
53
|
+
PATH: process.env.PATH,
|
|
54
|
+
GIT_CONFIG_NOSYSTEM: "1",
|
|
55
|
+
GIT_CONFIG_GLOBAL: "/dev/null",
|
|
56
|
+
GIT_ATTR_NOSYSTEM: "1",
|
|
57
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
58
|
+
LC_ALL: "C",
|
|
59
|
+
TZ: "UTC",
|
|
60
|
+
};
|
|
61
|
+
if (gitDir !== null)
|
|
62
|
+
env.GIT_DIR = gitDir;
|
|
63
|
+
return env;
|
|
64
|
+
}
|
|
65
|
+
/** A bare repository at `gitDir`, driven through the plumbing. */
|
|
66
|
+
export class GitRepo {
|
|
67
|
+
gitDir;
|
|
68
|
+
constructor(gitDir) {
|
|
69
|
+
this.gitDir = gitDir;
|
|
70
|
+
}
|
|
71
|
+
/** Does the bare repository exist (its `HEAD` file)? */
|
|
72
|
+
exists() {
|
|
73
|
+
return existsSync(join(this.gitDir, "HEAD"));
|
|
74
|
+
}
|
|
75
|
+
/** `git init --bare -b <branch>` (idempotent: an existing repository is left as it is). */
|
|
76
|
+
init(branch) {
|
|
77
|
+
if (this.exists())
|
|
78
|
+
return;
|
|
79
|
+
mkdirSync(this.gitDir, { recursive: true });
|
|
80
|
+
// `--initial-branch` so no `init.defaultBranch` setting (which we never read) decides the name.
|
|
81
|
+
this.run(["init", "--quiet", "--bare", "--initial-branch", branch, this.gitDir], { noGitDir: true });
|
|
82
|
+
}
|
|
83
|
+
/** Delete the bare repository. */
|
|
84
|
+
remove() {
|
|
85
|
+
if (!existsSync(this.gitDir))
|
|
86
|
+
return false;
|
|
87
|
+
rmSync(this.gitDir, { recursive: true, force: true });
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
run(args, opts = {}) {
|
|
91
|
+
const env = { ...baseEnv(opts.noGitDir ? null : this.gitDir), ...(opts.env ?? {}) };
|
|
92
|
+
const r = spawnSync(gitBin(), args, { env, input: opts.input, maxBuffer: 256 << 20 });
|
|
93
|
+
if (r.error)
|
|
94
|
+
throw r.error;
|
|
95
|
+
const status = r.status ?? -1;
|
|
96
|
+
const stderr = r.stderr ? r.stderr.toString("utf8") : "";
|
|
97
|
+
if (status !== 0 && !opts.allowFailure)
|
|
98
|
+
throw new GitError(args, status, stderr);
|
|
99
|
+
return { status, stdout: r.stdout ?? Buffer.alloc(0), stderr };
|
|
100
|
+
}
|
|
101
|
+
text(args, opts = {}) {
|
|
102
|
+
return this.run(args, opts).stdout.toString("utf8");
|
|
103
|
+
}
|
|
104
|
+
/** The branch tip, or null when the ref does not exist. */
|
|
105
|
+
tip(branch) {
|
|
106
|
+
const r = this.run(["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`], { allowFailure: true });
|
|
107
|
+
if (r.status !== 0)
|
|
108
|
+
return null;
|
|
109
|
+
const sha = r.stdout.toString("utf8").trim();
|
|
110
|
+
return SHA_RE.test(sha) ? sha : null;
|
|
111
|
+
}
|
|
112
|
+
/** `git hash-object -w --stdin`: the blob's sha. */
|
|
113
|
+
writeBlob(bytes) {
|
|
114
|
+
return this.text(["hash-object", "-w", "--stdin"], { input: bytes }).trim();
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Build a tree: the parent commit's tree (or empty) read into a temporary
|
|
118
|
+
* index, `update-index --index-info` with the adds (`100644 blob <sha>\t<path>`)
|
|
119
|
+
* and removes (`0 <zeros>\t<path>`), `write-tree`. Returns the tree sha. The
|
|
120
|
+
* index file lives inside the bare repository and is removed afterwards.
|
|
121
|
+
*/
|
|
122
|
+
buildTree(baseTree, adds, removes) {
|
|
123
|
+
const index = join(this.gitDir, `omgbase-index-${process.pid}.tmp`);
|
|
124
|
+
const env = { GIT_INDEX_FILE: index };
|
|
125
|
+
try {
|
|
126
|
+
if (baseTree === null)
|
|
127
|
+
this.run(["read-tree", "--empty"], { env });
|
|
128
|
+
else
|
|
129
|
+
this.run(["read-tree", baseTree], { env });
|
|
130
|
+
const lines = [];
|
|
131
|
+
for (const p of removes)
|
|
132
|
+
lines.push(`0 ${ZERO_SHA}\t${p}`);
|
|
133
|
+
for (const a of adds)
|
|
134
|
+
lines.push(`100644 blob ${a.blob}\t${a.path}`);
|
|
135
|
+
if (lines.length > 0)
|
|
136
|
+
this.run(["update-index", "--index-info"], { env, input: lines.join("\n") + "\n" });
|
|
137
|
+
return this.text(["write-tree"], { env }).trim();
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
rmSync(index, { force: true });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/** The tree a commit points at. */
|
|
144
|
+
treeOf(commit) {
|
|
145
|
+
return this.text(["rev-parse", `${commit}^{tree}`]).trim();
|
|
146
|
+
}
|
|
147
|
+
/** `git commit-tree` with the message on stdin and the identities/dates in the environment; the commit sha. `parent` may be several (a merge commit, 17.6). */
|
|
148
|
+
commitTree(tree, parent, message, id) {
|
|
149
|
+
const date = gitDate(id.ts);
|
|
150
|
+
const env = {
|
|
151
|
+
GIT_AUTHOR_NAME: id.author.name, GIT_AUTHOR_EMAIL: id.author.email, GIT_AUTHOR_DATE: date,
|
|
152
|
+
GIT_COMMITTER_NAME: id.committer.name, GIT_COMMITTER_EMAIL: id.committer.email, GIT_COMMITTER_DATE: date,
|
|
153
|
+
};
|
|
154
|
+
const parents = parent === null ? [] : Array.isArray(parent) ? parent : [parent];
|
|
155
|
+
const args = ["commit-tree", tree, ...parents.flatMap((p) => ["-p", p]), "-F", "-"];
|
|
156
|
+
return this.text(args, { env, input: message }).trim();
|
|
157
|
+
}
|
|
158
|
+
/** `git update-ref` with the old value as the compare-and-swap (`ZERO_SHA` = the ref must not exist). */
|
|
159
|
+
updateRef(branch, newSha, oldSha) {
|
|
160
|
+
this.updateRefRaw(`refs/heads/${branch}`, newSha, oldSha);
|
|
161
|
+
}
|
|
162
|
+
// ---- refs, ancestry and trees (17.6, the remote half) ----
|
|
163
|
+
/** `git update-ref <ref> <new> <old>` on any full ref name (the ids ref, the remote-tracking and bookkeeping refs). */
|
|
164
|
+
updateRefRaw(ref, newSha, oldSha) {
|
|
165
|
+
this.run(["update-ref", ref, newSha, oldSha ?? ZERO_SHA]);
|
|
166
|
+
}
|
|
167
|
+
/** `git update-ref <ref> <new>` without a compare-and-swap (a tracking or bookkeeping ref that mirrors something else). */
|
|
168
|
+
setRef(ref, sha) {
|
|
169
|
+
this.run(["update-ref", ref, sha]);
|
|
170
|
+
}
|
|
171
|
+
/** The author of a commit: `%an` and `%ae`. */
|
|
172
|
+
authorOf(commit) {
|
|
173
|
+
const [name, email] = this.text(["log", "-1", "--format=%an%x1f%ae", commit]).trim().split("\x1f");
|
|
174
|
+
return { name: name ?? "", email: email ?? "" };
|
|
175
|
+
}
|
|
176
|
+
/** `git update-ref -d <ref>`; a missing ref is fine. */
|
|
177
|
+
deleteRef(ref) {
|
|
178
|
+
this.run(["update-ref", "-d", ref], { allowFailure: true });
|
|
179
|
+
}
|
|
180
|
+
/** The sha a full ref name points at, or null when it does not exist. */
|
|
181
|
+
ref(ref) {
|
|
182
|
+
const r = this.run(["rev-parse", "--verify", "--quiet", ref], { allowFailure: true });
|
|
183
|
+
if (r.status !== 0)
|
|
184
|
+
return null;
|
|
185
|
+
const sha = r.stdout.toString("utf8").trim();
|
|
186
|
+
return SHA_RE.test(sha) ? sha : null;
|
|
187
|
+
}
|
|
188
|
+
/** Is `ancestor` an ancestor of (or equal to) `descendant`? (`git merge-base --is-ancestor`.) */
|
|
189
|
+
isAncestor(ancestor, descendant) {
|
|
190
|
+
return this.run(["merge-base", "--is-ancestor", ancestor, descendant], { allowFailure: true }).status === 0;
|
|
191
|
+
}
|
|
192
|
+
/** `git merge-base a b`, or null when the histories are unrelated. */
|
|
193
|
+
mergeBase(a, b) {
|
|
194
|
+
const r = this.run(["merge-base", a, b], { allowFailure: true });
|
|
195
|
+
if (r.status !== 0)
|
|
196
|
+
return null;
|
|
197
|
+
const sha = r.stdout.toString("utf8").trim();
|
|
198
|
+
return SHA_RE.test(sha) ? sha : null;
|
|
199
|
+
}
|
|
200
|
+
/** The commits reachable from `to` and not from `from`, oldest first (`git rev-list --reverse from..to`; `from` null = all of `to`). */
|
|
201
|
+
revList(from, to) {
|
|
202
|
+
const range = from === null ? to : `${from}..${to}`;
|
|
203
|
+
return this.text(["rev-list", "--reverse", range]).split("\n").map((l) => l.trim()).filter((l) => SHA_RE.test(l));
|
|
204
|
+
}
|
|
205
|
+
/** The parents of a commit. */
|
|
206
|
+
parentsOf(commit) {
|
|
207
|
+
return this.text(["rev-list", "--parents", "-n", "1", commit]).trim().split(/\s+/).slice(1).filter((p) => SHA_RE.test(p));
|
|
208
|
+
}
|
|
209
|
+
/** A tree-ish's blobs: `path → blob sha` (`git ls-tree -r`; only blobs — the content branch holds nothing else). */
|
|
210
|
+
treeEntries(treeish) {
|
|
211
|
+
const out = new Map();
|
|
212
|
+
if (treeish === EMPTY_TREE)
|
|
213
|
+
return out;
|
|
214
|
+
for (const line of this.text(["ls-tree", "-r", "-z", treeish]).split("\0")) {
|
|
215
|
+
if (line === "")
|
|
216
|
+
continue;
|
|
217
|
+
const m = /^(\d{6}) (\w+) ([0-9a-f]{40})\t(.*)$/s.exec(line);
|
|
218
|
+
if (m && m[2] === "blob")
|
|
219
|
+
out.set(m[4], m[3]);
|
|
220
|
+
}
|
|
221
|
+
return out;
|
|
222
|
+
}
|
|
223
|
+
/** The paths whose blob differs between two tree-ishes: `path → the blob in b` (null = gone in b). `git diff-tree -r`. */
|
|
224
|
+
diffTrees(a, b) {
|
|
225
|
+
const out = new Map();
|
|
226
|
+
// The -z form interleaves `:<mode a> <mode b> <sha a> <sha b> <status>` and `<path>` records; parse pairwise.
|
|
227
|
+
const records = this.text(["diff-tree", "-r", "-z", "--no-renames", a, b]).split("\0");
|
|
228
|
+
for (let i = 0; i + 1 < records.length; i += 2) {
|
|
229
|
+
const meta = records[i];
|
|
230
|
+
const path = records[i + 1];
|
|
231
|
+
const m = /^:(\d{6}) (\d{6}) ([0-9a-f]{40}) ([0-9a-f]{40}) ([ADMT])/.exec(meta);
|
|
232
|
+
if (!m)
|
|
233
|
+
continue;
|
|
234
|
+
out.set(path, m[5] === "D" ? null : m[4]);
|
|
235
|
+
}
|
|
236
|
+
return out;
|
|
237
|
+
}
|
|
238
|
+
/** A blob's bytes. */
|
|
239
|
+
blob(sha) {
|
|
240
|
+
return this.run(["cat-file", "blob", sha]).stdout;
|
|
241
|
+
}
|
|
242
|
+
/** The configured remotes (`git remote`). */
|
|
243
|
+
remotes() {
|
|
244
|
+
return this.text(["remote"]).split("\n").map((l) => l.trim()).filter((l) => l !== "");
|
|
245
|
+
}
|
|
246
|
+
/** `git remote add` or `set-url` so that `name` points at `url`; `remote.<name>.fetch` left as git sets it (the tracking refs under `refs/remotes/<name>/`). */
|
|
247
|
+
setRemote(name, url) {
|
|
248
|
+
if (this.remotes().includes(name))
|
|
249
|
+
this.run(["remote", "set-url", name, url]);
|
|
250
|
+
else
|
|
251
|
+
this.run(["remote", "add", name, url]);
|
|
252
|
+
}
|
|
253
|
+
/** `git remote remove`; a missing remote is fine. */
|
|
254
|
+
removeRemote(name) {
|
|
255
|
+
this.run(["remote", "remove", name], { allowFailure: true });
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* `git push <remote> <refspec>…` — NEVER `--force`. Returns `ok`, or `rejected`
|
|
259
|
+
* when the remote refused a non-fast-forward (git's `[rejected]` /
|
|
260
|
+
* `non-fast-forward` / `fetch first` / `stale info` lines), or `failed` with
|
|
261
|
+
* git's stderr for anything else (no network, no such remote).
|
|
262
|
+
*/
|
|
263
|
+
push(remote, refspecs) {
|
|
264
|
+
const r = this.run(["push", "--porcelain", "--quiet", remote, ...refspecs], { allowFailure: true });
|
|
265
|
+
if (r.status === 0)
|
|
266
|
+
return { status: "ok" };
|
|
267
|
+
const text = `${r.stdout.toString("utf8")}\n${r.stderr}`;
|
|
268
|
+
if (/\[rejected\]|non-fast-forward|fetch first|stale info|\n!\t/.test(text))
|
|
269
|
+
return { status: "rejected", stderr: r.stderr.trim() };
|
|
270
|
+
return { status: "failed", stderr: r.stderr.trim() };
|
|
271
|
+
}
|
|
272
|
+
/** `git fetch <remote> <refspec>…` (the refspecs may carry `+`: a tracking ref follows the remote wherever it went). Returns null on success, git's stderr on failure. */
|
|
273
|
+
fetch(remote, refspecs) {
|
|
274
|
+
const r = this.run(["fetch", "--quiet", "--no-tags", remote, ...refspecs], { allowFailure: true });
|
|
275
|
+
return r.status === 0 ? null : r.stderr.trim();
|
|
276
|
+
}
|
|
277
|
+
/** `git ls-remote <remote> <ref>`: the sha the remote holds for `ref`, null when it has none; throws when the remote cannot be reached. */
|
|
278
|
+
lsRemote(remote, ref) {
|
|
279
|
+
const out = this.text(["ls-remote", remote, ref]);
|
|
280
|
+
const m = /^([0-9a-f]{40})\t/.exec(out);
|
|
281
|
+
return m ? m[1] : null;
|
|
282
|
+
}
|
|
283
|
+
/** The type of an object (`commit`, `blob`, `tree`, `tag`), or null when the object is missing. */
|
|
284
|
+
objectType(sha) {
|
|
285
|
+
const r = this.run(["cat-file", "-t", sha], { allowFailure: true });
|
|
286
|
+
return r.status === 0 ? r.stdout.toString("utf8").trim() : null;
|
|
287
|
+
}
|
|
288
|
+
/** A commit's raw message (`%B`). */
|
|
289
|
+
message(commit) {
|
|
290
|
+
return this.text(["log", "-1", "--format=%B", commit]);
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* The commits of `branch`, oldest first, each as `{ sha, committerUnix, message }`;
|
|
294
|
+
* `grep` (fixed string) narrows to the commits whose message contains it.
|
|
295
|
+
*/
|
|
296
|
+
log(branch, opts = {}) {
|
|
297
|
+
if (this.tip(branch) === null)
|
|
298
|
+
return [];
|
|
299
|
+
const filter = opts.grep !== undefined ? ["--fixed-strings", `--grep=${opts.grep}`] : opts.regex !== undefined ? ["--extended-regexp", `--grep=${opts.regex}`] : [];
|
|
300
|
+
const args = ["log", "--reverse", "--format=%H%x1f%ct%x1f%B%x1e", ...filter, `refs/heads/${branch}`];
|
|
301
|
+
const out = this.text(args);
|
|
302
|
+
const records = out.split("\x1e").map((r) => r.replace(/^\n/, "")).filter((r) => r.trim() !== "");
|
|
303
|
+
return records.map((r) => {
|
|
304
|
+
const [sha, unix, message] = r.split("\x1f");
|
|
305
|
+
return { sha: sha.trim(), committerUnix: Number(unix), message: message ?? "" };
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* `git cat-file --batch` over `<commit>:<path>` specs: the bytes of each, or
|
|
310
|
+
* null when the object is missing (a path not in that tree). One spawn.
|
|
311
|
+
*/
|
|
312
|
+
blobsAt(specs) {
|
|
313
|
+
if (specs.length === 0)
|
|
314
|
+
return [];
|
|
315
|
+
const out = this.run(["cat-file", "--batch"], { input: specs.join("\n") + "\n" }).stdout;
|
|
316
|
+
const result = [];
|
|
317
|
+
let pos = 0;
|
|
318
|
+
for (let i = 0; i < specs.length; i++) {
|
|
319
|
+
const nl = out.indexOf(0x0a, pos);
|
|
320
|
+
if (nl < 0) {
|
|
321
|
+
result.push(null);
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
const header = out.subarray(pos, nl).toString("utf8");
|
|
325
|
+
pos = nl + 1;
|
|
326
|
+
const m = /^([0-9a-f]{40}) (\w+) (\d+)$/.exec(header);
|
|
327
|
+
if (!m) {
|
|
328
|
+
result.push(null);
|
|
329
|
+
continue;
|
|
330
|
+
} // `<spec> missing` / `ambiguous`
|
|
331
|
+
const size = Number(m[3]);
|
|
332
|
+
result.push(Buffer.from(out.subarray(pos, pos + size)));
|
|
333
|
+
pos += size + 1; // the trailing newline after the content
|
|
334
|
+
}
|
|
335
|
+
return result;
|
|
336
|
+
}
|
|
337
|
+
/** `git ls-tree -r <ref>` as text (the fixtures' projection). */
|
|
338
|
+
lsTree(ref) {
|
|
339
|
+
return this.text(["ls-tree", "-r", ref]);
|
|
340
|
+
}
|
|
341
|
+
/** `git count-objects -v` as a map (the measurement script's `size`/`size-pack`, in KiB). */
|
|
342
|
+
countObjects() {
|
|
343
|
+
const out = {};
|
|
344
|
+
for (const line of this.text(["count-objects", "-v"]).split("\n")) {
|
|
345
|
+
const m = /^([a-z-]+): (\d+)$/.exec(line.trim());
|
|
346
|
+
if (m)
|
|
347
|
+
out[m[1]] = Number(m[2]);
|
|
348
|
+
}
|
|
349
|
+
return out;
|
|
350
|
+
}
|
|
351
|
+
/** `git gc --quiet` (the measurement script packs before counting). */
|
|
352
|
+
gc() {
|
|
353
|
+
this.run(["gc", "--quiet"]);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
//# sourceMappingURL=git.js.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Store } from "@omgbase/core";
|
|
2
|
+
import type { GitRepo, GitIdentity } from "./git.js";
|
|
3
|
+
import type { GitHistoryConfig } from "./config.js";
|
|
4
|
+
export declare const IDS_PREFIX = ".omgbase/ids/";
|
|
5
|
+
export interface IdsBlock {
|
|
6
|
+
id: string;
|
|
7
|
+
parent: string | null;
|
|
8
|
+
ordinal: number;
|
|
9
|
+
hash: string;
|
|
10
|
+
}
|
|
11
|
+
export interface IdsRecord {
|
|
12
|
+
doc: string;
|
|
13
|
+
head: string;
|
|
14
|
+
blocks: IdsBlock[];
|
|
15
|
+
}
|
|
16
|
+
/** The id map of a live document from the rows, or null for a deleted/unknown one. */
|
|
17
|
+
export declare function idsRecord(store: Store, docId: string): IdsRecord | null;
|
|
18
|
+
/** The blob's bytes: canonical JSON (keys sorted bytewise at every level, as the manifest's `config`) and a final newline. */
|
|
19
|
+
export declare function idsBlobText(record: IdsRecord): string;
|
|
20
|
+
export declare const idsPath: (docId: string) => string;
|
|
21
|
+
/** The ids tip's `omgbase-seq` position, or null without a tip or with a foreign one. */
|
|
22
|
+
export declare function idsPosition(git: GitRepo, config: GitHistoryConfig): {
|
|
23
|
+
tip: string;
|
|
24
|
+
epoch: string;
|
|
25
|
+
seq: number;
|
|
26
|
+
} | null;
|
|
27
|
+
export interface IdsSyncInput {
|
|
28
|
+
store: Store;
|
|
29
|
+
repoId: string;
|
|
30
|
+
git: GitRepo;
|
|
31
|
+
config: GitHistoryConfig;
|
|
32
|
+
identity: GitIdentity;
|
|
33
|
+
/** the documents a checkpoint touched (deleted ones included — their blob leaves the tree); null = every live document (a catch-up or a snapshot) */
|
|
34
|
+
docs: string[] | null;
|
|
35
|
+
epoch: string;
|
|
36
|
+
from: number;
|
|
37
|
+
to: number;
|
|
38
|
+
ts: string;
|
|
39
|
+
/** the content commit this ids commit belongs to */
|
|
40
|
+
content: string;
|
|
41
|
+
actor: string | null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Write one ids commit for a checkpoint when the ids tree changed; returns the
|
|
45
|
+
* ids tip (unchanged when nothing moved). Never fails the checkpoint that
|
|
46
|
+
* called it beyond a thrown git error: the content ref already moved.
|
|
47
|
+
*/
|
|
48
|
+
export declare function syncIds(input: IdsSyncInput): string | null;
|
package/dist/src/ids.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { canonicalConfigJson } from "@omgbase/core";
|
|
2
|
+
import { parseMessage, renderMessage } from "./trailers.js";
|
|
3
|
+
// The identity record (spec/store §5.12 "The identity record", 17.6 — the
|
|
4
|
+
// design's option 3, second half): beside the content branch — a plain notes
|
|
5
|
+
// repository a customer can clone — the worker keeps a PARALLEL commit chain on
|
|
6
|
+
// `refs/omgbase/ids` (`config.ids_ref`) whose tree holds one blob per live
|
|
7
|
+
// document at `.omgbase/ids/<doc_id>`: canonical JSON
|
|
8
|
+
// `{ doc, head, blocks: [{ id, parent, ordinal, hash }] }` — the document's id
|
|
9
|
+
// and head token, its blocks in pre-order with their ids, parent ids (null at
|
|
10
|
+
// the top level), sibling ordinals and `raw_hash` (hex). Each ids commit
|
|
11
|
+
// carries the same `omgbase-seq` trailer as the checkpoint it belongs to and
|
|
12
|
+
// `omgbase-content: <content commit sha>` naming the content tip at that
|
|
13
|
+
// moment; its tree is the previous ids tree plus the touched documents' blobs
|
|
14
|
+
// (a deleted document's blob removed). Push and fetch carry the ref, so a
|
|
15
|
+
// clone of BOTH refs recovers content and identities; restoration from it is
|
|
16
|
+
// 6c. The chain is written AFTER the content ref moved (its own CAS), and
|
|
17
|
+
// catches up on resume: an ids tip whose `omgbase-seq` is behind the content
|
|
18
|
+
// tip's re-stages every live document once.
|
|
19
|
+
export const IDS_PREFIX = ".omgbase/ids/";
|
|
20
|
+
/** The id map of a live document from the rows, or null for a deleted/unknown one. */
|
|
21
|
+
export function idsRecord(store, docId) {
|
|
22
|
+
const doc = store.db.prepare("SELECT head, deleted_at FROM docs WHERE doc_id = ?").get(docId);
|
|
23
|
+
if (!doc || doc.deleted_at !== null || doc.head === null)
|
|
24
|
+
return null;
|
|
25
|
+
const rows = store.db
|
|
26
|
+
.prepare("SELECT block_id, parent_block, ordinal, raw_hash FROM blocks WHERE doc_id = ? AND deleted_at IS NULL ORDER BY order_key")
|
|
27
|
+
.all(docId);
|
|
28
|
+
return { doc: docId, head: doc.head, blocks: rows.map((r) => ({ id: r.block_id, parent: r.parent_block, ordinal: r.ordinal, hash: r.raw_hash.toString("hex") })) };
|
|
29
|
+
}
|
|
30
|
+
/** The blob's bytes: canonical JSON (keys sorted bytewise at every level, as the manifest's `config`) and a final newline. */
|
|
31
|
+
export function idsBlobText(record) {
|
|
32
|
+
return `${canonicalConfigJson(record)}\n`;
|
|
33
|
+
}
|
|
34
|
+
export const idsPath = (docId) => `${IDS_PREFIX}${docId}`;
|
|
35
|
+
/** The ids tip's `omgbase-seq` position, or null without a tip or with a foreign one. */
|
|
36
|
+
export function idsPosition(git, config) {
|
|
37
|
+
const tip = git.ref(config.ids_ref);
|
|
38
|
+
if (tip === null)
|
|
39
|
+
return null;
|
|
40
|
+
const t = parseMessage(git.message(tip));
|
|
41
|
+
return t ? { tip, epoch: t.epoch, seq: t.to } : null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Write one ids commit for a checkpoint when the ids tree changed; returns the
|
|
45
|
+
* ids tip (unchanged when nothing moved). Never fails the checkpoint that
|
|
46
|
+
* called it beyond a thrown git error: the content ref already moved.
|
|
47
|
+
*/
|
|
48
|
+
export function syncIds(input) {
|
|
49
|
+
const { store, git, config } = input;
|
|
50
|
+
const parent = git.ref(config.ids_ref);
|
|
51
|
+
const baseTree = parent === null ? null : git.treeOf(parent);
|
|
52
|
+
const live = new Set(store.db.prepare("SELECT doc_id FROM docs WHERE repo_id = ? AND deleted_at IS NULL AND head IS NOT NULL").all(input.repoId).map((r) => r.doc_id));
|
|
53
|
+
const existing = baseTree === null ? new Map() : git.treeEntries(baseTree);
|
|
54
|
+
const docs = input.docs ?? [...live];
|
|
55
|
+
const adds = [];
|
|
56
|
+
const removes = [];
|
|
57
|
+
for (const docId of [...new Set(docs)].sort()) {
|
|
58
|
+
const path = idsPath(docId);
|
|
59
|
+
const record = live.has(docId) ? idsRecord(store, docId) : null;
|
|
60
|
+
if (record === null) {
|
|
61
|
+
if (existing.has(path))
|
|
62
|
+
removes.push(path);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const blob = git.writeBlob(idsBlobText(record));
|
|
66
|
+
if (existing.get(path) !== blob)
|
|
67
|
+
adds.push({ path, blob });
|
|
68
|
+
}
|
|
69
|
+
if (input.docs === null)
|
|
70
|
+
for (const path of existing.keys())
|
|
71
|
+
if (path.startsWith(IDS_PREFIX) && !live.has(path.slice(IDS_PREFIX.length)))
|
|
72
|
+
removes.push(path);
|
|
73
|
+
if (adds.length === 0 && removes.length === 0)
|
|
74
|
+
return parent;
|
|
75
|
+
const tree = git.buildTree(baseTree, adds, removes);
|
|
76
|
+
if (baseTree !== null && tree === baseTree)
|
|
77
|
+
return parent;
|
|
78
|
+
const trailers = { epoch: input.epoch, from: input.from, to: input.to, ts: input.ts, commits: [], origin: null, actor: input.actor, plan: null, docs: [], rescan: null, merge: null, content: input.content };
|
|
79
|
+
const subject = `ids: ${adds.length + removes.length} document(s)`;
|
|
80
|
+
const sha = git.commitTree(tree, parent, renderMessage(subject, trailers), { author: { name: input.actor ?? input.identity.name, email: input.identity.email }, committer: input.identity, ts: input.ts });
|
|
81
|
+
git.updateRefRaw(config.ids_ref, sha, parent);
|
|
82
|
+
return sha;
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=ids.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { GitHistoryProvider, gitHistoryProvider, DEFAULT_IDENTITY, FIXTURE_IDENTITY, type GitProviderOptions } from "./provider.js";
|
|
2
|
+
export { GitHistoryReader } from "./reader.js";
|
|
3
|
+
export { runCheckpoint, commitSnapshot, tipPosition, resumePosition, type WorkerContext } from "./worker.js";
|
|
4
|
+
export { idsRecord, idsBlobText, idsPath, idsPosition, syncIds, IDS_PREFIX, type IdsRecord, type IdsBlock } from "./ids.js";
|
|
5
|
+
export { remoteSet, remoteUnset, remoteStatus, remoteSummary, pushBranch, fetchRemote, resolveConflict, reconcileRemote, remoteSync, pushAfterCheckpoint, pausedBy, sidecarPath, tsSegment, rejectedMessage, rewrittenMessage, conflictMessage, forkedMessage, GIT_REMOTE_DIVERGED_ALERT, GIT_MERGE_CONFLICT_ALERT, GIT_REMOTE_FORKED_ALERT, type RemoteStatus, type FetchResult, type FetchState, type ResolveResult, type ResolveChoice, type ReconcileResult, type ReconcileMode, } from "./remote.js";
|
|
6
|
+
export { createCheckpointScheduler, type CheckpointScheduler, type SchedulerOptions } from "./scheduler.js";
|
|
7
|
+
export { GitRepo, GitError, gitAvailable, resetGitProbe, gitDate, gitBin, GIT_BIN_ENV, ZERO_SHA, EMPTY_TREE, SHA_RE, type GitIdentity, type CommitIdentity } from "./git.js";
|
|
8
|
+
export { renderMessage, parseMessage, subjectOf, seqValue, TRAILER_PREFIX, type CommitTrailers, type DocTrailer } from "./trailers.js";
|
|
9
|
+
export { GIT_PROVIDER, DEFAULT_GIT_DIR, DEFAULT_BRANCH, IDS_REF, DEFAULT_REMOTE_NAME, PUSH_POLICIES, FETCH_POLICIES, gitConfigFor, readGitConfig, readRemoteConfig, remoteConfigFor, validRemoteName, parseCadence, refuseCredentials, type GitHistoryConfig, type GitCadence, type GitRemoteConfig, type GitRemoteOptions, type GitPushPolicy, type GitFetchPolicy, type GitEnableOptions, } from "./config.js";
|
|
10
|
+
export { GIT_CURSOR_SOURCE, readGitCursor, writeGitCursor, writeGitPushed, clearGitCursor, type GitCursor, type GitPushed } from "./cursor.js";
|
|
11
|
+
export { ensureExcluded, excludeFileFor, EXCLUDE_LINE } from "./exclude.js";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// @omgbase/git-adapter — the Git-backed history provider (modular-core Phase
|
|
2
|
+
// 6a; spec/store §5.12, spec/surface §3/§4, spec/cli `history`, spec/sync §6).
|
|
3
|
+
// The core knows a `HistoryProvider` by name; this package is the one named
|
|
4
|
+
// `git`. Nothing here is imported by `@omgbase/core`: the CLI/MCP host registers
|
|
5
|
+
// `gitHistoryProvider()` before it opens a store.
|
|
6
|
+
export { GitHistoryProvider, gitHistoryProvider, DEFAULT_IDENTITY, FIXTURE_IDENTITY } from "./provider.js";
|
|
7
|
+
export { GitHistoryReader } from "./reader.js";
|
|
8
|
+
export { runCheckpoint, commitSnapshot, tipPosition, resumePosition } from "./worker.js";
|
|
9
|
+
export { idsRecord, idsBlobText, idsPath, idsPosition, syncIds, IDS_PREFIX } from "./ids.js";
|
|
10
|
+
export { remoteSet, remoteUnset, remoteStatus, remoteSummary, pushBranch, fetchRemote, resolveConflict, reconcileRemote, remoteSync, pushAfterCheckpoint, pausedBy, sidecarPath, tsSegment, rejectedMessage, rewrittenMessage, conflictMessage, forkedMessage, GIT_REMOTE_DIVERGED_ALERT, GIT_MERGE_CONFLICT_ALERT, GIT_REMOTE_FORKED_ALERT, } from "./remote.js";
|
|
11
|
+
export { createCheckpointScheduler } from "./scheduler.js";
|
|
12
|
+
export { GitRepo, GitError, gitAvailable, resetGitProbe, gitDate, gitBin, GIT_BIN_ENV, ZERO_SHA, EMPTY_TREE, SHA_RE } from "./git.js";
|
|
13
|
+
export { renderMessage, parseMessage, subjectOf, seqValue, TRAILER_PREFIX } from "./trailers.js";
|
|
14
|
+
export { GIT_PROVIDER, DEFAULT_GIT_DIR, DEFAULT_BRANCH, IDS_REF, DEFAULT_REMOTE_NAME, PUSH_POLICIES, FETCH_POLICIES, gitConfigFor, readGitConfig, readRemoteConfig, remoteConfigFor, validRemoteName, parseCadence, refuseCredentials, } from "./config.js";
|
|
15
|
+
export { GIT_CURSOR_SOURCE, readGitCursor, writeGitCursor, writeGitPushed, clearGitCursor } from "./cursor.js";
|
|
16
|
+
export { ensureExcluded, excludeFileFor, EXCLUDE_LINE } from "./exclude.js";
|
|
17
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Store, CommitRecord, HistoryProvider, HistoryProviderAvailability, HistoryProviderConfig, HistoryCheckpointResult, HistoryProviderFactory, HistoryPushResult, HistoryRemoteSummary, HistoryRemoteSyncResult, HistoryVaultSink } from "@omgbase/core";
|
|
2
|
+
import { GitRepo, type GitIdentity } from "./git.js";
|
|
3
|
+
import { GitHistoryReader } from "./reader.js";
|
|
4
|
+
import { type GitHistoryConfig, type GitRemoteOptions } from "./config.js";
|
|
5
|
+
import { type FetchResult, type ReconcileMode, type ReconcileResult, type RemoteStatus, type ResolveChoice, type ResolveResult } from "./remote.js";
|
|
6
|
+
/** The committer identity in production. The author's name is the commit's actor when it has one. */
|
|
7
|
+
export declare const DEFAULT_IDENTITY: GitIdentity;
|
|
8
|
+
/** The identity the fixture runners and the CLI under the spec seams use (spec/store §9.4 "The git provider"): pinned shas follow from it. */
|
|
9
|
+
export declare const FIXTURE_IDENTITY: GitIdentity;
|
|
10
|
+
export interface GitProviderOptions {
|
|
11
|
+
identity?: GitIdentity;
|
|
12
|
+
}
|
|
13
|
+
export declare class GitHistoryProvider implements HistoryProvider {
|
|
14
|
+
private readonly store;
|
|
15
|
+
readonly name = "git";
|
|
16
|
+
private readonly identity;
|
|
17
|
+
constructor(store: Store, opts?: GitProviderOptions);
|
|
18
|
+
available(): HistoryProviderAvailability;
|
|
19
|
+
/** The workspace root: the parent of `.omgbase`. */
|
|
20
|
+
private root;
|
|
21
|
+
private repoOf;
|
|
22
|
+
private context;
|
|
23
|
+
reader(repoId: string, config: HistoryProviderConfig): GitHistoryReader;
|
|
24
|
+
enable(repoId: string, commit: CommitRecord, opts: Record<string, unknown>): {
|
|
25
|
+
config: HistoryProviderConfig;
|
|
26
|
+
baseline: {
|
|
27
|
+
revisions: number;
|
|
28
|
+
tip: string | null;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
purge(repoId: string, config: HistoryProviderConfig): {
|
|
32
|
+
removed: boolean;
|
|
33
|
+
path: string;
|
|
34
|
+
};
|
|
35
|
+
checkpoint(repoId: string, config: HistoryProviderConfig, opts?: {
|
|
36
|
+
now?: string;
|
|
37
|
+
}): HistoryCheckpointResult;
|
|
38
|
+
remoteSummary(repoId: string, config: HistoryProviderConfig): HistoryRemoteSummary | null;
|
|
39
|
+
remoteSync(repoId: string, config: HistoryProviderConfig, opts: {
|
|
40
|
+
now: string;
|
|
41
|
+
vault: HistoryVaultSink;
|
|
42
|
+
}): HistoryRemoteSyncResult | null;
|
|
43
|
+
/** `history remote set`: returns the config as written to the row. */
|
|
44
|
+
remoteSet(repoId: string, config: HistoryProviderConfig, options: GitRemoteOptions, now?: string): GitHistoryConfig;
|
|
45
|
+
/** `history remote unset`. */
|
|
46
|
+
remoteUnset(repoId: string, config: HistoryProviderConfig, now?: string): GitHistoryConfig;
|
|
47
|
+
/** `history remote status` (local refs only). */
|
|
48
|
+
remoteStatus(repoId: string, config: HistoryProviderConfig): RemoteStatus;
|
|
49
|
+
/** `history push`. */
|
|
50
|
+
push(repoId: string, config: HistoryProviderConfig, now?: string): HistoryPushResult & {
|
|
51
|
+
ids: "pushed" | "rejected" | "none";
|
|
52
|
+
};
|
|
53
|
+
/** `history fetch`: inbound content through `vault`. */
|
|
54
|
+
fetch(repoId: string, config: HistoryProviderConfig, vault: HistoryVaultSink, now?: string): FetchResult;
|
|
55
|
+
/** `history remote resolve <path> --ours|--theirs`. */
|
|
56
|
+
resolve(repoId: string, config: HistoryProviderConfig, vault: HistoryVaultSink, path: string, choice: ResolveChoice, now?: string): ResolveResult;
|
|
57
|
+
/** `history remote reconcile --rebase|--merge|--adopt|--fork`. */
|
|
58
|
+
reconcile(repoId: string, config: HistoryProviderConfig, vault: HistoryVaultSink, mode: ReconcileMode, now?: string): ReconcileResult;
|
|
59
|
+
/** The bare repository of a repository (the measurement script, tests). */
|
|
60
|
+
gitRepo(config: HistoryProviderConfig): GitRepo;
|
|
61
|
+
}
|
|
62
|
+
/** The factory the composition layer registers: `registerHistoryProvider("git", gitHistoryProvider())`. */
|
|
63
|
+
export declare function gitHistoryProvider(opts?: GitProviderOptions): HistoryProviderFactory;
|