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.
- package/Cargo.lock +103 -10
- package/Cargo.toml +1 -1
- package/holo-tree/Cargo.toml +4 -1
- package/holo-tree/src/error.rs +44 -0
- package/holo-tree/src/repo.rs +160 -24
- package/holo-tree/src/tree.rs +159 -40
- package/holo-tree/tests/clear_children.rs +95 -0
- package/holo-tree/tests/helpers/mod.rs +25 -0
- package/holo-tree/tests/repo_ops.rs +130 -0
- package/holo-tree/tests/write_child.rs +281 -0
- package/holo-tree-napi/Cargo.toml +22 -0
- package/holo-tree-napi/README.md +144 -0
- package/holo-tree-napi/build.rs +3 -0
- package/holo-tree-napi/index.d.ts +170 -0
- package/holo-tree-napi/index.js +317 -0
- package/holo-tree-napi/npm/darwin-arm64/README.md +3 -0
- package/holo-tree-napi/npm/darwin-arm64/package.json +24 -0
- package/holo-tree-napi/npm/darwin-x64/README.md +3 -0
- package/holo-tree-napi/npm/darwin-x64/package.json +24 -0
- package/holo-tree-napi/npm/linux-arm64-gnu/README.md +3 -0
- package/holo-tree-napi/npm/linux-arm64-gnu/package.json +27 -0
- package/holo-tree-napi/npm/linux-x64-gnu/README.md +3 -0
- package/holo-tree-napi/npm/linux-x64-gnu/package.json +27 -0
- package/holo-tree-napi/npm/linux-x64-musl/README.md +3 -0
- package/holo-tree-napi/npm/linux-x64-musl/package.json +27 -0
- package/holo-tree-napi/npm/win32-x64-msvc/README.md +3 -0
- package/holo-tree-napi/npm/win32-x64-msvc/package.json +24 -0
- package/holo-tree-napi/package-lock.json +89 -0
- package/holo-tree-napi/package.json +53 -0
- package/holo-tree-napi/src/lib.rs +456 -0
- package/package.json +1 -1
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
//! holo-tree-napi: Node.js native binding for [holo-tree](../holo-tree).
|
|
2
|
+
//!
|
|
3
|
+
//! Exposes the narrow slice of holo-tree's `MutableTree` + `repo` helpers that
|
|
4
|
+
//! gitsheets needs for its upsert→commit path:
|
|
5
|
+
//!
|
|
6
|
+
//! ```text
|
|
7
|
+
//! const repo = Repo.open(gitDir)
|
|
8
|
+
//! const tree = repo.createTreeFromRef('HEAD') // or repo.createTree()
|
|
9
|
+
//! tree.writeChild(path, content) // hash blob + deep insert
|
|
10
|
+
//! const treeHash = tree.write() // flush dirty subtrees → ODB
|
|
11
|
+
//! const commitHash = repo.commitTree(treeHash, [parentHash], message)
|
|
12
|
+
//! repo.updateRef(refname, commitHash)
|
|
13
|
+
//! ```
|
|
14
|
+
//!
|
|
15
|
+
//! Conventions across the FFI boundary:
|
|
16
|
+
//! - **Object ids** cross as lowercase hex `String` (matches gitsheets' existing
|
|
17
|
+
//! hash handling — it stores/compares hashes as hex strings everywhere).
|
|
18
|
+
//! - **Blob content** crosses as `Buffer` (binary-safe; records are TOML/text
|
|
19
|
+
//! but attachments are arbitrary bytes).
|
|
20
|
+
//!
|
|
21
|
+
//! This binding is a deliberately thin pass-through. Per the spike's governing
|
|
22
|
+
//! principle (see `plans/holo-tree-napi-spike.md` in gitsheets), rough edges in
|
|
23
|
+
//! holo-tree's API are recorded as `Phase-C finding` notes and fixed upstream —
|
|
24
|
+
//! not papered over with cleverness here.
|
|
25
|
+
|
|
26
|
+
use napi::bindgen_prelude::*;
|
|
27
|
+
use napi_derive::napi;
|
|
28
|
+
|
|
29
|
+
use holo_tree::repo as ht_repo;
|
|
30
|
+
use holo_tree::tree::empty_tree_id;
|
|
31
|
+
use holo_tree::{MutableTree, ObjectId};
|
|
32
|
+
|
|
33
|
+
// ── helpers ─────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
/// Map a holo-tree error into a JS exception.
|
|
36
|
+
///
|
|
37
|
+
/// Phase-C finding: `holo_tree::Error` collapses to a flat string here. gitsheets
|
|
38
|
+
/// needs to translate substrate failures into its typed error classes, which a
|
|
39
|
+
/// stringified `Display` makes lossy — candidate upstream improvement is a stable
|
|
40
|
+
/// error code / structured variant that survives FFI.
|
|
41
|
+
fn ht_err(e: holo_tree::Error) -> napi::Error {
|
|
42
|
+
napi::Error::from_reason(e.to_string())
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fn parse_oid(hex: &str) -> napi::Result<ObjectId> {
|
|
46
|
+
ObjectId::from_hex(hex.as_bytes())
|
|
47
|
+
.map_err(|e| napi::Error::from_reason(format!("invalid object id '{hex}': {e}")))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fn oid_hex(oid: ObjectId) -> String {
|
|
51
|
+
oid.to_hex().to_string()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// Git's well-known empty-tree hash (`4b825dc6…`).
|
|
55
|
+
#[napi]
|
|
56
|
+
pub fn empty_tree_hash() -> String {
|
|
57
|
+
oid_hex(empty_tree_id())
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// A commit identity (author or committer). `timeSeconds`/`offsetMinutes` are
|
|
61
|
+
/// optional; when omitted the current wall-clock time at UTC is used. Pass them
|
|
62
|
+
/// explicitly to reproduce a specific commit (e.g. match `git commit-tree`
|
|
63
|
+
/// under pinned `GIT_AUTHOR_DATE`/`GIT_COMMITTER_DATE`).
|
|
64
|
+
#[napi(object)]
|
|
65
|
+
pub struct Signature {
|
|
66
|
+
pub name: String,
|
|
67
|
+
pub email: String,
|
|
68
|
+
pub time_seconds: Option<i64>,
|
|
69
|
+
pub offset_minutes: Option<i32>,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fn now_secs() -> i64 {
|
|
73
|
+
std::time::SystemTime::now()
|
|
74
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
75
|
+
.map(|d| d.as_secs() as i64)
|
|
76
|
+
.unwrap_or(0)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// Render git's `"<seconds> ±HHMM"` signature-time format.
|
|
80
|
+
fn format_git_time(seconds: i64, offset_minutes: i32) -> String {
|
|
81
|
+
let sign = if offset_minutes < 0 { '-' } else { '+' };
|
|
82
|
+
let abs = offset_minutes.unsigned_abs();
|
|
83
|
+
format!("{seconds} {sign}{:02}{:02}", abs / 60, abs % 60)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fn to_gix_signature(sig: Signature) -> napi::Result<gix::actor::Signature> {
|
|
87
|
+
let seconds = sig.time_seconds.unwrap_or_else(now_secs);
|
|
88
|
+
let time = format_git_time(seconds, sig.offset_minutes.unwrap_or(0));
|
|
89
|
+
gix::actor::SignatureRef {
|
|
90
|
+
name: sig.name.as_str().into(),
|
|
91
|
+
email: sig.email.as_str().into(),
|
|
92
|
+
time: &time,
|
|
93
|
+
}
|
|
94
|
+
.to_owned()
|
|
95
|
+
.map_err(|e| napi::Error::from_reason(format!("invalid signature time: {e}")))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── Repo ────────────────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
/// A handle to a git repository, backed by gix.
|
|
101
|
+
///
|
|
102
|
+
/// Stored as a `ThreadSafeRepository` so the handle is `Send + Sync` and can be
|
|
103
|
+
/// cheaply cloned into each `Tree`; every call derives a thread-local
|
|
104
|
+
/// `gix::Repository` via `to_thread_local()`.
|
|
105
|
+
#[napi]
|
|
106
|
+
pub struct Repo {
|
|
107
|
+
inner: gix::ThreadSafeRepository,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
#[napi]
|
|
111
|
+
impl Repo {
|
|
112
|
+
/// Open a repository at `gitDir` (a `.git` directory, or any path gix can
|
|
113
|
+
/// discover a repo from).
|
|
114
|
+
#[napi(factory)]
|
|
115
|
+
pub fn open(git_dir: String) -> napi::Result<Repo> {
|
|
116
|
+
let inner = gix::open(&git_dir)
|
|
117
|
+
.map_err(|e| {
|
|
118
|
+
napi::Error::from_reason(format!("failed to open repo at '{git_dir}': {e}"))
|
|
119
|
+
})?
|
|
120
|
+
.into_sync();
|
|
121
|
+
Ok(Repo { inner })
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// Resolve a ref (branch, tag, or commit hash) to its tree and return a
|
|
125
|
+
/// mutable, in-memory view of it.
|
|
126
|
+
#[napi]
|
|
127
|
+
pub fn create_tree_from_ref(&self, git_ref: String) -> napi::Result<Tree> {
|
|
128
|
+
let local = self.inner.to_thread_local();
|
|
129
|
+
let inner = ht_repo::create_tree_from_ref(&local, &git_ref).map_err(ht_err)?;
|
|
130
|
+
Ok(Tree {
|
|
131
|
+
repo: self.inner.clone(),
|
|
132
|
+
inner,
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/// Create a fresh empty, mutable in-memory tree rooted at this repo.
|
|
137
|
+
#[napi]
|
|
138
|
+
pub fn create_tree(&self) -> Tree {
|
|
139
|
+
Tree {
|
|
140
|
+
repo: self.inner.clone(),
|
|
141
|
+
inner: MutableTree::empty(),
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// Write a commit object pointing at `treeHash` with `parents`. `author`
|
|
146
|
+
/// and `committer` are optional; each falls back to the repo's configured
|
|
147
|
+
/// identity, then a "holo-tree" default. Returns the new commit hash.
|
|
148
|
+
#[napi]
|
|
149
|
+
pub fn commit_tree(
|
|
150
|
+
&self,
|
|
151
|
+
tree_hash: String,
|
|
152
|
+
parents: Vec<String>,
|
|
153
|
+
message: String,
|
|
154
|
+
author: Option<Signature>,
|
|
155
|
+
committer: Option<Signature>,
|
|
156
|
+
) -> napi::Result<String> {
|
|
157
|
+
let local = self.inner.to_thread_local();
|
|
158
|
+
let tree = parse_oid(&tree_hash)?;
|
|
159
|
+
let parent_oids = parents
|
|
160
|
+
.iter()
|
|
161
|
+
.map(|p| parse_oid(p))
|
|
162
|
+
.collect::<napi::Result<Vec<_>>>()?;
|
|
163
|
+
let author = author.map(to_gix_signature).transpose()?;
|
|
164
|
+
let committer = committer.map(to_gix_signature).transpose()?;
|
|
165
|
+
let commit = ht_repo::commit_tree(&local, tree, &parent_oids, &message, author, committer)
|
|
166
|
+
.map_err(ht_err)?;
|
|
167
|
+
Ok(oid_hex(commit))
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/// Point a ref at an object hash.
|
|
171
|
+
///
|
|
172
|
+
/// When `expectedOldHash` is provided this is a **compare-and-swap**: the
|
|
173
|
+
/// update only succeeds if the ref currently resolves to exactly that hash,
|
|
174
|
+
/// so a concurrent writer who moved the ref makes the swap fail rather than
|
|
175
|
+
/// silently clobbering their commit. Omit it to force the ref (the prior
|
|
176
|
+
/// unconditional behavior).
|
|
177
|
+
#[napi]
|
|
178
|
+
pub fn update_ref(
|
|
179
|
+
&self,
|
|
180
|
+
refname: String,
|
|
181
|
+
hash: String,
|
|
182
|
+
expected_old_hash: Option<String>,
|
|
183
|
+
) -> napi::Result<()> {
|
|
184
|
+
let local = self.inner.to_thread_local();
|
|
185
|
+
let oid = parse_oid(&hash)?;
|
|
186
|
+
let expected = expected_old_hash.as_deref().map(parse_oid).transpose()?;
|
|
187
|
+
ht_repo::update_ref(&local, &refname, oid, expected).map_err(ht_err)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// Resolve a ref / rev-spec (branch, tag, `HEAD`, hash, …) to its commit
|
|
191
|
+
/// hash, peeling annotated tags. Returns `null` when the ref does not
|
|
192
|
+
/// resolve — the natural "does this ref exist?" probe before a CAS
|
|
193
|
+
/// `updateRef`.
|
|
194
|
+
#[napi]
|
|
195
|
+
pub fn resolve_ref(&self, git_ref: String) -> napi::Result<Option<String>> {
|
|
196
|
+
let local = self.inner.to_thread_local();
|
|
197
|
+
let oid = ht_repo::resolve_ref(&local, &git_ref).map_err(ht_err)?;
|
|
198
|
+
Ok(oid.map(oid_hex))
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/// Hash raw bytes as a loose blob in the ODB and return its hash, without
|
|
202
|
+
/// inserting it into any tree. Binary-safe.
|
|
203
|
+
#[napi]
|
|
204
|
+
pub fn write_blob(&self, content: Buffer) -> napi::Result<String> {
|
|
205
|
+
let local = self.inner.to_thread_local();
|
|
206
|
+
let oid = local
|
|
207
|
+
.write_blob(content.as_ref())
|
|
208
|
+
.map_err(|e| napi::Error::from_reason(e.to_string()))?
|
|
209
|
+
.detach();
|
|
210
|
+
Ok(oid_hex(oid))
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ── Tree read-models ─────────────────────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
/// A child entry returned by read-only navigation. `type` is `"tree"`,
|
|
217
|
+
/// `"blob"`, or `"commit"`; `mode` is the git filemode as a number
|
|
218
|
+
/// (e.g. `33188` = `0o100644`, `16384` = `0o040000` for a tree).
|
|
219
|
+
#[napi(object)]
|
|
220
|
+
pub struct ChildInfo {
|
|
221
|
+
pub r#type: String,
|
|
222
|
+
pub hash: String,
|
|
223
|
+
pub mode: u32,
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/// A named child entry, returned by `getChildren`.
|
|
227
|
+
#[napi(object)]
|
|
228
|
+
pub struct NamedChildInfo {
|
|
229
|
+
pub name: String,
|
|
230
|
+
pub r#type: String,
|
|
231
|
+
pub hash: String,
|
|
232
|
+
pub mode: u32,
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/// A blob entry in a flattened blob map, returned by `getBlobMap`. `path` is
|
|
236
|
+
/// relative to the navigated subtree.
|
|
237
|
+
#[napi(object)]
|
|
238
|
+
pub struct BlobEntry {
|
|
239
|
+
pub path: String,
|
|
240
|
+
pub hash: String,
|
|
241
|
+
pub mode: u32,
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/// Options for `Tree.merge`. `mode` is `"overlay"`, `"replace"`, or
|
|
245
|
+
/// `"underlay"`; `files` is an optional list of glob patterns restricting which
|
|
246
|
+
/// paths merge (omit to merge everything).
|
|
247
|
+
#[napi(object)]
|
|
248
|
+
pub struct MergeOpts {
|
|
249
|
+
pub files: Option<Vec<String>>,
|
|
250
|
+
pub mode: String,
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/// Classify a holo-tree `Child` into `(type, hash, mode)` for the read-models.
|
|
254
|
+
///
|
|
255
|
+
/// Note: for a `Tree` child the reported `hash` is the child's *stored* tree
|
|
256
|
+
/// hash, which is stale if that subtree has been mutated but not yet
|
|
257
|
+
/// `write()`-flushed. Read-only navigation on a freshly loaded/written tree
|
|
258
|
+
/// reports accurate hashes.
|
|
259
|
+
fn classify(child: &holo_tree::Child) -> (&'static str, String, u32) {
|
|
260
|
+
match child {
|
|
261
|
+
holo_tree::Child::Tree(t) => ("tree", oid_hex(t.hash), 0o040000),
|
|
262
|
+
holo_tree::Child::Blob { mode, hash } => ("blob", oid_hex(*hash), u32::from(*mode)),
|
|
263
|
+
holo_tree::Child::Commit { hash } => ("commit", oid_hex(*hash), 0o160000),
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── Tree ────────────────────────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
/// A mutable, in-memory git tree.
|
|
270
|
+
///
|
|
271
|
+
/// Holds its own clone of the repo handle so JS callers don't thread a repo
|
|
272
|
+
/// argument through every call.
|
|
273
|
+
///
|
|
274
|
+
/// Phase-C finding #1: holo-tree's `MutableTree` takes `&gix::Repository` on
|
|
275
|
+
/// nearly every method and keeps a *thread-local* tree cache. We smooth the
|
|
276
|
+
/// first half here (the handle lives on the `Tree`) but NOT the second: each
|
|
277
|
+
/// call does `to_thread_local()`, and whether holo-tree's thread-local cache
|
|
278
|
+
/// stays warm across libuv-dispatched calls is the open ergonomics question to
|
|
279
|
+
/// resolve upstream (e.g. a repo-bound tree handle, or an explicit session/
|
|
280
|
+
/// cache object the consumer owns).
|
|
281
|
+
#[napi]
|
|
282
|
+
pub struct Tree {
|
|
283
|
+
repo: gix::ThreadSafeRepository,
|
|
284
|
+
inner: MutableTree,
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
#[napi]
|
|
288
|
+
impl Tree {
|
|
289
|
+
/// Hash `content` (UTF-8 text) as a blob and insert it at `path`, creating
|
|
290
|
+
/// intermediate trees as needed. Returns the blob hash.
|
|
291
|
+
#[napi]
|
|
292
|
+
pub fn write_child(&mut self, path: String, content: String) -> napi::Result<String> {
|
|
293
|
+
let local = self.repo.to_thread_local();
|
|
294
|
+
let oid = self
|
|
295
|
+
.inner
|
|
296
|
+
.write_child(&local, &path, &content)
|
|
297
|
+
.map_err(ht_err)?;
|
|
298
|
+
Ok(oid_hex(oid))
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/// Hash raw bytes as a blob and insert at `path`. Binary-safe.
|
|
302
|
+
#[napi]
|
|
303
|
+
pub fn write_child_bytes(&mut self, path: String, content: Buffer) -> napi::Result<String> {
|
|
304
|
+
let local = self.repo.to_thread_local();
|
|
305
|
+
let oid = self
|
|
306
|
+
.inner
|
|
307
|
+
.write_child_bytes(&local, &path, content.as_ref())
|
|
308
|
+
.map_err(ht_err)?;
|
|
309
|
+
Ok(oid_hex(oid))
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/// Place an already-written blob at `path` by its `hash`, without reading its
|
|
313
|
+
/// bytes. Unlike `writeChildBytes` (which re-hashes content), this grafts a
|
|
314
|
+
/// blob already in the ODB — validated to exist and be a blob via a header
|
|
315
|
+
/// lookup, so a large attachment isn't read back and re-hashed. `mode` is the
|
|
316
|
+
/// git filemode: `0o100644` regular, `0o100755` executable, `0o120000`
|
|
317
|
+
/// symlink. Returns the placed hash.
|
|
318
|
+
#[napi]
|
|
319
|
+
pub fn write_child_hash(
|
|
320
|
+
&mut self,
|
|
321
|
+
path: String,
|
|
322
|
+
hash: String,
|
|
323
|
+
mode: u32,
|
|
324
|
+
) -> napi::Result<String> {
|
|
325
|
+
let local = self.repo.to_thread_local();
|
|
326
|
+
let oid = parse_oid(&hash)?;
|
|
327
|
+
let mode = u16::try_from(mode)
|
|
328
|
+
.map_err(|_| napi::Error::from_reason(format!("invalid blob mode {mode:o}")))?;
|
|
329
|
+
self.inner
|
|
330
|
+
.write_child_hash(&local, &path, oid, mode)
|
|
331
|
+
.map_err(ht_err)?;
|
|
332
|
+
Ok(oid_hex(oid))
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/// Read a blob's bytes at `path`, or `null` if no blob exists there.
|
|
336
|
+
#[napi]
|
|
337
|
+
pub fn read_blob(&mut self, path: String) -> napi::Result<Option<Buffer>> {
|
|
338
|
+
let local = self.repo.to_thread_local();
|
|
339
|
+
let bytes = self.inner.read_blob(&local, &path).map_err(ht_err)?;
|
|
340
|
+
Ok(bytes.map(Buffer::from))
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/// Read-only: look up the child at a deep `path` and report its type,
|
|
344
|
+
/// hash, and mode, or `null` if nothing exists there.
|
|
345
|
+
#[napi]
|
|
346
|
+
pub fn get_child(&mut self, path: String) -> napi::Result<Option<ChildInfo>> {
|
|
347
|
+
let local = self.repo.to_thread_local();
|
|
348
|
+
let info = self
|
|
349
|
+
.inner
|
|
350
|
+
.get_child(&local, &path)
|
|
351
|
+
.map_err(ht_err)?
|
|
352
|
+
.map(|child| {
|
|
353
|
+
let (ty, hash, mode) = classify(child);
|
|
354
|
+
ChildInfo {
|
|
355
|
+
r#type: ty.to_string(),
|
|
356
|
+
hash,
|
|
357
|
+
mode,
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
Ok(info)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/// Read-only: list the direct children of the subtree at `path` (use `"."`
|
|
364
|
+
/// for the root). Returns an empty array if `path` is missing or not a tree.
|
|
365
|
+
#[napi]
|
|
366
|
+
pub fn get_children(&mut self, path: String) -> napi::Result<Vec<NamedChildInfo>> {
|
|
367
|
+
let local = self.repo.to_thread_local();
|
|
368
|
+
let subtree = match self.inner.get_subtree(&local, &path).map_err(ht_err)? {
|
|
369
|
+
Some(t) => t,
|
|
370
|
+
None => return Ok(vec![]),
|
|
371
|
+
};
|
|
372
|
+
subtree.ensure_children(&local).map_err(ht_err)?;
|
|
373
|
+
let mut out = Vec::new();
|
|
374
|
+
for (name, child) in subtree.children.as_ref().unwrap().iter() {
|
|
375
|
+
let (ty, hash, mode) = classify(child);
|
|
376
|
+
out.push(NamedChildInfo {
|
|
377
|
+
name: name.clone(),
|
|
378
|
+
r#type: ty.to_string(),
|
|
379
|
+
hash,
|
|
380
|
+
mode,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
Ok(out)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/// Read-only: recursively collect every blob under the subtree at `path`
|
|
387
|
+
/// (defaults to the whole tree) into a flat list. Each `path` is relative
|
|
388
|
+
/// to the navigated subtree. Returns an empty array if `path` is missing.
|
|
389
|
+
#[napi]
|
|
390
|
+
pub fn get_blob_map(&mut self, path: Option<String>) -> napi::Result<Vec<BlobEntry>> {
|
|
391
|
+
let local = self.repo.to_thread_local();
|
|
392
|
+
let path = path.unwrap_or_else(|| ".".to_string());
|
|
393
|
+
let map = match self.inner.get_subtree(&local, &path).map_err(ht_err)? {
|
|
394
|
+
Some(subtree) => subtree.get_blob_map(&local).map_err(ht_err)?,
|
|
395
|
+
None => return Ok(vec![]),
|
|
396
|
+
};
|
|
397
|
+
let out = map
|
|
398
|
+
.into_iter()
|
|
399
|
+
.map(|(p, info)| BlobEntry {
|
|
400
|
+
path: p,
|
|
401
|
+
hash: oid_hex(info.hash),
|
|
402
|
+
mode: u32::from(info.mode),
|
|
403
|
+
})
|
|
404
|
+
.collect();
|
|
405
|
+
Ok(out)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/// Delete a child at a deep `path`. Returns whether it existed.
|
|
409
|
+
#[napi]
|
|
410
|
+
pub fn delete_child_deep(&mut self, path: String) -> napi::Result<bool> {
|
|
411
|
+
let local = self.repo.to_thread_local();
|
|
412
|
+
self.inner
|
|
413
|
+
.delete_child_deep(&local, &path)
|
|
414
|
+
.map_err(ht_err)
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/// Clear all children under a deep `path` in O(1) — replace the subtree
|
|
418
|
+
/// there with the empty tree (and dirty its ancestors) without loading the
|
|
419
|
+
/// cleared subtree's contents. `path == "."` clears the whole tree. Used to
|
|
420
|
+
/// wipe a directory before a full rewrite.
|
|
421
|
+
#[napi]
|
|
422
|
+
pub fn clear_children(&mut self, path: String) -> napi::Result<()> {
|
|
423
|
+
let local = self.repo.to_thread_local();
|
|
424
|
+
self.inner.clear_children(&local, &path).map_err(ht_err)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/// Merge another tree into this one in place, per `options.mode`
|
|
428
|
+
/// (`overlay`/`replace`/`underlay`) and optional `options.files` globs.
|
|
429
|
+
/// `other` must be a *different* `Tree` instance.
|
|
430
|
+
#[napi]
|
|
431
|
+
pub fn merge(&mut self, other: &mut Tree, options: MergeOpts) -> napi::Result<()> {
|
|
432
|
+
let local = self.repo.to_thread_local();
|
|
433
|
+
let mode = match options.mode.as_str() {
|
|
434
|
+
"overlay" => holo_tree::MergeMode::Overlay,
|
|
435
|
+
"replace" => holo_tree::MergeMode::Replace,
|
|
436
|
+
"underlay" => holo_tree::MergeMode::Underlay,
|
|
437
|
+
other => {
|
|
438
|
+
return Err(napi::Error::from_reason(format!(
|
|
439
|
+
"invalid merge mode '{other}', expected 'overlay', 'replace', or 'underlay'"
|
|
440
|
+
)))
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
let opts = holo_tree::MergeOptions::new(options.files.as_deref(), mode).map_err(ht_err)?;
|
|
444
|
+
self.inner
|
|
445
|
+
.merge(&local, &mut other.inner, &opts, ".")
|
|
446
|
+
.map_err(ht_err)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/// Flush dirty subtrees to the ODB and return the resulting tree hash.
|
|
450
|
+
#[napi]
|
|
451
|
+
pub fn write(&mut self) -> napi::Result<String> {
|
|
452
|
+
let local = self.repo.to_thread_local();
|
|
453
|
+
let oid = self.inner.write(&local).map_err(ht_err)?;
|
|
454
|
+
Ok(oid_hex(oid))
|
|
455
|
+
}
|
|
456
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hologit",
|
|
3
|
-
"version": "0.50.
|
|
3
|
+
"version": "0.50.3",
|
|
4
4
|
"description": "Hologit automates the projection of layered composite file trees based on flat, declarative plans",
|
|
5
5
|
"repository": "https://github.com/JarvusInnovations/hologit",
|
|
6
6
|
"main": "lib/index.js",
|