hologit 0.50.2 → 0.50.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.
@@ -0,0 +1,281 @@
1
+ //! Regression: writing a child into a subtree that already exists in the
2
+ //! parent commit must load that subtree's existing children (preserving
3
+ //! siblings) rather than panic on `children.as_mut().unwrap()`.
4
+ //!
5
+ //! Surfaced by the gitsheets holo-tree spike (JarvusInnovations/gitsheets#127):
6
+ //! every upsert into an already-populated sheet directory hit this. Before the
7
+ //! fix, `get_or_create_subtree` left the final (lazily-loaded) node's
8
+ //! `children` as `None`, so `write_child_bytes` panicked — aborting the host
9
+ //! process when called across the napi FFI boundary.
10
+
11
+ mod helpers;
12
+
13
+ use helpers::Sandbox;
14
+ use holo_tree::MutableTree;
15
+
16
+ #[test]
17
+ fn write_child_into_existing_subtree_preserves_siblings() {
18
+ let sb = Sandbox::new();
19
+
20
+ // A committed tree that already contains data/a.toml.
21
+ let base = sb.write_tree(&[("data/a.toml", "id = 1\n")]);
22
+
23
+ // Load it lazily and write a sibling into the existing data/ directory.
24
+ let mut tree = MutableTree::new(base);
25
+ tree.write_child(&sb.repo, "data/b.toml", "id = 2\n").unwrap();
26
+ let new_hash = tree.write(&sb.repo).unwrap();
27
+ assert_ne!(new_hash, base, "writing a child must change the tree hash");
28
+
29
+ // Round-trip through the ODB: both records are present.
30
+ let mut reloaded = MutableTree::new(new_hash);
31
+ assert_eq!(
32
+ reloaded.read_blob(&sb.repo, "data/a.toml").unwrap().as_deref(),
33
+ Some(&b"id = 1\n"[..]),
34
+ "existing sibling must be preserved",
35
+ );
36
+ assert_eq!(
37
+ reloaded.read_blob(&sb.repo, "data/b.toml").unwrap().as_deref(),
38
+ Some(&b"id = 2\n"[..]),
39
+ "newly written child must be present",
40
+ );
41
+ }
42
+
43
+ #[test]
44
+ fn write_child_into_fresh_subtree_still_works() {
45
+ let sb = Sandbox::new();
46
+
47
+ // Empty starting point — the subtree is created fresh.
48
+ let mut tree = MutableTree::empty();
49
+ tree.write_child(&sb.repo, "data/only.toml", "id = 1\n").unwrap();
50
+ let hash = tree.write(&sb.repo).unwrap();
51
+
52
+ let mut reloaded = MutableTree::new(hash);
53
+ assert_eq!(
54
+ reloaded.read_blob(&sb.repo, "data/only.toml").unwrap().as_deref(),
55
+ Some(&b"id = 1\n"[..]),
56
+ );
57
+ }
58
+
59
+ /// Regression: writing a file at the REPO ROOT (dir = ".") into a lazily-loaded
60
+ /// tree must load the root's children rather than panic on
61
+ /// `children.as_mut().unwrap()`. The earlier fix only covered deep paths; the
62
+ /// `path == "."` early-return in get_or_create_subtree bypassed it. Surfaced by
63
+ /// install-testing the published binding with a root-level `writeChild`.
64
+ #[test]
65
+ fn write_child_at_repo_root_preserves_siblings() {
66
+ let sb = Sandbox::new();
67
+
68
+ // Committed tree with a root-level file; reload it lazily (children: None).
69
+ let base = sb.write_tree(&[("existing.toml", "id = 0\n")]);
70
+ let mut tree = MutableTree::new(base);
71
+
72
+ tree.write_child(&sb.repo, "new.toml", "id = 1\n").unwrap();
73
+ let hash = tree.write(&sb.repo).unwrap();
74
+ assert_ne!(hash, base, "a root-level write must change the tree hash");
75
+
76
+ let mut reloaded = MutableTree::new(hash);
77
+ assert_eq!(
78
+ reloaded.read_blob(&sb.repo, "existing.toml").unwrap().as_deref(),
79
+ Some(&b"id = 0\n"[..]),
80
+ "existing root sibling must be preserved",
81
+ );
82
+ assert_eq!(
83
+ reloaded.read_blob(&sb.repo, "new.toml").unwrap().as_deref(),
84
+ Some(&b"id = 1\n"[..]),
85
+ );
86
+ }
87
+
88
+ #[test]
89
+ fn write_child_at_repo_root_into_empty() {
90
+ let sb = Sandbox::new();
91
+ let mut tree = MutableTree::empty();
92
+ tree.write_child(&sb.repo, "only.toml", "id = 1\n").unwrap();
93
+ let hash = tree.write(&sb.repo).unwrap();
94
+
95
+ let mut reloaded = MutableTree::new(hash);
96
+ assert_eq!(
97
+ reloaded.read_blob(&sb.repo, "only.toml").unwrap().as_deref(),
98
+ Some(&b"id = 1\n"[..]),
99
+ );
100
+ }
101
+
102
+ /// Regression: deleting a child deep inside a lazily-loaded tree must dirty the
103
+ /// ancestor chain, or `write()` short-circuits on a clean root and silently
104
+ /// drops the deletion (returns the original tree hash). Surfaced by the
105
+ /// gitsheets migration (#127): every Sheet delete/rename-cleanup hit this once
106
+ /// the working tree was binding-backed.
107
+ #[test]
108
+ fn delete_child_deep_into_existing_tree_persists() {
109
+ let sb = Sandbox::new();
110
+
111
+ // Committed tree with two records in a directory; reload it lazily.
112
+ let base = sb.write_tree(&[("data/a.toml", "id = 1\n"), ("data/b.toml", "id = 2\n")]);
113
+ let mut tree = MutableTree::new(base);
114
+
115
+ let deleted = tree.delete_child_deep(&sb.repo, "data/a.toml").unwrap();
116
+ assert!(deleted, "the record existed and should report deleted");
117
+
118
+ let new_hash = tree.write(&sb.repo).unwrap();
119
+ assert_ne!(new_hash, base, "deletion must change the tree hash (the bug: it didn't)");
120
+
121
+ let mut reloaded = MutableTree::new(new_hash);
122
+ assert!(
123
+ reloaded.read_blob(&sb.repo, "data/a.toml").unwrap().is_none(),
124
+ "deleted record must be gone after write()",
125
+ );
126
+ assert_eq!(
127
+ reloaded.read_blob(&sb.repo, "data/b.toml").unwrap().as_deref(),
128
+ Some(&b"id = 2\n"[..]),
129
+ "sibling must be preserved",
130
+ );
131
+ }
132
+
133
+ /// Deleting a non-existent deep path is a clean no-op: returns false and leaves
134
+ /// the tree hash unchanged.
135
+ #[test]
136
+ fn delete_child_deep_missing_is_noop() {
137
+ let sb = Sandbox::new();
138
+ let base = sb.write_tree(&[("data/a.toml", "id = 1\n")]);
139
+ let mut tree = MutableTree::new(base);
140
+
141
+ assert!(!tree.delete_child_deep(&sb.repo, "data/missing.toml").unwrap());
142
+ assert_eq!(tree.write(&sb.repo).unwrap(), base, "no-op delete must not change the hash");
143
+ }
144
+
145
+ // ── write_child_hash (#477): place an existing blob by hash ─────────────────
146
+
147
+ /// Placing a blob by hash yields a byte-identical tree to writing the same
148
+ /// content via `write_child_bytes` — the whole point is to skip re-hashing while
149
+ /// producing exactly the same result.
150
+ #[test]
151
+ fn write_child_hash_matches_write_child_bytes() {
152
+ let sb = Sandbox::new();
153
+ let content = b"attachment payload\n";
154
+ let blob = sb.repo.write_blob(content).unwrap().detach();
155
+
156
+ let mut by_bytes = MutableTree::empty();
157
+ by_bytes
158
+ .write_child_bytes(&sb.repo, "data/att.bin", content)
159
+ .unwrap();
160
+ let bytes_hash = by_bytes.write(&sb.repo).unwrap();
161
+
162
+ let mut by_hash = MutableTree::empty();
163
+ by_hash
164
+ .write_child_hash(&sb.repo, "data/att.bin", blob, 0o100644)
165
+ .unwrap();
166
+ let hash_hash = by_hash.write(&sb.repo).unwrap();
167
+
168
+ assert_eq!(
169
+ hash_hash, bytes_hash,
170
+ "place-by-hash must produce the same tree as write-by-content",
171
+ );
172
+ }
173
+
174
+ /// Placing a blob by hash into an already-populated, lazily-loaded subtree
175
+ /// preserves the existing siblings (same subtree-loading path as
176
+ /// `write_child_bytes`).
177
+ #[test]
178
+ fn write_child_hash_preserves_siblings() {
179
+ let sb = Sandbox::new();
180
+ let base = sb.write_tree(&[("data/a.toml", "id = 1\n")]);
181
+ let blob = sb.repo.write_blob(b"id = 2\n").unwrap().detach();
182
+
183
+ let mut tree = MutableTree::new(base);
184
+ tree.write_child_hash(&sb.repo, "data/b.toml", blob, 0o100644)
185
+ .unwrap();
186
+ let new_hash = tree.write(&sb.repo).unwrap();
187
+ assert_ne!(new_hash, base);
188
+
189
+ let mut reloaded = MutableTree::new(new_hash);
190
+ assert_eq!(
191
+ reloaded.read_blob(&sb.repo, "data/a.toml").unwrap().as_deref(),
192
+ Some(&b"id = 1\n"[..]),
193
+ "existing sibling must be preserved",
194
+ );
195
+ assert_eq!(
196
+ reloaded.read_blob(&sb.repo, "data/b.toml").unwrap().as_deref(),
197
+ Some(&b"id = 2\n"[..]),
198
+ "placed-by-hash blob must be present",
199
+ );
200
+ }
201
+
202
+ /// The `mode` argument is honored — placing the same blob as executable records
203
+ /// mode 100755, producing a different tree than the regular-mode placement.
204
+ #[test]
205
+ fn write_child_hash_honors_executable_mode() {
206
+ let sb = Sandbox::new();
207
+ let blob = sb.repo.write_blob(b"#!/bin/sh\n").unwrap().detach();
208
+
209
+ let mut regular = MutableTree::empty();
210
+ regular
211
+ .write_child_hash(&sb.repo, "run", blob, 0o100644)
212
+ .unwrap();
213
+ let regular_hash = regular.write(&sb.repo).unwrap();
214
+
215
+ let mut exec = MutableTree::empty();
216
+ exec.write_child_hash(&sb.repo, "run", blob, 0o100755)
217
+ .unwrap();
218
+ let exec_hash = exec.write(&sb.repo).unwrap();
219
+
220
+ assert_ne!(
221
+ regular_hash, exec_hash,
222
+ "executable mode must be reflected in the tree entry",
223
+ );
224
+ }
225
+
226
+ /// A hash that isn't in the ODB is rejected — no invalid entry is grafted.
227
+ #[test]
228
+ fn write_child_hash_rejects_missing_object() {
229
+ let sb = Sandbox::new();
230
+ // A valid-shaped sha1 that was never written to this repo's ODB.
231
+ let absent: gix::ObjectId = "0123456789abcdef0123456789abcdef01234567"
232
+ .parse()
233
+ .unwrap();
234
+
235
+ let mut tree = MutableTree::empty();
236
+ let err = tree
237
+ .write_child_hash(&sb.repo, "x", absent, 0o100644)
238
+ .unwrap_err();
239
+ assert!(matches!(err, holo_tree::Error::Git(_)), "got {err:?}");
240
+ }
241
+
242
+ /// A hash pointing at a non-blob object (here a tree) is rejected rather than
243
+ /// silently producing a tree entry that claims a blob mode over a tree object.
244
+ #[test]
245
+ fn write_child_hash_rejects_non_blob() {
246
+ let sb = Sandbox::new();
247
+ let tree_oid = sb.write_tree(&[("inner.toml", "id = 1\n")]);
248
+
249
+ let mut tree = MutableTree::empty();
250
+ let err = tree
251
+ .write_child_hash(&sb.repo, "x", tree_oid, 0o100644)
252
+ .unwrap_err();
253
+ match err {
254
+ holo_tree::Error::Git(msg) => assert!(
255
+ msg.contains("not a blob"),
256
+ "expected a not-a-blob error, got: {msg}",
257
+ ),
258
+ other => panic!("expected Git error, got {other:?}"),
259
+ }
260
+ }
261
+
262
+ /// An invalid (non-blob) mode is rejected up front, before any tree mutation —
263
+ /// it would otherwise panic later in `write()` when mapped to an `EntryMode`.
264
+ #[test]
265
+ fn write_child_hash_rejects_invalid_mode() {
266
+ let sb = Sandbox::new();
267
+ let blob = sb.repo.write_blob(b"x\n").unwrap().detach();
268
+
269
+ let mut tree = MutableTree::empty();
270
+ // 040000 is a tree mode, not valid for a blob entry.
271
+ let err = tree
272
+ .write_child_hash(&sb.repo, "x", blob, 0o040000)
273
+ .unwrap_err();
274
+ assert!(matches!(err, holo_tree::Error::Git(_)), "got {err:?}");
275
+ // The tree must be untouched by a rejected call.
276
+ assert_eq!(
277
+ tree.write(&sb.repo).unwrap(),
278
+ MutableTree::empty().write(&sb.repo).unwrap(),
279
+ "a rejected write_child_hash must not mutate the tree",
280
+ );
281
+ }
@@ -0,0 +1,22 @@
1
+ [package]
2
+ name = "holo-tree-napi"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ license = "MIT"
6
+ description = "Node.js native binding for holo-tree — mutable in-memory git trees via gix"
7
+ publish = false
8
+
9
+ [lib]
10
+ crate-type = ["cdylib"]
11
+
12
+ [dependencies]
13
+ # Node-API floor of 6 covers Node.js >= 20 comfortably.
14
+ napi = { version = "2", default-features = false, features = ["napi6"] }
15
+ napi-derive = "2"
16
+ holo-tree = { path = "../holo-tree" }
17
+ # gix defaults only — the binding does no networking, so we avoid the TLS/
18
+ # OpenSSL pull-in of `blocking-network-client` to keep cross-compilation clean.
19
+ gix = { version = "0.83" }
20
+
21
+ [build-dependencies]
22
+ napi-build = "2"
@@ -0,0 +1,144 @@
1
+ # holo-tree-napi
2
+
3
+ Node.js native binding for [`holo-tree`](../holo-tree) — mutable in-memory git
4
+ trees via [gitoxide](https://github.com/GitoxideLabs/gitoxide), with no `git`
5
+ subprocess.
6
+
7
+ This crate exposes the narrow slice of holo-tree that record-oriented consumers
8
+ (notably [gitsheets](https://github.com/JarvusInnovations/gitsheets)) need for an
9
+ upsert→commit path. It is intentionally a thin pass-through; it is also the first
10
+ integrated consumer of the new Rust libs, so it doubles as a hardening vehicle —
11
+ rough edges in holo-tree's API are recorded as `Phase-C finding` notes in the
12
+ source and fixed upstream rather than worked around here.
13
+
14
+ ## API
15
+
16
+ ```js
17
+ const { Repo, emptyTreeHash } = require('@hologit/holo-tree');
18
+
19
+ const repo = Repo.open('/path/to/repo/.git');
20
+
21
+ const tree = repo.createTreeFromRef('HEAD'); // or repo.createTree() for empty
22
+ tree.writeChild('data/widgets/1.toml', 'id = 1\n'); // hash blob + deep insert
23
+ const treeHash = tree.write(); // flush dirty subtrees → ODB, returns tree hash
24
+
25
+ const commit = repo.commitTree(treeHash, [parentHash], 'add widget 1');
26
+ repo.updateRef('refs/heads/main', commit);
27
+
28
+ const bytes = repo.createTreeFromRef(commit).readBlob('data/widgets/1.toml');
29
+ ```
30
+
31
+ Conventions: object ids cross the boundary as lowercase 40-char hex strings;
32
+ blob content crosses as `Buffer` (binary-safe).
33
+
34
+ ### Surface
35
+
36
+ | Method | holo-tree call | Notes |
37
+ |---|---|---|
38
+ | `Repo.open(gitDir)` | `gix::open().into_sync()` | factory |
39
+ | `repo.createTreeFromRef(ref)` → `Tree` | `repo::create_tree_from_ref` | resolves ref→commit→tree |
40
+ | `repo.createTree()` → `Tree` | `MutableTree::empty` | |
41
+ | `repo.commitTree(treeHash, parents[], msg)` → hash | `repo::commit_tree` | uses git-config identity |
42
+ | `repo.updateRef(ref, hash, expectedOldHash?)` | `repo::update_ref` | compare-and-swap when `expectedOldHash` given; force otherwise |
43
+ | `repo.resolveRef(ref)` → `hash\|null` | `repo::resolve_ref` | peels tags; `null` if unresolved |
44
+ | `repo.writeBlob(buf)` → hash | `gix write_blob` | hash bytes into the ODB, no tree |
45
+ | `tree.writeChild(path, text)` → hash | `MutableTree::write_child` | UTF-8 text |
46
+ | `tree.writeChildBytes(path, buf)` → hash | `MutableTree::write_child_bytes` | binary |
47
+ | `tree.readBlob(path)` → `Buffer\|null` | `MutableTree::read_blob` | |
48
+ | `tree.getChild(path)` → `{type,hash,mode}\|null` | `MutableTree::get_child` | read-only; deep path |
49
+ | `tree.getChildren(path)` → `[{name,type,hash,mode}]` | `get_subtree`+`ensure_children` | read-only; direct children |
50
+ | `tree.getBlobMap(path?)` → `[{path,hash,mode}]` | `get_subtree`+`get_blob_map` | read-only; paths relative to subtree |
51
+ | `tree.deleteChildDeep(path)` → bool | `MutableTree::delete_child_deep` | |
52
+ | `tree.clearChildren(path)` | `MutableTree::clear_children` | O(1) subtree wipe |
53
+ | `tree.merge(other, {files?, mode})` | `MutableTree::merge` | `mode`: `overlay`/`replace`/`underlay` |
54
+ | `tree.write()` → treeHash | `MutableTree::write` | |
55
+ | `emptyTreeHash()` → hash | `tree::empty_tree_id` | module fn |
56
+
57
+ `mode` values are the git filemode as a number (e.g. `33188` = `0o100644`). Tree
58
+ hashes reported by the read-only navigators reflect the last `write()`/load and
59
+ are stale for a subtree mutated since — flush with `write()` for canonical
60
+ hashes.
61
+
62
+ ## Building
63
+
64
+ Requires a Rust toolchain and `@napi-rs/cli` (a devDependency):
65
+
66
+ ```sh
67
+ npm install
68
+ npm run build:debug # or: npm run build (release)
69
+ npm test # node --test against a scratch git repo
70
+ ```
71
+
72
+ `napi build` emits `holo-tree.<triple>.node`. The generated `index.js` loader
73
+ and `index.d.ts` types **are committed**; only the `.node` binaries are
74
+ git-ignored (built per-platform in CI).
75
+
76
+ ## Publishing
77
+
78
+ Published as the scoped package **`@hologit/holo-tree`** with per-platform
79
+ prebuilt binaries shipped as `optionalDependencies`:
80
+
81
+ | Platform package | Triple | Built on | Smoke-tested |
82
+ | --- | --- | --- | --- |
83
+ | `@hologit/holo-tree-linux-x64-gnu` | `x86_64-unknown-linux-gnu` | ubuntu-latest | ✓ native |
84
+ | `@hologit/holo-tree-linux-arm64-gnu` | `aarch64-unknown-linux-gnu` | ubuntu-24.04-arm | ✓ native |
85
+ | `@hologit/holo-tree-linux-x64-musl` | `x86_64-unknown-linux-musl` | ubuntu-latest (musl cross) | build-only |
86
+ | `@hologit/holo-tree-darwin-arm64` | `aarch64-apple-darwin` | macos-latest | ✓ native |
87
+ | `@hologit/holo-tree-darwin-x64` | `x86_64-apple-darwin` | macos-latest (cross) | build-only |
88
+ | `@hologit/holo-tree-win32-x64-msvc` | `x86_64-pc-windows-msvc` | windows-latest | ✓ native |
89
+
90
+ Native targets build + smoke-test on a matching runner; cross targets (musl,
91
+ darwin-x64) build only, since their `.node` can't run on the host arch/libc (the
92
+ logic is covered by the native runs). The `.github/workflows/holo-tree-napi.yml`
93
+ workflow builds all six on every PR touching the binding, and on a
94
+ `holo-tree-v*` tag it builds then publishes.
95
+
96
+ Auth is **npm trusted publishing (OIDC)** — no tokens, matching hologit's
97
+ `publish-npm.yml`. Trusted publishing is configured *per package*, and a package
98
+ can't get a trusted publisher until it exists — so the four packages need a
99
+ **one-time manual bootstrap** before automated releases work.
100
+
101
+ ### One-time bootstrap (manual first publish, then configure trusted publishing)
102
+
103
+ The four packages all start at an early version (currently `0.0.1`). They must
104
+ exist on npm before trusted publishing can be turned on.
105
+
106
+ 1. **Get the prebuilt binaries.** Run the `holo-tree-napi` workflow (push the
107
+ branch / open a PR, or trigger `workflow_dispatch`) and download its three
108
+ `bindings-*` artifacts — they hold the `.node` for each platform. A single
109
+ machine can't build all three natively, so use the CI artifacts.
110
+
111
+ 2. **Publish all four manually**, logged in as an `@hologit` org member
112
+ (`npm login`):
113
+
114
+ ```sh
115
+ cd holo-tree-napi
116
+ npm install
117
+ npx napi artifacts --dir <downloaded-artifacts-dir> # → npm/<triple>/*.node
118
+ # platform packages first, then the main package:
119
+ for d in npm/*/ ; do ( cd "$d" && npm publish --access public ); done
120
+ npm publish --access public --ignore-scripts # main; skip the napi
121
+ # prepublish hook
122
+ ```
123
+
124
+ 3. **Turn on trusted publishing** on npmjs.com for **each** of the four packages
125
+ → Settings → Trusted Publisher → GitHub Actions, repo
126
+ `JarvusInnovations/hologit`, workflow `holo-tree-napi.yml`.
127
+
128
+ ### Releases (after bootstrap — fully automated, tokenless)
129
+
130
+ ```sh
131
+ git tag holo-tree-v0.1.1 && git push origin holo-tree-v0.1.1
132
+ ```
133
+
134
+ The tag drives the published version; CI builds all three platforms, then
135
+ publishes via OIDC (provenance). No secret needed. The `holo-tree-v*` tag is the
136
+ release marker — napi runs with `--skip-gh-release` so it does **not** create a
137
+ bare `v<version>` GitHub release/tag (which would collide with hologit's own
138
+ `v*` JS-package release namespace).
139
+
140
+ To add or drop a platform later, edit `napi.triples.additional` +
141
+ `optionalDependencies` in `package.json`, run `napi create-npm-dir -t .`, add the
142
+ matching matrix entry in the workflow, and (since it's a new package) bootstrap
143
+
144
+ + trust that one package too.
@@ -0,0 +1,3 @@
1
+ fn main() {
2
+ napi_build::setup();
3
+ }
@@ -0,0 +1,170 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+
4
+ /* auto-generated by NAPI-RS */
5
+
6
+ /** Git's well-known empty-tree hash (`4b825dc6…`). */
7
+ export declare function emptyTreeHash(): string
8
+ /**
9
+ * A commit identity (author or committer). `timeSeconds`/`offsetMinutes` are
10
+ * optional; when omitted the current wall-clock time at UTC is used. Pass them
11
+ * explicitly to reproduce a specific commit (e.g. match `git commit-tree`
12
+ * under pinned `GIT_AUTHOR_DATE`/`GIT_COMMITTER_DATE`).
13
+ */
14
+ export interface Signature {
15
+ name: string
16
+ email: string
17
+ timeSeconds?: number
18
+ offsetMinutes?: number
19
+ }
20
+ /**
21
+ * A child entry returned by read-only navigation. `type` is `"tree"`,
22
+ * `"blob"`, or `"commit"`; `mode` is the git filemode as a number
23
+ * (e.g. `33188` = `0o100644`, `16384` = `0o040000` for a tree).
24
+ */
25
+ export interface ChildInfo {
26
+ type: string
27
+ hash: string
28
+ mode: number
29
+ }
30
+ /** A named child entry, returned by `getChildren`. */
31
+ export interface NamedChildInfo {
32
+ name: string
33
+ type: string
34
+ hash: string
35
+ mode: number
36
+ }
37
+ /**
38
+ * A blob entry in a flattened blob map, returned by `getBlobMap`. `path` is
39
+ * relative to the navigated subtree.
40
+ */
41
+ export interface BlobEntry {
42
+ path: string
43
+ hash: string
44
+ mode: number
45
+ }
46
+ /**
47
+ * Options for `Tree.merge`. `mode` is `"overlay"`, `"replace"`, or
48
+ * `"underlay"`; `files` is an optional list of glob patterns restricting which
49
+ * paths merge (omit to merge everything).
50
+ */
51
+ export interface MergeOpts {
52
+ files?: Array<string>
53
+ mode: string
54
+ }
55
+ /**
56
+ * A handle to a git repository, backed by gix.
57
+ *
58
+ * Stored as a `ThreadSafeRepository` so the handle is `Send + Sync` and can be
59
+ * cheaply cloned into each `Tree`; every call derives a thread-local
60
+ * `gix::Repository` via `to_thread_local()`.
61
+ */
62
+ export declare class Repo {
63
+ /**
64
+ * Open a repository at `gitDir` (a `.git` directory, or any path gix can
65
+ * discover a repo from).
66
+ */
67
+ static open(gitDir: string): Repo
68
+ /**
69
+ * Resolve a ref (branch, tag, or commit hash) to its tree and return a
70
+ * mutable, in-memory view of it.
71
+ */
72
+ createTreeFromRef(gitRef: string): Tree
73
+ /** Create a fresh empty, mutable in-memory tree rooted at this repo. */
74
+ createTree(): Tree
75
+ /**
76
+ * Write a commit object pointing at `treeHash` with `parents`. `author`
77
+ * and `committer` are optional; each falls back to the repo's configured
78
+ * identity, then a "holo-tree" default. Returns the new commit hash.
79
+ */
80
+ commitTree(treeHash: string, parents: Array<string>, message: string, author?: Signature | undefined | null, committer?: Signature | undefined | null): string
81
+ /**
82
+ * Point a ref at an object hash.
83
+ *
84
+ * When `expectedOldHash` is provided this is a **compare-and-swap**: the
85
+ * update only succeeds if the ref currently resolves to exactly that hash,
86
+ * so a concurrent writer who moved the ref makes the swap fail rather than
87
+ * silently clobbering their commit. Omit it to force the ref (the prior
88
+ * unconditional behavior).
89
+ */
90
+ updateRef(refname: string, hash: string, expectedOldHash?: string | undefined | null): void
91
+ /**
92
+ * Resolve a ref / rev-spec (branch, tag, `HEAD`, hash, …) to its commit
93
+ * hash, peeling annotated tags. Returns `null` when the ref does not
94
+ * resolve — the natural "does this ref exist?" probe before a CAS
95
+ * `updateRef`.
96
+ */
97
+ resolveRef(gitRef: string): string | null
98
+ /**
99
+ * Hash raw bytes as a loose blob in the ODB and return its hash, without
100
+ * inserting it into any tree. Binary-safe.
101
+ */
102
+ writeBlob(content: Buffer): string
103
+ }
104
+ /**
105
+ * A mutable, in-memory git tree.
106
+ *
107
+ * Holds its own clone of the repo handle so JS callers don't thread a repo
108
+ * argument through every call.
109
+ *
110
+ * Phase-C finding #1: holo-tree's `MutableTree` takes `&gix::Repository` on
111
+ * nearly every method and keeps a *thread-local* tree cache. We smooth the
112
+ * first half here (the handle lives on the `Tree`) but NOT the second: each
113
+ * call does `to_thread_local()`, and whether holo-tree's thread-local cache
114
+ * stays warm across libuv-dispatched calls is the open ergonomics question to
115
+ * resolve upstream (e.g. a repo-bound tree handle, or an explicit session/
116
+ * cache object the consumer owns).
117
+ */
118
+ export declare class Tree {
119
+ /**
120
+ * Hash `content` (UTF-8 text) as a blob and insert it at `path`, creating
121
+ * intermediate trees as needed. Returns the blob hash.
122
+ */
123
+ writeChild(path: string, content: string): string
124
+ /** Hash raw bytes as a blob and insert at `path`. Binary-safe. */
125
+ writeChildBytes(path: string, content: Buffer): string
126
+ /**
127
+ * Place an already-written blob at `path` by its `hash`, without reading its
128
+ * bytes. Unlike `writeChildBytes` (which re-hashes content), this grafts a
129
+ * blob already in the ODB — validated to exist and be a blob via a header
130
+ * lookup, so a large attachment isn't read back and re-hashed. `mode` is the
131
+ * git filemode: `0o100644` regular, `0o100755` executable, `0o120000`
132
+ * symlink. Returns the placed hash.
133
+ */
134
+ writeChildHash(path: string, hash: string, mode: number): string
135
+ /** Read a blob's bytes at `path`, or `null` if no blob exists there. */
136
+ readBlob(path: string): Buffer | null
137
+ /**
138
+ * Read-only: look up the child at a deep `path` and report its type,
139
+ * hash, and mode, or `null` if nothing exists there.
140
+ */
141
+ getChild(path: string): ChildInfo | null
142
+ /**
143
+ * Read-only: list the direct children of the subtree at `path` (use `"."`
144
+ * for the root). Returns an empty array if `path` is missing or not a tree.
145
+ */
146
+ getChildren(path: string): Array<NamedChildInfo>
147
+ /**
148
+ * Read-only: recursively collect every blob under the subtree at `path`
149
+ * (defaults to the whole tree) into a flat list. Each `path` is relative
150
+ * to the navigated subtree. Returns an empty array if `path` is missing.
151
+ */
152
+ getBlobMap(path?: string | undefined | null): Array<BlobEntry>
153
+ /** Delete a child at a deep `path`. Returns whether it existed. */
154
+ deleteChildDeep(path: string): boolean
155
+ /**
156
+ * Clear all children under a deep `path` in O(1) — replace the subtree
157
+ * there with the empty tree (and dirty its ancestors) without loading the
158
+ * cleared subtree's contents. `path == "."` clears the whole tree. Used to
159
+ * wipe a directory before a full rewrite.
160
+ */
161
+ clearChildren(path: string): void
162
+ /**
163
+ * Merge another tree into this one in place, per `options.mode`
164
+ * (`overlay`/`replace`/`underlay`) and optional `options.files` globs.
165
+ * `other` must be a *different* `Tree` instance.
166
+ */
167
+ merge(other: Tree, options: MergeOpts): void
168
+ /** Flush dirty subtrees to the ODB and return the resulting tree hash. */
169
+ write(): string
170
+ }