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 CHANGED
@@ -193,6 +193,15 @@ version = "1.0.5"
193
193
  source = "registry+https://github.com/rust-lang/crates.io-index"
194
194
  checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
195
195
 
196
+ [[package]]
197
+ name = "convert_case"
198
+ version = "0.6.0"
199
+ source = "registry+https://github.com/rust-lang/crates.io-index"
200
+ checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
201
+ dependencies = [
202
+ "unicode-segmentation",
203
+ ]
204
+
196
205
  [[package]]
197
206
  name = "cpufeatures"
198
207
  version = "0.2.17"
@@ -236,6 +245,16 @@ dependencies = [
236
245
  "typenum",
237
246
  ]
238
247
 
248
+ [[package]]
249
+ name = "ctor"
250
+ version = "0.2.9"
251
+ source = "registry+https://github.com/rust-lang/crates.io-index"
252
+ checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
253
+ dependencies = [
254
+ "quote",
255
+ "syn",
256
+ ]
257
+
239
258
  [[package]]
240
259
  name = "dashmap"
241
260
  version = "6.1.0"
@@ -427,7 +446,6 @@ dependencies = [
427
446
  "gix-submodule",
428
447
  "gix-tempfile",
429
448
  "gix-trace",
430
- "gix-transport",
431
449
  "gix-traverse",
432
450
  "gix-url",
433
451
  "gix-utils",
@@ -939,9 +957,7 @@ dependencies = [
939
957
  "gix-hashtable",
940
958
  "gix-object",
941
959
  "gix-path",
942
- "gix-tempfile",
943
960
  "memmap2",
944
- "parking_lot",
945
961
  "smallvec",
946
962
  "thiserror",
947
963
  "uluru",
@@ -1006,18 +1022,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
1006
1022
  checksum = "aa4bee82db63ec635996b96efae71cf467c155fa3f34a556184373224a26c4fd"
1007
1023
  dependencies = [
1008
1024
  "bstr",
1009
- "gix-credentials",
1010
1025
  "gix-date",
1011
1026
  "gix-features",
1012
1027
  "gix-hash",
1013
- "gix-lock",
1014
- "gix-negotiate",
1015
- "gix-object",
1016
1028
  "gix-ref",
1017
- "gix-refspec",
1018
- "gix-revwalk",
1019
1029
  "gix-shallow",
1020
- "gix-trace",
1021
1030
  "gix-transport",
1022
1031
  "gix-utils",
1023
1032
  "maybe-async",
@@ -1401,6 +1410,17 @@ dependencies = [
1401
1410
  "toml",
1402
1411
  ]
1403
1412
 
1413
+ [[package]]
1414
+ name = "holo-tree-napi"
1415
+ version = "0.1.0"
1416
+ dependencies = [
1417
+ "gix",
1418
+ "holo-tree",
1419
+ "napi",
1420
+ "napi-build",
1421
+ "napi-derive",
1422
+ ]
1423
+
1404
1424
  [[package]]
1405
1425
  name = "human_format"
1406
1426
  version = "1.2.1"
@@ -1611,6 +1631,16 @@ version = "0.2.183"
1611
1631
  source = "registry+https://github.com/rust-lang/crates.io-index"
1612
1632
  checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
1613
1633
 
1634
+ [[package]]
1635
+ name = "libloading"
1636
+ version = "0.8.9"
1637
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1638
+ checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
1639
+ dependencies = [
1640
+ "cfg-if",
1641
+ "windows-link",
1642
+ ]
1643
+
1614
1644
  [[package]]
1615
1645
  name = "libredox"
1616
1646
  version = "0.1.15"
@@ -1676,6 +1706,63 @@ dependencies = [
1676
1706
  "libc",
1677
1707
  ]
1678
1708
 
1709
+ [[package]]
1710
+ name = "napi"
1711
+ version = "2.16.17"
1712
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1713
+ checksum = "55740c4ae1d8696773c78fdafd5d0e5fe9bc9f1b071c7ba493ba5c413a9184f3"
1714
+ dependencies = [
1715
+ "bitflags",
1716
+ "ctor",
1717
+ "napi-derive",
1718
+ "napi-sys",
1719
+ "once_cell",
1720
+ ]
1721
+
1722
+ [[package]]
1723
+ name = "napi-build"
1724
+ version = "2.3.2"
1725
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1726
+ checksum = "c9c366d2c8c60b86fa632df75f745509b52f9128f91a6bad4c796e44abb505e1"
1727
+
1728
+ [[package]]
1729
+ name = "napi-derive"
1730
+ version = "2.16.13"
1731
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1732
+ checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c"
1733
+ dependencies = [
1734
+ "cfg-if",
1735
+ "convert_case",
1736
+ "napi-derive-backend",
1737
+ "proc-macro2",
1738
+ "quote",
1739
+ "syn",
1740
+ ]
1741
+
1742
+ [[package]]
1743
+ name = "napi-derive-backend"
1744
+ version = "1.0.75"
1745
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1746
+ checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf"
1747
+ dependencies = [
1748
+ "convert_case",
1749
+ "once_cell",
1750
+ "proc-macro2",
1751
+ "quote",
1752
+ "regex",
1753
+ "semver",
1754
+ "syn",
1755
+ ]
1756
+
1757
+ [[package]]
1758
+ name = "napi-sys"
1759
+ version = "2.4.0"
1760
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1761
+ checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3"
1762
+ dependencies = [
1763
+ "libloading",
1764
+ ]
1765
+
1679
1766
  [[package]]
1680
1767
  name = "nonempty"
1681
1768
  version = "0.12.0"
@@ -2165,6 +2252,12 @@ dependencies = [
2165
2252
  "tinyvec",
2166
2253
  ]
2167
2254
 
2255
+ [[package]]
2256
+ name = "unicode-segmentation"
2257
+ version = "1.13.3"
2258
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2259
+ checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
2260
+
2168
2261
  [[package]]
2169
2262
  name = "unicode-xid"
2170
2263
  version = "0.2.6"
package/Cargo.toml CHANGED
@@ -1,3 +1,3 @@
1
1
  [workspace]
2
- members = ["holo-tree", "holo-projector"]
2
+ members = ["holo-tree", "holo-projector", "holo-tree-napi"]
3
3
  resolver = "2"
@@ -6,7 +6,10 @@ description = "Mutable in-memory git trees — read, merge, write via gix"
6
6
  license = "MIT"
7
7
 
8
8
  [dependencies]
9
- gix = { version = "0.83", features = ["blocking-network-client"] }
9
+ # Tree/object manipulation only no networking, so gix defaults suffice
10
+ # (drops the TLS/OpenSSL pull-in of `blocking-network-client`, easing
11
+ # cross-compilation for the holo-tree-napi binding).
12
+ gix = { version = "0.83" }
10
13
  globset = "0.4"
11
14
  toml = "0.8"
12
15
  serde = { version = "1", features = ["derive"] }
@@ -18,6 +18,24 @@ pub enum Error {
18
18
  Toml { path: String, message: String },
19
19
  }
20
20
 
21
+ impl Error {
22
+ /// A stable, machine-matchable code for this error variant.
23
+ ///
24
+ /// Unlike the human-readable `Display` string (which embeds variable
25
+ /// context), these codes are part of the API contract: downstream
26
+ /// consumers — notably the napi binding and gitsheets — match on them to
27
+ /// map substrate failures onto their own typed errors. Keep them stable.
28
+ pub fn code(&self) -> &'static str {
29
+ match self {
30
+ Error::Git(_) => "GIT",
31
+ Error::NotATree(_) => "NOT_A_TREE",
32
+ Error::PathNotFound { .. } => "PATH_NOT_FOUND",
33
+ Error::Glob(_) => "GLOB",
34
+ Error::Toml { .. } => "TOML",
35
+ }
36
+ }
37
+ }
38
+
21
39
  pub type Result<T> = std::result::Result<T, Error>;
22
40
 
23
41
  // ── gix error conversions ──────────────────────────────────────────────────
@@ -45,3 +63,29 @@ impl From<gix::reference::find::existing::Error> for Error {
45
63
  Error::Git(e.to_string())
46
64
  }
47
65
  }
66
+
67
+ #[cfg(test)]
68
+ mod tests {
69
+ use super::Error;
70
+
71
+ #[test]
72
+ fn codes_are_stable_per_variant() {
73
+ assert_eq!(Error::Git("x".into()).code(), "GIT");
74
+ assert_eq!(Error::NotATree("x".into()).code(), "NOT_A_TREE");
75
+ assert_eq!(
76
+ Error::PathNotFound {
77
+ component: "x".into()
78
+ }
79
+ .code(),
80
+ "PATH_NOT_FOUND"
81
+ );
82
+ assert_eq!(
83
+ Error::Toml {
84
+ path: "p".into(),
85
+ message: "m".into()
86
+ }
87
+ .code(),
88
+ "TOML"
89
+ );
90
+ }
91
+ }
@@ -81,33 +81,41 @@ pub fn create_tree_from_path(
81
81
 
82
82
  /// Create a git commit pointing to a tree.
83
83
  ///
84
- /// Uses the repository's configured author/committer identity (from
85
- /// git config or `GIT_AUTHOR_NAME`/`GIT_COMMITTER_NAME` env vars).
86
- /// Falls back to "holo-tree" if no identity is configured.
84
+ /// Identity resolution, per field, is: explicit `author`/`committer` argument
85
+ /// the repository's configured identity (git config or `GIT_AUTHOR_*`/
86
+ /// `GIT_COMMITTER_*` env) a "holo-tree" fallback. Passing explicit signatures
87
+ /// (with timestamps) is what lets an embedding consumer reproduce a specific
88
+ /// commit bit-for-bit — e.g. match `git commit-tree` under pinned dates.
87
89
  pub fn commit_tree(
88
90
  repo: &gix::Repository,
89
91
  tree_hash: ObjectId,
90
92
  parents: &[ObjectId],
91
93
  message: &str,
94
+ author: Option<gix::actor::Signature>,
95
+ committer: Option<gix::actor::Signature>,
92
96
  ) -> Result<ObjectId> {
93
97
  use gix::objs::Commit;
94
98
 
95
- let author = repo
96
- .author()
97
- .and_then(|r| r.ok())
98
- .map(|s| s.to_owned())
99
- .transpose()
100
- .ok()
101
- .flatten()
99
+ let author = author
100
+ .or_else(|| {
101
+ repo.author()
102
+ .and_then(|r| r.ok())
103
+ .map(|s| s.to_owned())
104
+ .transpose()
105
+ .ok()
106
+ .flatten()
107
+ })
102
108
  .unwrap_or_else(default_signature);
103
109
 
104
- let committer = repo
105
- .committer()
106
- .and_then(|r| r.ok())
107
- .map(|s| s.to_owned())
108
- .transpose()
109
- .ok()
110
- .flatten()
110
+ let committer = committer
111
+ .or_else(|| {
112
+ repo.committer()
113
+ .and_then(|r| r.ok())
114
+ .map(|s| s.to_owned())
115
+ .transpose()
116
+ .ok()
117
+ .flatten()
118
+ })
111
119
  .unwrap_or_else(default_signature);
112
120
 
113
121
  let commit = Commit {
@@ -126,22 +134,131 @@ pub fn commit_tree(
126
134
  Ok(id.detach())
127
135
  }
128
136
 
137
+ /// Resolve a ref / rev-spec (branch, tag, `HEAD`, hash, …) to its object hash,
138
+ /// peeling annotated tags down to the object they point at (typically a commit).
139
+ ///
140
+ /// Returns `Ok(None)` when the ref does not resolve — an unknown name, an
141
+ /// unborn branch, or any spec gix can't parse to a single object. That is the
142
+ /// natural "does this ref exist?" contract a caller wants from a resolver
143
+ /// (gitsheets uses it to discover the current commit before a compare-and-swap
144
+ /// `update_ref`). Genuine ODB failures *after* a spec resolves still surface as
145
+ /// `Err`.
146
+ pub fn resolve_ref(repo: &gix::Repository, git_ref: &str) -> Result<Option<ObjectId>> {
147
+ let spec = match repo.rev_parse_single(git_ref) {
148
+ Ok(s) => s,
149
+ Err(_) => return Ok(None),
150
+ };
151
+ let mut obj = spec.object().map_err(|e| Error::Git(e.to_string()))?;
152
+
153
+ // Peel annotated tags to their target object.
154
+ while obj.kind == gix::object::Kind::Tag {
155
+ let tag = obj
156
+ .try_into_tag()
157
+ .map_err(|_| Error::Git("failed to parse tag".into()))?;
158
+ let target = tag
159
+ .target_id()
160
+ .map_err(|e| Error::Git(e.to_string()))?
161
+ .detach();
162
+ obj = repo.find_object(target)?;
163
+ }
164
+
165
+ Ok(Some(obj.id))
166
+ }
167
+
129
168
  /// Update a git ref to point at a new object.
169
+ ///
170
+ /// Accepts the same leniency as `git update-ref`: a bare branch name (e.g.
171
+ /// `main`) is qualified to `refs/heads/main`. Already-qualified names (anything
172
+ /// containing `/`, like `refs/heads/x` or `refs/tags/x`) and all-caps pseudo-refs
173
+ /// (e.g. `HEAD`) pass through unchanged. Without this, gix's `reference()`
174
+ /// rejects a standalone lowercase name ("Standalone references must be all
175
+ /// uppercased").
176
+ ///
177
+ /// When `expected_old` is `Some`, this is a **compare-and-swap**: the update
178
+ /// only succeeds if the ref currently resolves to exactly that object
179
+ /// (`PreviousValue::MustExistAndMatch`), so a concurrent writer that moved the
180
+ /// ref out from under the caller makes the swap fail loudly rather than clobber
181
+ /// their commit. When `None`, the ref is set unconditionally
182
+ /// (`PreviousValue::Any`), matching the original force behavior.
183
+ ///
184
+ /// The reflog identity is derived from the **committer of the commit the ref now
185
+ /// points at**, falling back to a stable `holo-tree` default for non-commit
186
+ /// targets or unreadable commits. It never reads ambient git config
187
+ /// (`user.name` / `user.email`). gix's convenience `reference()` does reach for
188
+ /// ambient config to stamp the reflog, and fails with "The reflog could not be
189
+ /// created or updated" when none is set — so an embedding consumer that supplies
190
+ /// a fully-specified commit (matching [`commit_tree`]'s explicit-identity
191
+ /// contract) could still see the *ref update* fail on an unconfigured runner.
192
+ /// Sourcing the identity from the commit itself keeps the whole operation
193
+ /// independent of machine config.
130
194
  pub fn update_ref(
131
195
  repo: &gix::Repository,
132
196
  refname: &str,
133
197
  target: ObjectId,
198
+ expected_old: Option<ObjectId>,
134
199
  ) -> Result<()> {
135
- repo.reference(
136
- refname,
137
- target,
138
- gix::refs::transaction::PreviousValue::Any,
139
- "holo-tree",
140
- )
141
- .map_err(|e| Error::Git(e.to_string()))?;
200
+ use gix::refs::transaction::{Change, LogChange, RefEdit, RefLog};
201
+
202
+ let qualified = qualify_ref(refname);
203
+ let previous = match expected_old {
204
+ Some(old) => gix::refs::transaction::PreviousValue::MustExistAndMatch(
205
+ gix::refs::Target::Object(old),
206
+ ),
207
+ None => gix::refs::transaction::PreviousValue::Any,
208
+ };
209
+
210
+ let name: gix::refs::FullName = qualified
211
+ .as_ref()
212
+ .try_into()
213
+ .map_err(|e: gix::refs::name::Error| Error::Git(e.to_string()))?;
214
+
215
+ // Reflog identity: the target commit's committer, else a stable default.
216
+ // Read the commit up front so its data outlives the borrowed `SignatureRef`
217
+ // we hand to the transaction below.
218
+ let commit = repo
219
+ .find_object(target)
220
+ .ok()
221
+ .and_then(|obj| obj.try_into_commit().ok());
222
+ let fallback = default_signature();
223
+ let mut time_buf = gix::date::parse::TimeBuf::default();
224
+ let committer = commit
225
+ .as_ref()
226
+ .and_then(|c| c.committer().ok())
227
+ .unwrap_or_else(|| fallback.to_ref(&mut time_buf));
228
+
229
+ let edit = RefEdit {
230
+ change: Change::Update {
231
+ log: LogChange {
232
+ mode: RefLog::AndReference,
233
+ force_create_reflog: false,
234
+ message: "holo-tree".into(),
235
+ },
236
+ expected: previous,
237
+ new: gix::refs::Target::Object(target),
238
+ },
239
+ name,
240
+ deref: false,
241
+ };
242
+
243
+ repo.edit_references_as(Some(edit), Some(committer))
244
+ .map_err(|e| Error::Git(e.to_string()))?;
142
245
  Ok(())
143
246
  }
144
247
 
248
+ /// Map a bare branch name to a fully-qualified ref, matching `git update-ref`'s
249
+ /// leniency. See [`update_ref`].
250
+ fn qualify_ref(refname: &str) -> std::borrow::Cow<'_, str> {
251
+ let is_pseudo_ref = !refname.is_empty()
252
+ && refname
253
+ .bytes()
254
+ .all(|b| b.is_ascii_uppercase() || b == b'_');
255
+ if refname.contains('/') || is_pseudo_ref {
256
+ std::borrow::Cow::Borrowed(refname)
257
+ } else {
258
+ std::borrow::Cow::Owned(format!("refs/heads/{refname}"))
259
+ }
260
+ }
261
+
145
262
  /// Fallback signature when git config has no author/committer.
146
263
  fn default_signature() -> gix::actor::Signature {
147
264
  gix::actor::SignatureRef {
@@ -158,3 +275,22 @@ fn default_signature() -> gix::actor::Signature {
158
275
  .to_owned()
159
276
  .expect("valid fallback signature")
160
277
  }
278
+
279
+ #[cfg(test)]
280
+ mod tests {
281
+ use super::qualify_ref;
282
+
283
+ #[test]
284
+ fn qualifies_bare_branch_names() {
285
+ assert_eq!(qualify_ref("main"), "refs/heads/main");
286
+ assert_eq!(qualify_ref("feature-x"), "refs/heads/feature-x");
287
+ }
288
+
289
+ #[test]
290
+ fn passes_through_qualified_and_pseudo_refs() {
291
+ assert_eq!(qualify_ref("refs/heads/main"), "refs/heads/main");
292
+ assert_eq!(qualify_ref("refs/tags/v1"), "refs/tags/v1");
293
+ assert_eq!(qualify_ref("HEAD"), "HEAD");
294
+ assert_eq!(qualify_ref("FETCH_HEAD"), "FETCH_HEAD");
295
+ }
296
+ }