From 02d76ed48353d16d1678b7743e4fbc1cc645f803 Mon Sep 17 00:00:00 2001 From: Christian Schilling Date: Tue, 25 Aug 2026 19:43:50 +0200 Subject: [PATCH 1/5] Remove obsolete Oid wrapper The migration left josh_core::Oid reachable only through an unused proxy Ref type. Remove both instead of mechanically porting the wrapper and its git2 conversion surface to gitoxide. Change: gix-oid-wrapper-removal Assisted-By: openai-codex/gpt-5.6-sol --- josh-core/src/lib.rs | 37 ------------------------------------- josh-proxy/src/lib.rs | 6 ------ 2 files changed, 43 deletions(-) diff --git a/josh-core/src/lib.rs b/josh-core/src/lib.rs index b4e0c8415..546726918 100644 --- a/josh-core/src/lib.rs +++ b/josh-core/src/lib.rs @@ -26,43 +26,6 @@ pub mod trailers; pub use josh_gix_ext as objects; pub use josh_memodb as memodb; -#[derive( - Clone, Hash, PartialEq, Eq, Copy, PartialOrd, Ord, Debug, serde::Serialize, serde::Deserialize, -)] -#[serde(try_from = "String", into = "String")] -pub struct Oid(git2::Oid); - -impl Default for Oid { - fn default() -> Self { - Oid(git2::Oid::ZERO_SHA1) - } -} - -impl std::convert::TryFrom for Oid { - type Error = anyhow::Error; - fn try_from(s: String) -> anyhow::Result { - Ok(Oid(git2::Oid::from_str(&s)?)) - } -} - -impl From for String { - fn from(val: Oid) -> Self { - val.0.to_string() - } -} - -impl From for git2::Oid { - fn from(val: Oid) -> Self { - val.0 - } -} - -impl From for Oid { - fn from(oid: git2::Oid) -> Self { - Self(oid) - } -} - /// Determine the josh version number with the following precedence: /// /// 1. If in a git checkout, and `git` binary is present, use the diff --git a/josh-proxy/src/lib.rs b/josh-proxy/src/lib.rs index 7a5f9627d..d73d6636a 100644 --- a/josh-proxy/src/lib.rs +++ b/josh-proxy/src/lib.rs @@ -16,7 +16,6 @@ pub(crate) const MAX_MEM_PACK_SIZE: usize = 128 * 1024 * 1024; use crate::http::{IntoRetryable, RetryableError}; use crate::upstream::RemoteAuth; -use josh_core; josh_core::regex_parsed!( FilteredRepoUrl, @@ -24,11 +23,6 @@ josh_core::regex_parsed!( [api, upstream_repo, filter_spec, pathinfo, headref, rest] ); -#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Default)] -pub struct Ref { - pub target: josh_core::Oid, -} - fn make_ssh_command() -> String { let ssh_options = [ "LogLevel=ERROR", From 15646ed5160bb99259d2fce45869f7b4600e906c Mon Sep 17 00:00:00 2001 From: Christian Schilling Date: Tue, 25 Aug 2026 19:58:34 +0200 Subject: [PATCH 2/5] Remove git2 from josh-memodb Make the object facade's inherent API use gitoxide ObjectId throughout, and keep git2 conversions at the still-legacy caller seams. Packing and flushing now return pure Rust errors, while the object directory comes from the gitoxide store. git2 and josh-gix-ext remain dev-only test dependencies. Change: gix-pure-memodb Assisted-By: openai-codex/gpt-5.6-sol --- josh-core/src/cache/distributed.rs | 2 +- josh-core/src/cache/history_graph.rs | 8 ++- josh-core/src/cache/transaction.rs | 21 ++++--- josh-core/src/filter/mod.rs | 29 +++++---- josh-core/src/filter/tree.rs | 18 ++++-- josh-core/src/git.rs | 6 +- josh-core/src/history.rs | 4 +- josh-graphql/src/graphql.rs | 2 +- josh-gui/Cargo.lock | 2 - josh-memodb/Cargo.toml | 6 +- josh-memodb/src/flusher.rs | 12 ++-- josh-memodb/src/lib.rs | 1 - josh-memodb/src/mem_odb.rs | 4 +- josh-memodb/src/odb.rs | 93 ++++++++++++++-------------- josh-memodb/src/pack.rs | 48 +++++++------- 15 files changed, 136 insertions(+), 120 deletions(-) diff --git a/josh-core/src/cache/distributed.rs b/josh-core/src/cache/distributed.rs index e5bbd9441..8076280fa 100644 --- a/josh-core/src/cache/distributed.rs +++ b/josh-core/src/cache/distributed.rs @@ -60,7 +60,7 @@ impl DistributedCacheBackend { fn open(repo_path: impl AsRef, writable: bool) -> anyhow::Result { let repo = git2::Repository::open(repo_path.as_ref())?; - let objects_dir = josh_memodb::objects_dir(&repo); + let objects_dir = repo.commondir().join("objects"); let mem_odb = josh_memodb::MemOdb::new(None, objects_dir.clone()); let odb = josh_memodb::Odb::at(mem_odb.clone(), &objects_dir)?; Ok(Self { diff --git a/josh-core/src/cache/history_graph.rs b/josh-core/src/cache/history_graph.rs index 066c5e4e0..4296d9e63 100644 --- a/josh-core/src/cache/history_graph.rs +++ b/josh-core/src/cache/history_graph.rs @@ -129,7 +129,7 @@ fn ensure_hint_cached( } let odb = transaction.odb(); - if !odb.contains(input) { + if !odb.contains(crate::objects::gix_oid(input)) { return Err(anyhow!("ensure_hint_cached: input does not exist")); } @@ -283,11 +283,13 @@ fn write_roots_blob(odb: &josh_memodb::Odb, roots: &[git2::Oid]) -> anyhow::Resu for r in roots { bytes.extend_from_slice(r.as_bytes()); } - Ok(odb.write(gix_object::Kind::Blob, &bytes)) + Ok(crate::objects::git2_oid( + &odb.write(gix_object::Kind::Blob, &bytes), + )) } fn read_roots_blob(odb: &josh_memodb::Odb, oid: git2::Oid) -> anyhow::Result> { - let (kind, content) = odb.read(oid)?; + let (kind, content) = odb.read(crate::objects::gix_oid(oid))?; if kind != gix_object::Kind::Blob { return Err(anyhow!("reachable_roots object {} is not a blob", oid)); } diff --git a/josh-core/src/cache/transaction.rs b/josh-core/src/cache/transaction.rs index 043d9f570..c5c4734ae 100644 --- a/josh-core/src/cache/transaction.rs +++ b/josh-core/src/cache/transaction.rs @@ -346,7 +346,7 @@ impl Transaction { // An ephemeral transaction must not contribute to a store that outlives it, so it // buffers privately and reads through to the shared one. - let objects_dir = josh_memodb::objects_dir(&repo); + let objects_dir = gix_repo.objects.path().to_owned(); let shared = josh_memodb::registry::shared(mem_odb_limit, &objects_dir); let mem_odb = if ephemeral { josh_memodb::MemOdb::chained(mem_odb_limit, objects_dir, shared) @@ -451,7 +451,7 @@ impl Transaction { if let Some(bytes) = self.t2.borrow().tree_cache.get(oid) { return Ok(Some(TreeBytes::Cached(bytes))); } - let (kind, bytes) = odb.read(oid)?; + let (kind, bytes) = odb.read(crate::objects::gix_oid(oid))?; if kind != gix_object::Kind::Tree { return Ok(None); } @@ -1224,7 +1224,7 @@ impl Transaction { let oid = t2.cache.read_propagate(filter, tree, hint, true).ok()??; // Per-subtree index trees are anchored by no ref, so gc may have pruned a // cached one; treat a dangling hit as a miss and reindex. - if self.odb().contains(oid) { + if self.odb().contains(crate::objects::gix_oid(oid)) { Some(oid) } else { None @@ -1259,7 +1259,7 @@ impl Transaction { pub fn get_ref(&self, filter: crate::filter::Filter, from: git2::Oid) -> Option { if let Some(m) = REF_CACHE.read().unwrap().get(&filter.id()) && let Some(oid) = m.get(&from) - && self.odb().contains(*oid) + && self.odb().contains(crate::objects::gix_oid(*oid)) { return Some(*oid); } @@ -1398,7 +1398,7 @@ impl Transaction { return Ok(Some(oid)); } - if self.odb().contains(oid) { + if self.odb().contains(crate::objects::gix_oid(oid)) { // Only report an object as cached if it exists in the object database. // This forces a rebuild in case the object was garbage collected. return Ok(Some(oid)); @@ -2193,9 +2193,11 @@ mod tests { let oid = { let transaction = context.open().unwrap(); - let oid = transaction - .odb() - .write(gix_object::Kind::Blob, b"published"); + let oid = crate::objects::git2_oid( + &transaction + .odb() + .write(gix_object::Kind::Blob, b"published"), + ); transaction .update_ref("refs/josh/blob", Expected::Absent, oid, "test") .unwrap(); @@ -2224,7 +2226,8 @@ mod tests { fn flush_mem_odb_publishes_pending_refs_after_objects() { let (dir, context) = test_context(); let transaction = context.open().unwrap(); - let oid = transaction.odb().write(gix_object::Kind::Blob, b"boundary"); + let oid = + crate::objects::git2_oid(&transaction.odb().write(gix_object::Kind::Blob, b"boundary")); transaction .update_ref("refs/josh/blob", Expected::Absent, oid, "test") .unwrap(); diff --git a/josh-core/src/filter/mod.rs b/josh-core/src/filter/mod.rs index 580df6d1c..b02dec3db 100644 --- a/josh-core/src/filter/mod.rs +++ b/josh-core/src/filter/mod.rs @@ -548,7 +548,9 @@ fn get_rev_filter( } else { return Err(anyhow!("unresolved lazy ref")); }; - if match_op != &RevMatch::Default && !transaction.odb().contains(*filter_tip) { + if match_op != &RevMatch::Default + && !transaction.odb().contains(objects::gix_oid(*filter_tip)) + { return Err(anyhow!("`:rev(...)` with nonexistent OID: {}", filter_tip)); } let matches = match match_op { @@ -614,7 +616,7 @@ pub fn apply_to_commit2( Op::Squash(None) => { let odb = transaction.odb(); let commit = objects::CommitData::read(odb, commit_id)?; - odb.read_header(commit.tree_id()?)?; + odb.read_header(objects::gix_oid(commit.tree_id()?))?; return Some(history::rewrite_commit( odb, &commit, @@ -649,7 +651,7 @@ pub fn apply_to_commit2( // no parse). Without this gate, an apply over a partially unreadable input can buffer // fresh objects into the store before a later read aborts the walk, and the partial // write would change the next flush's pack. - odb.read_header(commit.tree_id()?)?; + odb.read_header(objects::gix_oid(commit.tree_id()?))?; let rewrite_data = match &op { Op::Squash(Some(ids)) => { @@ -1344,7 +1346,9 @@ fn apply_impl( odb, result_tree, &submodule_path.join(".link.josh"), - odb.write(gix_object::Kind::Blob, link_content.as_bytes()), + objects::git2_oid( + &odb.write(gix_object::Kind::Blob, link_content.as_bytes()), + ), 0o0100644, )?; } @@ -1388,7 +1392,7 @@ fn apply_impl( odb, result_tree, &link_path.join(".link.josh"), - odb.write(gix_object::Kind::Blob, link_content.as_bytes()), + objects::git2_oid(&odb.write(gix_object::Kind::Blob, link_content.as_bytes())), 0o0100644, )?; } @@ -1434,7 +1438,7 @@ fn apply_impl( odb, result_tree, &root.join(".link.josh"), - odb.write(gix_object::Kind::Blob, link_content.as_bytes()), + objects::git2_oid(&odb.write(gix_object::Kind::Blob, link_content.as_bytes())), 0o0100644, )?; } @@ -1478,13 +1482,13 @@ fn apply_impl( Op::Insert(dest_path, content) => { let (oid, mode, is_tree) = match content { InsertContent::Inline(s) => ( - odb.write(gix_object::Kind::Blob, s.as_bytes()), + objects::git2_oid(&odb.write(gix_object::Kind::Blob, s.as_bytes())), git2::FileMode::Blob.into(), false, ), // The kind comes from the header alone; a missing oid folds into the // "neither" arm below. - InsertContent::Oid(oid) => match odb.try_kind(*oid) { + InsertContent::Oid(oid) => match odb.try_kind(objects::gix_oid(*oid)) { Ok(Some(gix_object::Kind::Blob)) => (*oid, git2::FileMode::Blob.into(), false), Ok(Some(gix_object::Kind::Tree)) => (*oid, git2::FileMode::Tree.into(), true), _ => { @@ -1622,7 +1626,8 @@ fn apply_impl( Op::ObjectRef(path) => { if let Ok(Some(entry)) = tree::get_path_entry(transaction, odb, x.tree_id(), path) { let oid_str = objects::git2_oid(&entry.oid).to_string(); - let blob_oid = odb.write(gix_object::Kind::Blob, oid_str.as_bytes()); + let blob_oid = + objects::git2_oid(&odb.write(gix_object::Kind::Blob, oid_str.as_bytes())); Ok(x.with_tree(tree::insert_oid( odb, tree::empty_id(), @@ -1654,7 +1659,7 @@ fn apply_impl( if let Ok(oid) = git2::Oid::from_str(&oid_str) { // Kind by header, never by `contains`: `read_header`'s disk fallback // virtualizes the empty tree, `exists` does not. - let (oid, mode) = match odb.try_kind(oid) { + let (oid, mode) = match odb.try_kind(objects::gix_oid(oid)) { Ok(Some(gix_object::Kind::Tree)) => (oid, git2::FileMode::Tree.into()), Ok(Some(gix_object::Kind::Blob)) => (oid, git2::FileMode::Blob.into()), _ => { @@ -1664,7 +1669,7 @@ fn apply_impl( Ok(x.with_tree(tree::insert_oid(odb, tree::empty_id(), path, oid, mode)?)) } else { // Content is not a valid OID: insert empty blob at path. - let empty_blob = odb.write(gix_object::Kind::Blob, b""); + let empty_blob = objects::git2_oid(&odb.write(gix_object::Kind::Blob, b"")); Ok(x.with_tree(tree::insert_oid( odb, tree::empty_id(), @@ -1995,7 +2000,7 @@ fn pre_process_tree( odb, tree, path, - odb.write(gix_object::Kind::Blob, blob.as_bytes()), + objects::git2_oid(&odb.write(gix_object::Kind::Blob, blob.as_bytes())), git2::FileMode::Blob.into(), // Should this handle filemode? )?; diff --git a/josh-core/src/filter/tree.rs b/josh-core/src/filter/tree.rs index 66de47b5f..46fe310a5 100644 --- a/josh-core/src/filter/tree.rs +++ b/josh-core/src/filter/tree.rs @@ -61,7 +61,7 @@ fn pathstree_inner( rebuild.keep(gix_object::tree::Entry { mode: gix_object::tree::EntryKind::Blob.into(), filename: entry.filename.to_owned(), - oid: objects::gix_oid(odb.write(gix_object::Kind::Blob, file_contents.as_bytes())), + oid: odb.write(gix_object::Kind::Blob, file_contents.as_bytes()), }); } } @@ -120,7 +120,7 @@ fn regex_replace_inner( rebuild.keep(gix_object::tree::Entry { mode: entry.mode, filename: entry.filename.to_owned(), - oid: objects::gix_oid(odb.write(gix_object::Kind::Blob, replaced.as_bytes())), + oid: odb.write(gix_object::Kind::Blob, replaced.as_bytes()), }); } } @@ -130,7 +130,7 @@ fn regex_replace_inner( /// The raw bytes of the blob `oid`, or `None` when the object is missing or not a blob -- /// `find_blob`'s tolerance, in facade currency. pub fn blob_bytes(odb: &josh_memodb::Odb, oid: git2::Oid) -> Option { - match odb.read(oid) { + match odb.read(objects::gix_oid(oid)) { Ok((gix_object::Kind::Blob, bytes)) => Some(bytes), _ => None, } @@ -1135,7 +1135,7 @@ pub fn invert_paths( odb, result, Path::new(&opath), - odb.write(gix_object::Kind::Blob, mpath.as_bytes()), + objects::git2_oid(&odb.write(gix_object::Kind::Blob, mpath.as_bytes())), 0o0100644, ) .unwrap(); @@ -1200,8 +1200,14 @@ pub fn populate( } use gix_object::Kind; - let paths_kind = odb.read_header(paths).map(|(kind, _)| kind).ok(); - let content_kind = odb.read_header(content).map(|(kind, _)| kind).ok(); + let paths_kind = odb + .read_header(objects::gix_oid(paths)) + .map(|(kind, _)| kind) + .ok(); + let content_kind = odb + .read_header(objects::gix_oid(content)) + .map(|(kind, _)| kind) + .ok(); let mut result_tree = empty_id(); if let (Some(Kind::Blob), Some(Kind::Blob)) = (paths_kind, content_kind) { diff --git a/josh-core/src/git.rs b/josh-core/src/git.rs index 22c54f0cc..cd4ac9d8d 100644 --- a/josh-core/src/git.rs +++ b/josh-core/src/git.rs @@ -248,7 +248,7 @@ impl GitCommand { /// lock-guarded global that also decodes author/committer/message) whenever a caller only /// needs the parent ids; memory-store hits are zero-copy. pub fn read_parent_ids(odb: &josh_memodb::Odb, oid: git2::Oid) -> anyhow::Result> { - let (kind, bytes) = odb.read(oid)?; + let (kind, bytes) = odb.read(crate::objects::gix_oid(oid))?; // A hard error, not an assert: this is reachable from inside git2 callback frames, // where unwinding across the FFI boundary would abort. if kind != gix_object::Kind::Commit { @@ -267,7 +267,7 @@ pub fn read_parent_ids(odb: &josh_memodb::Odb, oid: git2::Oid) -> anyhow::Result /// Sibling of [`read_parent_ids`]: read a commit's tree OID without touching libgit2's /// commit parse cache. pub fn read_tree_id(odb: &josh_memodb::Odb, oid: git2::Oid) -> anyhow::Result { - let (kind, bytes) = odb.read(oid)?; + let (kind, bytes) = odb.read(crate::objects::gix_oid(oid))?; // Same hard-error rationale as read_parent_ids. if kind != gix_object::Kind::Commit { return Err(anyhow::anyhow!( @@ -291,7 +291,7 @@ mod tests { let tree = repo.find_tree(tree_id).unwrap(); let commit_id = repo.commit(None, &sig, &sig, "test", &tree, &[]).unwrap(); - let objects_dir = josh_memodb::objects_dir(&repo); + let objects_dir = repo.commondir().join("objects"); let store = josh_memodb::MemOdb::new(None, objects_dir.clone()); let odb = josh_memodb::Odb::at(store, &objects_dir).unwrap(); assert_eq!( diff --git a/josh-core/src/history.rs b/josh-core/src/history.rs index 5efed2e2b..f72591bcf 100644 --- a/josh-core/src/history.rs +++ b/josh-core/src/history.rs @@ -292,7 +292,7 @@ pub fn rewrite_commit( let mut b = vec![]; gix_object::WriteTo::write_to(&commit, &mut b)?; - Ok(odb.write(gix_object::Kind::Commit, &b)) + Ok(objects::git2_oid(&odb.write(gix_object::Kind::Commit, &b))) } // Given an OID of an unfiltered commit and a filter, @@ -426,7 +426,7 @@ pub fn unapply_filter( // The old filtered oid can be missing from the repo (e.g. a new branch); // there is no range to exclude then, so take everything reachable. let old_filtered_exists = matches!( - odb.read_header(old_filtered_oid), + odb.read_header(objects::gix_oid(old_filtered_oid)), Ok((gix_object::Kind::Commit, _)) ); let revs = if old_filtered_exists { diff --git a/josh-graphql/src/graphql.rs b/josh-graphql/src/graphql.rs index dc7805ac4..aa9b16fed 100644 --- a/josh-graphql/src/graphql.rs +++ b/josh-graphql/src/graphql.rs @@ -1046,7 +1046,7 @@ impl Repository { let transaction_mirror = context.transaction_mirror.lock().unwrap(); let commit_id = { let oid = if let Ok(id) = git2::Oid::from_str(&at) { - Some((id, transaction_mirror.odb().contains(id))) + Some((id, transaction_mirror.odb().contains(objects::gix_oid(id)))) } else { None }; diff --git a/josh-gui/Cargo.lock b/josh-gui/Cargo.lock index 494539343..fca0874ef 100644 --- a/josh-gui/Cargo.lock +++ b/josh-gui/Cargo.lock @@ -4424,14 +4424,12 @@ name = "josh-memodb" version = "26.7.28" dependencies = [ "anyhow", - "git2", "gix-features", "gix-hash", "gix-object", "gix-odb", "gix-pack", "gix-zlib", - "josh-gix-ext", "log", "parking_lot 0.12.5", "tempfile", diff --git a/josh-memodb/Cargo.toml b/josh-memodb/Cargo.toml index fa6616850..f7e82447a 100644 --- a/josh-memodb/Cargo.toml +++ b/josh-memodb/Cargo.toml @@ -11,14 +11,16 @@ edition = "2024" [dependencies] anyhow.workspace = true -git2.workspace = true gix-features.workspace = true gix-hash.workspace = true gix-object.workspace = true gix-odb.workspace = true gix-pack.workspace = true gix-zlib.workspace = true -josh-gix-ext.workspace = true log.workspace = true parking_lot.workspace = true tempfile.workspace = true + +[dev-dependencies] +git2.workspace = true +josh-gix-ext.workspace = true diff --git a/josh-memodb/src/flusher.rs b/josh-memodb/src/flusher.rs index f598fd293..6b2f38581 100644 --- a/josh-memodb/src/flusher.rs +++ b/josh-memodb/src/flusher.rs @@ -80,22 +80,18 @@ pub(crate) fn enqueue_chunk(store: Arc) { /// Pack `store` to disk and block until it is done, so the objects are durable before the caller /// proceeds. Any queued chunks for the same store complete first (FIFO on the single worker). -pub(crate) fn drain(store: Arc) -> Result<(), git2::Error> { +pub(crate) fn drain(store: Arc) -> anyhow::Result<()> { let (ack_tx, ack_rx) = sync_channel::>(1); if FLUSHER .sender .send(Job::Drain { store, ack: ack_tx }) .is_err() { - return Err(git2::Error::from_str( - "mem-odb flusher channel disconnected", - )); + return Err(anyhow::anyhow!("mem-odb flusher channel disconnected")); } match ack_rx.recv() { Ok(Ok(())) => Ok(()), - Ok(Err(msg)) => Err(git2::Error::from_str(&msg)), - Err(_) => Err(git2::Error::from_str( - "mem-odb flusher ack channel disconnected", - )), + Ok(Err(msg)) => Err(anyhow::anyhow!(msg)), + Err(_) => Err(anyhow::anyhow!("mem-odb flusher ack channel disconnected")), } } diff --git a/josh-memodb/src/lib.rs b/josh-memodb/src/lib.rs index 16e5e5cd6..20d215ced 100644 --- a/josh-memodb/src/lib.rs +++ b/josh-memodb/src/lib.rs @@ -15,5 +15,4 @@ pub mod registry; pub use hash::PassthroughHasher; pub use mem_odb::MemOdb; pub use odb::{Bytes, Odb}; -pub use pack::objects_dir; pub use registry::FlushGuard; diff --git a/josh-memodb/src/mem_odb.rs b/josh-memodb/src/mem_odb.rs index 7fe44d4e9..593e61d13 100644 --- a/josh-memodb/src/mem_odb.rs +++ b/josh-memodb/src/mem_odb.rs @@ -189,7 +189,7 @@ impl MemOdb { /// ref ends. Everything *readable* through the store has to become durable, so a chained /// store drains what it reads through too. Runs on the background flusher behind any queued /// overflow chunks; a store holding nothing skips the round trip. - pub fn flush(self: &Arc) -> Result<(), git2::Error> { + pub fn flush(self: &Arc) -> anyhow::Result<()> { if let Some(behind) = &self.read_through { behind.flush()?; } @@ -234,7 +234,7 @@ impl MemOdb { /// no repository handle involved. The snapshot shares the object buffers (`Arc`), and the /// objects stay in the map while the pack is written, so concurrent reads keep resolving until /// eviction — which happens only once the pack is on disk. - pub(crate) fn pack_to_disk(self: &Arc) -> Result<(), git2::Error> { + pub(crate) fn pack_to_disk(self: &Arc) -> anyhow::Result<()> { // One packer per store at a time: the flusher's own jobs are already serialised, but the // process-exit flush packs inline and would otherwise snapshot and evict concurrently. let _packing = self.pack_lock.lock(); diff --git a/josh-memodb/src/odb.rs b/josh-memodb/src/odb.rs index be2aac0dc..97a582fad 100644 --- a/josh-memodb/src/odb.rs +++ b/josh-memodb/src/odb.rs @@ -1,8 +1,8 @@ //! The transaction object-database facade: the [`MemOdb`] store consulted directly, backed by //! the repository's gitoxide object database for objects that are not buffered. //! -//! Implements the [`gix_object`] object-access traits (memory-first, disk fallback) plus -//! inherent helpers in `git2::Oid` currency; memory hits hand out zero-copy `Arc` buffers. +//! Implements the [`gix_object`] object-access traits and `ObjectId`-typed inherent helpers +//! (memory-first, disk fallback); memory hits hand out zero-copy `Arc` buffers. //! //! A store resolves `objects/info/alternates` when it opens, so an alternate registered at //! runtime (the proxy overlay's mirror) is a store of its own, consulted after the @@ -137,10 +137,9 @@ impl Odb { .any(|alt| alt.gate.exists(id)) } - /// Read the raw bytes and kind of `oid`; memory hits are zero-copy. A missing object is an + /// Read the raw bytes and kind of `id`; memory hits are zero-copy. A missing object is an /// error, like a plain odb read. - pub fn read(&self, oid: git2::Oid) -> anyhow::Result<(Kind, Bytes)> { - let id = josh_gix_ext::gix_oid(oid); + pub fn read(&self, id: ObjectId) -> anyhow::Result<(Kind, Bytes)> { if let Some((kind, data)) = self.mem.get(&id) { return Ok((kind, Bytes::Mem(data))); } @@ -150,13 +149,13 @@ impl Odb { let mut buffer = Vec::new(); if let Some(kind) = self .find_on_disk(&id, &mut buffer) - .map_err(|e| anyhow::anyhow!("failed to read {}: {}", oid, e))? + .map_err(|e| anyhow::anyhow!("failed to read {}: {}", id, e))? { return Ok((kind, Bytes::Disk(buffer))); } Err(anyhow::anyhow!( "object not found - no match for id ({})", - oid + id )) } @@ -178,30 +177,28 @@ impl Odb { Ok(None) } - /// Kind and size of `oid` without reading (or decompressing) its bytes. - pub fn read_header(&self, oid: git2::Oid) -> anyhow::Result<(Kind, u64)> { - self.header(oid)? - .ok_or_else(|| anyhow::anyhow!("object not found - no match for id ({})", oid)) + /// Kind and size of `id` without reading (or decompressing) its bytes. + pub fn read_header(&self, id: ObjectId) -> anyhow::Result<(Kind, u64)> { + self.header(id)? + .ok_or_else(|| anyhow::anyhow!("object not found - no match for id ({})", id)) } - pub fn contains(&self, oid: git2::Oid) -> bool { - let id = josh_gix_ext::gix_oid(oid); + pub fn contains(&self, id: ObjectId) -> bool { self.mem.contains(&id) || self.disk.exists(&id) || self.in_alternate(&id) } - /// Kind of `oid`, or `None` if the object does not exist. Never decompresses. Resolves the + /// Kind of `id`, or `None` if the object does not exist. Never decompresses. Resolves the /// empty tree even when it is stored nowhere, so probes on possibly-empty trees belong /// here and never on [`contains`](Odb::contains). - pub fn try_kind(&self, oid: git2::Oid) -> anyhow::Result> { - Ok(self.header(oid)?.map(|(kind, _)| kind)) + pub fn try_kind(&self, id: ObjectId) -> anyhow::Result> { + Ok(self.header(id)?.map(|(kind, _)| kind)) } - /// Kind and size of `oid`, or `None` when no store holds it. - fn header(&self, oid: git2::Oid) -> anyhow::Result> { - let id = josh_gix_ext::gix_oid(oid); + /// Kind and size of `id`, or `None` when no store holds it. + fn header(&self, id: ObjectId) -> anyhow::Result> { Ok(self .try_header(&id) - .map_err(|e| anyhow::anyhow!("failed to read header of {}: {}", oid, e))? + .map_err(|e| anyhow::anyhow!("failed to read header of {}: {}", id, e))? .map(|header| (header.kind, header.size))) } @@ -212,11 +209,11 @@ impl Odb { /// the repository's own on-disk objects: objects already durable there are buffered anyway /// and dropped at pack time, and a repository without alternates writes with zero /// filesystem I/O. - pub fn write(&self, kind: Kind, data: &[u8]) -> git2::Oid { + pub fn write(&self, kind: Kind, data: &[u8]) -> ObjectId { let id = gix_object::compute_hash(gix_hash::Kind::Sha1, kind, data) .expect("failed to compute hash"); self.write_with_id(id, kind, data); - josh_gix_ext::git2_oid(&id) + id } /// [`Odb::write`] with a caller-computed content hash, trusted verbatim. @@ -296,7 +293,7 @@ impl gix_object::Exists for Odb { impl gix_object::Write for Odb { fn write_buf(&self, object: Kind, from: &[u8]) -> Result { - Ok(josh_gix_ext::gix_oid(self.write(object, from))) + Ok(self.write(object, from)) } fn write_buf_with_known_id( @@ -360,11 +357,17 @@ mod tests { // Not yet on disk. let fresh = git2::Repository::open(dir.path()).unwrap(); - assert!(fresh.find_blob(oid).is_err()); + assert!(fresh.find_blob(josh_gix_ext::git2_oid(&oid)).is_err()); store.flush().unwrap(); let fresh = git2::Repository::open(dir.path()).unwrap(); - assert_eq!(fresh.find_blob(oid).unwrap().content(), b"facade blob"); + assert_eq!( + fresh + .find_blob(josh_gix_ext::git2_oid(&oid)) + .unwrap() + .content(), + b"facade blob" + ); } /// The write gate: an object already in a registered runtime alternate is not buffered @@ -389,14 +392,14 @@ mod tests { // Alternate hit: skipped, still readable through the alternate. let oid = odb.write(Kind::Blob, b"mirror blob"); - assert_eq!(oid, in_alternate); - assert!(!store.contains(&josh_gix_ext::gix_oid(oid))); + assert_eq!(oid, josh_gix_ext::gix_oid(in_alternate)); + assert!(!store.contains(&oid)); assert!(matches!(odb.read(oid).unwrap().1, Bytes::Disk(_))); // Main-disk object: buffered — the gate does not probe the repository's own objects. let oid = odb.write(Kind::Blob, b"already on disk"); - assert_eq!(oid, on_disk); - assert!(store.contains(&josh_gix_ext::gix_oid(oid))); + assert_eq!(oid, josh_gix_ext::gix_oid(on_disk)); + assert!(store.contains(&oid)); assert!(matches!(odb.read(oid).unwrap().1, Bytes::Mem(_))); } @@ -409,7 +412,7 @@ mod tests { let mirror_dir = crate::pack::objects_dir(&mirror); // Packed before the alternate is registered. let mirror_store = MemOdb::new(None, mirror_dir.clone()); - let packed = josh_gix_ext::git2_oid(&mirror_store.write(Kind::Blob, b"packed in mirror")); + let packed = mirror_store.write(Kind::Blob, b"packed in mirror"); mirror_store.flush().unwrap(); let repo = git2::Repository::init(tmp.path().join("overlay")).unwrap(); @@ -418,14 +421,14 @@ mod tests { odb.add_alternate(&mirror_dir).unwrap(); assert_eq!(odb.write(Kind::Blob, b"packed in mirror"), packed); - assert!(!store.contains(&josh_gix_ext::gix_oid(packed))); + assert!(!store.contains(&packed)); // Packed after registration: the gate does not go looking, so it buffers a duplicate. - let later = josh_gix_ext::git2_oid(&mirror_store.write(Kind::Blob, b"packed later")); - let unread = josh_gix_ext::git2_oid(&mirror_store.write(Kind::Blob, b"never written")); + let later = mirror_store.write(Kind::Blob, b"packed later"); + let unread = mirror_store.write(Kind::Blob, b"never written"); mirror_store.flush().unwrap(); assert_eq!(odb.write(Kind::Blob, b"packed later"), later); - assert!(store.contains(&josh_gix_ext::gix_oid(later))); + assert!(store.contains(&later)); // Reads do go looking, and find an object from that same pack. assert_eq!(&*odb.read(unread).unwrap().1, b"never written"); @@ -441,15 +444,19 @@ mod tests { let store = MemOdb::new(None, crate::pack::objects_dir(&repo)); let odb = facade(&store, &repo); - let empty_tree = git2::Oid::from_str("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap(); - assert!(!store.contains(&josh_gix_ext::gix_oid(empty_tree))); + let empty_tree = josh_gix_ext::gix_oid( + git2::Oid::from_str("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap(), + ); + assert!(!store.contains(&empty_tree)); assert_eq!(odb.try_kind(empty_tree).unwrap(), Some(Kind::Tree)); assert_eq!(odb.read(empty_tree).unwrap().0, Kind::Tree); assert!(odb.read(empty_tree).unwrap().1.is_empty()); assert!(!odb.contains(empty_tree)); // A genuinely absent object is None; a buffered object reports its memory kind. - let absent = git2::Oid::from_str("0123456789012345678901234567890123456789").unwrap(); + let absent = josh_gix_ext::gix_oid( + git2::Oid::from_str("0123456789012345678901234567890123456789").unwrap(), + ); assert_eq!(odb.try_kind(absent).unwrap(), None); assert!(odb.read(absent).is_err()); let blob = odb.write(Kind::Blob, b"probe"); @@ -468,13 +475,10 @@ mod tests { let oid = odb.write(Kind::Blob, b"facade blob"); let mut buf = Vec::new(); - let data = odb - .try_find(&josh_gix_ext::gix_oid(oid), &mut buf) - .unwrap() - .unwrap(); + let data = odb.try_find(&oid, &mut buf).unwrap().unwrap(); assert_eq!(data.kind, Kind::Blob); assert_eq!(data.data, b"facade blob"); - assert!(odb.exists(&josh_gix_ext::gix_oid(oid))); + assert!(odb.exists(&oid)); } /// A pack written after the facade was built is still found, which is how a flush, a @@ -487,9 +491,8 @@ mod tests { let odb = facade(&store, &repo); // Miss first, so the store has settled on the packs it found at open. - let oid = gix_object::compute_hash(gix_hash::Kind::Sha1, Kind::Blob, b"packed later") - .map(|id| josh_gix_ext::git2_oid(&id)) - .unwrap(); + let oid = + gix_object::compute_hash(gix_hash::Kind::Sha1, Kind::Blob, b"packed later").unwrap(); assert!(!odb.contains(oid)); let store2 = MemOdb::new(None, crate::pack::objects_dir(&repo)); diff --git a/josh-memodb/src/pack.rs b/josh-memodb/src/pack.rs index cbdfb939f..ccf41ab5a 100644 --- a/josh-memodb/src/pack.rs +++ b/josh-memodb/src/pack.rs @@ -7,7 +7,7 @@ //! layout stays deterministic. use std::io::{Seek, Write}; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::sync::atomic::AtomicBool; use gix_object::Exists; @@ -15,20 +15,11 @@ use gix_pack::data::output; use crate::mem_odb::Snapshot; -/// The directory where `repo`'s objects live (`/objects`), captured at -/// [`MemOdb::new`](crate::mem_odb::MemOdb::new) time while a repository handle exists. -/// -/// Resolved from the repository's *common* directory rather than its gitdir: a linked worktree has -/// no `objects/` of its own, so its objects live under the common dir. For a non-worktree repo the -/// two are the same. -pub fn objects_dir(repo: &git2::Repository) -> PathBuf { +#[cfg(test)] +pub(crate) fn objects_dir(repo: &git2::Repository) -> std::path::PathBuf { repo.commondir().join("objects") } -fn pack_error(e: impl std::fmt::Display) -> git2::Error { - git2::Error::from_str(&format!("mem-odb pack write failed: {e}")) -} - /// Compress and write the objects of `snapshot` that are not already present in `objects_dir` /// (loose, packed, or via alternates) as a single packfile-plus-index pair in /// `objects_dir/pack`. A no-op if every object is already on disk. @@ -38,12 +29,13 @@ fn pack_error(e: impl std::fmt::Display) -> git2::Error { /// trailer checksum (the same rule libgit2 and modern git use), so identical snapshots produce /// identical packs. Files are written via tempfile-and-rename, index last, so a concurrent /// reader scanning for `.idx` files never sees a torn pair. -pub(crate) fn write_snapshot(objects_dir: &Path, snapshot: &Snapshot) -> Result<(), git2::Error> { +pub(crate) fn write_snapshot(objects_dir: &Path, snapshot: &Snapshot) -> anyhow::Result<()> { // A fresh store handle per flush observes every pack written by previous flushes. Misses are // the expected case below, and the default refresh mode re-lists the pack directory on every // miss — disable it; the first lookup still loads all indices present now, and loose-object // probes stat the filesystem directly either way. - let mut odb = gix_odb::at(objects_dir.to_owned()).map_err(pack_error)?; + let mut odb = gix_odb::at(objects_dir.to_owned()) + .map_err(|e| anyhow::anyhow!("mem-odb pack write failed: {e}"))?; odb.refresh = gix_odb::store::RefreshMode::Never; let to_pack: Vec<_> = snapshot @@ -55,17 +47,21 @@ pub(crate) fn write_snapshot(objects_dir: &Path, snapshot: &Snapshot) -> Result< return Ok(()); } let num_entries = u32::try_from(to_pack.len()) - .map_err(|_| pack_error("more objects in one flush than a pack header can count"))?; + .map_err(|e| anyhow::anyhow!("mem-odb pack write failed: {e}"))?; let pack_dir = objects_dir.join("pack"); - std::fs::create_dir_all(&pack_dir).map_err(pack_error)?; + std::fs::create_dir_all(&pack_dir) + .map_err(|e| anyhow::anyhow!("mem-odb pack write failed: {e}"))?; // Serialize the pack byte stream (header, compressed entries, checksum trailer) through an // anonymous spool file in the pack directory: objects are compressed one at a time as the // serializer pulls them, so no more than one compressed object is ever held in memory — // a snapshot's size is only *typically* bounded by the store's chunk limit (unbounded stores // exist, and the limit is an overflow trigger, not a cap). - let mut spool = std::io::BufWriter::new(tempfile::tempfile_in(&pack_dir).map_err(pack_error)?); + let mut spool = std::io::BufWriter::new( + tempfile::tempfile_in(&pack_dir) + .map_err(|e| anyhow::anyhow!("mem-odb pack write failed: {e}"))?, + ); let mut iter = output::bytes::FromEntriesIter::new( to_pack.iter().map(|(oid, kind, data)| { output::Entry::from_data( @@ -82,12 +78,18 @@ pub(crate) fn write_snapshot(objects_dir: &Path, snapshot: &Snapshot) -> Result< gix_hash::Kind::Sha1, ); for written in &mut iter { - written.map_err(pack_error)?; + written.map_err(|e| anyhow::anyhow!("mem-odb pack write failed: {e}"))?; } drop(iter); - spool.flush().map_err(pack_error)?; - let mut spool = spool.into_inner().map_err(pack_error)?; - spool.rewind().map_err(pack_error)?; + spool + .flush() + .map_err(|e| anyhow::anyhow!("mem-odb pack write failed: {e}"))?; + let mut spool = spool + .into_inner() + .map_err(|e| anyhow::anyhow!("mem-odb pack write failed: {e}"))?; + spool + .rewind() + .map_err(|e| anyhow::anyhow!("mem-odb pack write failed: {e}"))?; let outcome = gix_pack::Bundle::write_to_directory( &mut std::io::BufReader::new(spool), @@ -105,7 +107,7 @@ pub(crate) fn write_snapshot(objects_dir: &Path, snapshot: &Snapshot) -> Result< compression: gix_zlib::Compression::DEFAULT, }, ) - .map_err(pack_error)?; + .map_err(|e| anyhow::anyhow!("mem-odb pack write failed: {e}"))?; // gix marks the freshly-landed pack with a `.keep` file for the caller to remove once its // referencing refs exist. josh's flushes carry no such handshake (libgit2's packbuilder wrote // no `.keep` either), and a leftover one would exempt the pack from `git repack -d` forever. @@ -116,7 +118,7 @@ pub(crate) fn write_snapshot(objects_dir: &Path, snapshot: &Snapshot) -> Result< if let Some(data_path) = &outcome.data_path { if let Err(e) = std::fs::remove_file(data_path.with_extension("keep")) { if e.kind() != std::io::ErrorKind::NotFound { - return Err(pack_error(e)); + return Err(anyhow::anyhow!("mem-odb pack write failed: {e}")); } } } From de3de6e61dd75f48e873c972c0a63c60c45c6aca Mon Sep 17 00:00:00 2001 From: Christian Schilling Date: Tue, 25 Aug 2026 20:17:34 +0200 Subject: [PATCH 3/5] Move persisted object IDs to gitoxide Use gix_hash::ObjectId throughout josh-filter and josh-git-serde public and persisted representations. Keep explicit conversions at callers that still use libgit2 OIDs, and key filter-identity caches directly by ObjectId. Remove the resulting direct git2 dependencies and preserve serialized filter trees and legacy consumer behavior. Use anyhow context for mem-ODB pack errors that cross the updated dependency boundary. Change: gix-filter-oid-currency Assisted-By: openai-codex/gpt-5.6-sol --- Cargo.lock | 2 - josh-changes/src/change.rs | 3 +- josh-changes/src/comments.rs | 2 +- josh-changes/src/store.rs | 14 ++++-- josh-cli/src/bin/josh-filter.rs | 12 +++-- josh-core/benches/deephistory_subdir.rs | 4 +- josh-core/benches/ultrawide_pin_hook.rs | 2 +- josh-core/src/cache/distributed.rs | 10 +++-- josh-core/src/cache/sled.rs | 2 +- josh-core/src/cache/transaction.rs | 8 ++-- josh-core/src/filter/mod.rs | 58 ++++++++++++++++--------- josh-filter/Cargo.toml | 1 - josh-filter/src/filter.rs | 15 ++++--- josh-filter/src/flang/parse.rs | 6 ++- josh-filter/src/op.rs | 6 +-- josh-filter/src/persist.rs | 36 ++++++++------- josh-git-serde/Cargo.toml | 6 +-- josh-git-serde/src/store.rs | 27 ++++++++---- josh-git-serde/tests/store.rs | 21 ++++----- josh-gui/Cargo.lock | 26 +++++------ josh-memodb/src/pack.rs | 31 +++++-------- 21 files changed, 164 insertions(+), 128 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8794d849b..88b0f1e79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3479,7 +3479,6 @@ name = "josh-filter" version = "26.7.28" dependencies = [ "anyhow", - "git2", "gix-hash", "gix-object", "glob", @@ -3499,7 +3498,6 @@ name = "josh-git-serde" version = "26.7.28" dependencies = [ "anyhow", - "git2", "gix-hash", "gix-object", "josh-gix-ext", diff --git a/josh-changes/src/change.rs b/josh-changes/src/change.rs index b36260ab0..69df9667d 100644 --- a/josh-changes/src/change.rs +++ b/josh-changes/src/change.rs @@ -124,7 +124,8 @@ pub(crate) fn split_changes( changes .into_values() .map(|c| { - let filter = josh_core::filter::Filter::new().downstack(c.base); + let filter = + josh_core::filter::Filter::new().downstack(josh_core::objects::gix_oid(c.base)); let new_oid = josh_core::filter::apply_to_commit(filter, c.commit, transaction)?; let mut result = c; result.commit = new_oid; diff --git a/josh-changes/src/comments.rs b/josh-changes/src/comments.rs index 92df50055..907bea116 100644 --- a/josh-changes/src/comments.rs +++ b/josh-changes/src/comments.rs @@ -246,7 +246,7 @@ fn read_comment( entry_oid: git2::Oid, file: Option, ) -> anyhow::Result { - let value = from_tree_oid(odb, entry_oid)?; + let value = from_tree_oid(odb, objects::gix_oid(entry_oid))?; let meta: CommentMeta = from_value(&value)?; Ok(Comment { id: id.to_string(), diff --git a/josh-changes/src/store.rs b/josh-changes/src/store.rs index cd154d07d..003855fa4 100644 --- a/josh-changes/src/store.rs +++ b/josh-changes/src/store.rs @@ -124,7 +124,7 @@ pub fn value_oid( GitValue::Tree(_) => 0o0040000, GitValue::Blob(_) => git2::FileMode::Blob.into(), }; - Ok((root, mode)) + Ok((objects::git2_oid(&root), mode)) } /// Place the already-written object `root` at `path` inside `scope`'s ref and @@ -197,7 +197,7 @@ pub fn read_filtered( filter, josh_core::filter::Rewrite::from_tree(root), )?; - let value = josh_git_serde::from_tree_oid(odb, filtered.tree_id())?; + let value = josh_git_serde::from_tree_oid(odb, objects::gix_oid(filtered.tree_id()))?; Ok(Some(josh_git_serde::from_value(&value)?)) } @@ -235,12 +235,18 @@ pub fn store_diff_data( let path = std::path::Path::new("diffs").join(encode_change_id_path(&change_id)); if let Ok(Some(existing)) = tree::get_path_entry(transaction, odb, base_tree, &path) { - if objects::git2_oid(&existing.oid) == tree_oid { + if existing.oid == tree_oid { return Ok(()); } } - let tree = tree::insert_oid(odb, base_tree, &path, tree_oid, 0o0040000)?; + let tree = tree::insert_oid( + odb, + base_tree, + &path, + objects::git2_oid(&tree_oid), + 0o0040000, + )?; let sig = transaction.signature()?; diff --git a/josh-cli/src/bin/josh-filter.rs b/josh-cli/src/bin/josh-filter.rs index 8ec9a1c20..39bfffa80 100644 --- a/josh-cli/src/bin/josh-filter.rs +++ b/josh-cli/src/bin/josh-filter.rs @@ -222,7 +222,7 @@ fn run_filter(args: Vec) -> anyhow::Result { let input_ref = args.get_one::("input").unwrap(); let mut refs = vec![]; - let mut ids: Vec<(git2::Oid, josh_core::filter::Filter)> = vec![]; + let mut ids = vec![]; let (input_ref, oid) = resolve_input_ref(&transaction, input_ref)?; refs.push((input_ref.clone(), oid)); @@ -243,7 +243,10 @@ fn run_filter(args: Vec) -> anyhow::Result { return Ok(()); } let target = repo.find_object(oid, None)?.peel_to_commit()?.id(); - ids.push((target, josh_core::filter::Filter::new().message(name))); + ids.push(( + josh_core::objects::gix_oid(target), + josh_core::filter::Filter::new().message(name), + )); refs.push((name.to_string(), target)); Ok(()) })?; @@ -260,7 +263,10 @@ fn run_filter(args: Vec) -> anyhow::Result { if let [sha, name] = split.as_slice() { let target = git2::Oid::from_str(sha)?; let target = repo.find_object(target, None)?.peel_to_commit()?.id(); - ids.push((target, josh_core::filter::Filter::new().message(name))); + ids.push(( + josh_core::objects::gix_oid(target), + josh_core::filter::Filter::new().message(name), + )); refs.push((name.to_string(), target)); } else if !split.is_empty() { eprintln!("Warning: malformed line: {:?}", line); diff --git a/josh-core/benches/deephistory_subdir.rs b/josh-core/benches/deephistory_subdir.rs index b7fcb6962..a1ad48bfe 100644 --- a/josh-core/benches/deephistory_subdir.rs +++ b/josh-core/benches/deephistory_subdir.rs @@ -310,7 +310,7 @@ fn deephistory_rev(c: &mut Criterion) { let case = bench.cases.first().expect("at least one case"); let rev_filter = Filter::new().rev(vec![( RevMatch::AncestorInclusive, - case.head, + gix_oid(case.head), Filter::new().subdir(SUBDIR), )]); let transaction = bench.context.open().expect("open transaction"); @@ -338,7 +338,7 @@ fn deephistory_rev(c: &mut Criterion) { for case in &bench.cases { let rev_filter = Filter::new().rev(vec![( RevMatch::AncestorInclusive, - case.head, + gix_oid(case.head), Filter::new().subdir(SUBDIR), )]); group.throughput(Throughput::Elements(case.n_commits as u64)); diff --git a/josh-core/benches/ultrawide_pin_hook.rs b/josh-core/benches/ultrawide_pin_hook.rs index 6502d13fc..9ce75a56c 100644 --- a/josh-core/benches/ultrawide_pin_hook.rs +++ b/josh-core/benches/ultrawide_pin_hook.rs @@ -290,7 +290,7 @@ fn pin_filter_tree( gix_oid(placeholder), )?; } - let tree = git2_oid(builder.write()?.detach()); + let tree = builder.write()?.detach(); let param = josh_filter::Filter::new().insert_oid(".", tree)?; Ok(josh_filter::Filter::new().pin(param)) } diff --git a/josh-core/src/cache/distributed.rs b/josh-core/src/cache/distributed.rs index 8076280fa..5f16decf1 100644 --- a/josh-core/src/cache/distributed.rs +++ b/josh-core/src/cache/distributed.rs @@ -34,7 +34,7 @@ pub struct DistributedCacheBackend { // Filter -> persisted tree id (`as_tree`), used to name cache refs. `as_tree` resolves // insert OIDs, so ref names always reference persisted, reachable filter trees even when // the filter passed in still contains unresolved ones. - tree_ids: std::sync::Mutex>, + tree_ids: std::sync::Mutex>, } impl Drop for DistributedCacheBackend { @@ -74,7 +74,11 @@ impl DistributedCacheBackend { }) } - fn tree_id(&self, odb: &josh_memodb::Odb, filter: Filter) -> anyhow::Result { + fn tree_id( + &self, + odb: &josh_memodb::Odb, + filter: Filter, + ) -> anyhow::Result { if let Some(oid) = self.tree_ids.lock().unwrap().get(&filter) { return Ok(*oid); } @@ -197,7 +201,7 @@ impl DistributedCacheBackend { // To additionally limit the size of the trees the cache is also sharded by sequence // number in groups of 10000. Note that this does not limit the number of entries per bucket // as branches mean many commits share the same sequence number. -fn ref_path(filter_tree_id: git2::Oid, shard: u64) -> String { +fn ref_path(filter_tree_id: gix_hash::ObjectId, shard: u64) -> String { format!( "refs/josh/cache/{}/{}/{}", CACHE_VERSION, shard, filter_tree_id, diff --git a/josh-core/src/cache/sled.rs b/josh-core/src/cache/sled.rs index 9d29d7042..7446173a1 100644 --- a/josh-core/src/cache/sled.rs +++ b/josh-core/src/cache/sled.rs @@ -17,7 +17,7 @@ struct State { /// Cache directory, set by [`SledCacheBackend::new`]. `None` until the first backend is built. path: Option, db: Option, - trees: std::collections::HashMap, + trees: std::collections::HashMap, active: usize, } diff --git a/josh-core/src/cache/transaction.rs b/josh-core/src/cache/transaction.rs index c5c4734ae..cb0acc230 100644 --- a/josh-core/src/cache/transaction.rs +++ b/josh-core/src/cache/transaction.rs @@ -87,7 +87,7 @@ fn previous_value(expected: Expected) -> gix::refs::transaction::PreviousValue { } } -static REF_CACHE: LazyLock>>> = +static REF_CACHE: LazyLock>>> = LazyLock::new(Default::default); static POPULATE_MAP: LazyLock>> = @@ -239,12 +239,12 @@ impl TransactionContext { #[allow(unused)] struct Transaction2 { - commit_map: HashMap>, - apply_map: HashMap>, + commit_map: HashMap>, + apply_map: HashMap>, subtract_map: HashMap<(git2::Oid, git2::Oid), git2::Oid>, intersect_map: HashMap<(git2::Oid, git2::Oid), git2::Oid>, overlay_map: HashMap<(git2::Oid, git2::Oid), git2::Oid>, - unapply_map: HashMap>, + unapply_map: HashMap>, legalize_map: HashMap<(crate::filter::Filter, git2::Oid), crate::filter::Filter>, downstack_deps_map: HashMap>, merge_trees_map: HashMap<(git2::Oid, git2::Oid, git2::Oid), git2::Oid>, diff --git a/josh-core/src/filter/mod.rs b/josh-core/src/filter/mod.rs index b02dec3db..7f6b3bb20 100644 --- a/josh-core/src/filter/mod.rs +++ b/josh-core/src/filter/mod.rs @@ -24,11 +24,14 @@ pub mod text; pub mod tree; pub fn as_tree(transaction: &cache::Transaction, filter: Filter) -> anyhow::Result { - josh_filter::persist::as_tree(transaction.odb(), filter) + Ok(objects::git2_oid(&josh_filter::persist::as_tree( + transaction.odb(), + filter, + )?)) } pub fn from_tree(transaction: &cache::Transaction, tree_oid: git2::Oid) -> anyhow::Result { - josh_filter::persist::from_tree(transaction.odb(), tree_oid) + josh_filter::persist::from_tree(transaction.odb(), objects::gix_oid(tree_oid)) } static WORKSPACES: LazyLock>> = @@ -230,7 +233,7 @@ fn resolve_refs2(refs: &std::collections::HashMap, op: &Op) - let f = resolve_refs(refs, *f); let resolved_r = if let LazyRef::Lazy(s) = r { if let Some(res) = refs.get(s) { - LazyRef::Resolved(*res) + LazyRef::Resolved(objects::gix_oid(*res)) } else { r.clone() } @@ -248,7 +251,7 @@ fn resolve_refs2(refs: &std::collections::HashMap, op: &Op) - .map(|(r, m)| { if let LazyRef::Lazy(s) = r { if let Some(res) = refs.get(s) { - (LazyRef::Resolved(*res), *m) + (LazyRef::Resolved(objects::gix_oid(*res)), *m) } else { (r.clone(), *m) } @@ -261,7 +264,7 @@ fn resolve_refs2(refs: &std::collections::HashMap, op: &Op) - } Op::Downstack(LazyRef::Lazy(s)) => { if let Some(res) = refs.get(s) { - Op::Downstack(LazyRef::Resolved(*res)) + Op::Downstack(LazyRef::Resolved(objects::gix_oid(*res))) } else { op.clone() } @@ -544,12 +547,12 @@ fn get_rev_filter( // First match wins - iterate in order for (match_op, filter_tip_ref, startfilter) in filters.iter() { let filter_tip = if let LazyRef::Resolved(filter_tip) = filter_tip_ref { - filter_tip + objects::git2_oid(filter_tip.as_ref()) } else { return Err(anyhow!("unresolved lazy ref")); }; if match_op != &RevMatch::Default - && !transaction.odb().contains(objects::gix_oid(*filter_tip)) + && !transaction.odb().contains(objects::gix_oid(filter_tip)) { return Err(anyhow!("`:rev(...)` with nonexistent OID: {}", filter_tip)); } @@ -557,17 +560,17 @@ fn get_rev_filter( RevMatch::AncestorStrict => { // `<` - matches if commit is ancestor of tip AND commit != tip (strict) - is_ancestor_of(transaction, commit_id, *filter_tip)? && commit_id != *filter_tip + is_ancestor_of(transaction, commit_id, filter_tip)? && commit_id != filter_tip } RevMatch::AncestorInclusive => { // `<=` - matches if commit is ancestor of tip OR commit == tip (inclusive) - is_ancestor_of(transaction, commit_id, *filter_tip)? + is_ancestor_of(transaction, commit_id, filter_tip)? } RevMatch::Equal => { // `==` - matches if commit == tip - commit_id == *filter_tip + commit_id == filter_tip } RevMatch::Default => { // `_` - always matches (makes filters after it unreachable) @@ -630,7 +633,7 @@ pub fn apply_to_commit2( if let Some(oid) = transaction.get(filter, commit_id)? { return Ok(Some(oid)); } - let new_oid = downstack(transaction, commit_id, *base)?; + let new_oid = downstack(transaction, commit_id, objects::git2_oid(base.as_ref()))?; transaction.insert(filter, commit_id, new_oid, false)?; return Ok(Some(new_oid)); } @@ -655,7 +658,7 @@ pub fn apply_to_commit2( let rewrite_data = match &op { Op::Squash(Some(ids)) => { - if let Some(sq) = ids.get(&LazyRef::Resolved(commit.id())) { + if let Some(sq) = ids.get(&LazyRef::Resolved(objects::gix_oid(commit.id()))) { let oid = if let Some(oid) = apply_to_commit2( filter::Filter::new().squash(None).chain(*sq), commit_id, @@ -1037,7 +1040,7 @@ pub fn apply_to_commit2( check_experimental_features_enabled("unapply filter")?; if let LazyRef::Resolved(target) = target { /* dbg!(target); */ - let target = objects::CommitData::read(odb, *target)?; + let target = objects::CommitData::read(odb, objects::git2_oid(target.as_ref()))?; // Only a root commit (no first parent) skips link detection; a // first parent that is present must be readable. if let Some(parent_id) = target.first_parent_id() { @@ -1057,8 +1060,10 @@ pub fn apply_to_commit2( if let Some(commit_str) = link.get_meta("commit") { if let Ok(link_commit) = git2::Oid::from_str(&commit_str) { if commit.id() == link_commit { - let unapply = - to_filter(Op::Unapply(LazyRef::Resolved(parent.id()), *uf)); + let unapply = to_filter(Op::Unapply( + LazyRef::Resolved(objects::gix_oid(parent.id())), + *uf, + )); let r = some_or!(transaction.get(unapply, link_commit)?, { return Ok(None); }); @@ -1088,7 +1093,10 @@ pub fn apply_to_commit2( let tree_reader = tree::read_tree(transaction, odb, tree)?; if let Some(link) = read_josh_link(transaction, odb, &tree_reader, path, ".link.josh") { let subdir = filter::invert(link.peel())?; - let unapply = to_filter(Op::Unapply(LazyRef::Resolved(commit.id()), subdir)); + let unapply = to_filter(Op::Unapply( + LazyRef::Resolved(objects::gix_oid(commit.id())), + subdir, + )); if let Some(commit_str) = link.get_meta("commit") { if let Ok(commit_oid) = git2::Oid::from_str(&commit_str) { let r = some_or!(transaction.get(unapply, commit_oid)?, { @@ -1456,7 +1464,7 @@ fn apply_impl( Op::Pattern(cp) => { let input = x.tree_id(); - let key = peel_filter(filter).id(); + let key = objects::git2_oid(peel_filter(filter).id().as_ref()); let t = if cp.fallback { // More components than the NFA state mask can hold: match full paths. tree::remove_pred( @@ -1488,9 +1496,17 @@ fn apply_impl( ), // The kind comes from the header alone; a missing oid folds into the // "neither" arm below. - InsertContent::Oid(oid) => match odb.try_kind(objects::gix_oid(*oid)) { - Ok(Some(gix_object::Kind::Blob)) => (*oid, git2::FileMode::Blob.into(), false), - Ok(Some(gix_object::Kind::Tree)) => (*oid, git2::FileMode::Tree.into(), true), + InsertContent::Oid(oid) => match odb.try_kind(*oid) { + Ok(Some(gix_object::Kind::Blob)) => ( + objects::git2_oid(oid.as_ref()), + git2::FileMode::Blob.into(), + false, + ), + Ok(Some(gix_object::Kind::Tree)) => ( + objects::git2_oid(oid.as_ref()), + git2::FileMode::Tree.into(), + true, + ), _ => { return Err(anyhow::anyhow!( "insert: {} is neither a blob nor a tree", @@ -1705,7 +1721,7 @@ fn apply_impl( Op::Unapply(target, uf) => { check_experimental_features_enabled("unapply filter")?; if let LazyRef::Resolved(target) = target { - let target = objects::CommitData::read(odb, *target)?; + let target = objects::CommitData::read(odb, objects::git2_oid(target.as_ref()))?; // The message must parse as an oid, so non-UTF-8 is an error. let target_msg = target.message()?; let target = git2::Oid::from_str(std::str::from_utf8(target_msg)?)?; diff --git a/josh-filter/Cargo.toml b/josh-filter/Cargo.toml index 7be216014..ca7f0a81b 100644 --- a/josh-filter/Cargo.toml +++ b/josh-filter/Cargo.toml @@ -18,7 +18,6 @@ indoc = "2.0.7" itertools = "0.15.0" anyhow.workspace = true -git2.workspace = true josh-memodb.workspace = true josh-gix-ext.workspace = true gix-object.workspace = true diff --git a/josh-filter/src/filter.rs b/josh-filter/src/filter.rs index 029b20f5e..08163ba6d 100644 --- a/josh-filter/src/filter.rs +++ b/josh-filter/src/filter.rs @@ -56,7 +56,7 @@ impl Default for Filter { impl Filter { /// The content-addressed OID of this filter, computed lazily on first call and cached on the /// node. This is the only place a filter's tree is built (via `build_op`). - pub fn id(&self) -> git2::Oid { + pub fn id(&self) -> gix_hash::ObjectId { *self.0.oid.get_or_init(|| persist::build_node_oid(self.0)) } } @@ -90,7 +90,7 @@ impl Filter { /// relationship to `tip` satisfies `match` (e.g. `AncestorInclusive` is `<=tip`); the first /// matching arm wins and a commit matching none passes through unchanged. Tips are resolved /// oids (`RevMatch::Default` ignores its tip); construct `Op::Rev` directly for lazy refs. - pub fn rev(self, arms: Vec<(RevMatch, git2::Oid, Filter)>) -> Filter { + pub fn rev(self, arms: Vec<(RevMatch, gix_hash::ObjectId, Filter)>) -> Filter { self.chain(to_filter(Op::Rev( arms.into_iter() .map(|(m, tip, then)| (m, LazyRef::Resolved(tip), then)) @@ -196,7 +196,7 @@ impl Filter { pub fn insert_oid( self, path: impl Into, - oid: git2::Oid, + oid: gix_hash::ObjectId, ) -> anyhow::Result { Ok(self.chain(to_filter(Op::Insert(path.into(), InsertContent::Oid(oid))))) } @@ -242,7 +242,7 @@ impl Filter { } /// Chain a squash filter - pub fn squash(self, ids: Option<&[(git2::Oid, Filter)]>) -> Filter { + pub fn squash(self, ids: Option<&[(gix_hash::ObjectId, Filter)]>) -> Filter { self.chain(if let Some(ids) = ids { to_filter(Op::Squash(Some( ids.iter() @@ -256,7 +256,7 @@ impl Filter { /// Chain a downstack filter that rebuilds the stack from `base` to the input commit, /// dropping intermediate commits whose paths are disjoint from the tip's changes. - pub fn downstack(self, base: git2::Oid) -> Filter { + pub fn downstack(self, base: gix_hash::ObjectId) -> Filter { self.chain(to_filter(Op::Downstack(LazyRef::Resolved(base)))) } @@ -373,7 +373,8 @@ pub fn invert(filter: Filter) -> anyhow::Result { /// The sequence_number filter used for tracking commit sequence numbers. A memoized sentinel /// node whose OID is the zero OID, so identity comparison and cache-keying stay correct. pub fn sequence_number() -> Filter { - static F: LazyLock = LazyLock::new(|| persist::sentinel(git2::Oid::ZERO_SHA1)); + static F: LazyLock = + LazyLock::new(|| persist::sentinel(gix_hash::ObjectId::null(gix_hash::Kind::Sha1))); *F } @@ -386,7 +387,7 @@ pub fn reachable_roots() -> Filter { static F: LazyLock = LazyLock::new(|| { let mut bytes = [0u8; 20]; bytes[19] = 1; - persist::sentinel(git2::Oid::from_bytes(&bytes).expect("valid sentinel oid")) + persist::sentinel(gix_hash::ObjectId::from_bytes_or_panic(&bytes)) }); *F } diff --git a/josh-filter/src/flang/parse.rs b/josh-filter/src/flang/parse.rs index d4ccfb3de..8087059e5 100644 --- a/josh-filter/src/flang/parse.rs +++ b/josh-filter/src/flang/parse.rs @@ -183,7 +183,7 @@ fn parse_item(pair: pest::iterators::Pair) -> anyhow::Result { let content_pair = inner.next().unwrap(); let content = match content_pair.as_rule() { Rule::string => InsertContent::Inline(unquote(content_pair.as_str())), - Rule::object_oid => InsertContent::Oid(git2::Oid::from_str(content_pair.as_str())?), + Rule::object_oid => InsertContent::Oid(content_pair.as_str().parse()?), _ => unreachable!(), }; Ok(to_filter(Op::Insert(path, content))) @@ -278,7 +278,9 @@ fn parse_item(pair: pest::iterators::Pair) -> anyhow::Result { let filter = parse(filter_pair.as_str())?; entries.push(( RevMatch::Default, - LazyRef::Resolved(git2::Oid::ZERO_SHA1), + LazyRef::Resolved(gix_hash::ObjectId::null( + gix_hash::Kind::Sha1, + )), filter, )); } diff --git a/josh-filter/src/op.rs b/josh-filter/src/op.rs index c9827418b..ddc09c239 100644 --- a/josh-filter/src/op.rs +++ b/josh-filter/src/op.rs @@ -57,7 +57,7 @@ impl LinkMode { #[derive(Hash, Clone, Debug, PartialEq, PartialOrd, Eq, Ord)] pub enum LazyRef { - Resolved(git2::Oid), + Resolved(gix_hash::ObjectId), Lazy(String), } @@ -67,7 +67,7 @@ pub enum InsertContent { /// An object referenced by OID. The kind (blob or tree) is resolved against a repository /// when the filter is applied or persisted; `persist::as_tree` references the object as a /// tree entry with the matching mode so it is reachable from the filter tree. - Oid(git2::Oid), + Oid(gix_hash::ObjectId), } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] @@ -98,7 +98,7 @@ impl LazyRef { return Ok(LazyRef::Lazy(s)); } - if let Ok(oid) = git2::Oid::from_str(&s) { + if let Ok(oid) = s.parse() { Ok(LazyRef::Resolved(oid)) } else { Err(anyhow!("invalid ref: {:?}", s)) diff --git a/josh-filter/src/persist.rs b/josh-filter/src/persist.rs index 6968a92cb..b2dd164f1 100644 --- a/josh-filter/src/persist.rs +++ b/josh-filter/src/persist.rs @@ -12,7 +12,7 @@ use crate::op::{InsertContent, LazyRef, Op, Regex, RevMatch}; /// most once, on demand, via `Filter::id`/`build_node_oid` — never during `to_filter`. pub(crate) struct Node { pub(crate) op: Op, - pub(crate) oid: OnceLock, + pub(crate) oid: OnceLock, } /// Canonicalizes each `Op` to a single interned node by structural equality, so equal `Op`s @@ -121,7 +121,7 @@ impl<'a> InMemoryBuilder<'a> { self.persist_ids .get(&filter) .copied() - .unwrap_or_else(|| gix_hash::ObjectId::from_bytes_or_panic(filter.id().as_bytes())) + .unwrap_or_else(|| filter.id()) } fn write_blob(&mut self, data: &[u8]) -> gix_hash::ObjectId { @@ -403,7 +403,7 @@ impl<'a> InMemoryBuilder<'a> { InsertContent::Oid(oid) => { if let Some(src) = self.src { let kind = src - .try_header(&josh_gix_ext::gix_oid(*oid)) + .try_header(oid.as_ref()) .map_err(|e| anyhow!("insert: object {}: {}", oid, e))? .map(|header| header.kind); let mode = match kind { @@ -426,7 +426,7 @@ impl<'a> InMemoryBuilder<'a> { gix_object::tree::Entry { mode: mode.into(), filename: BString::from("o"), - oid: gix_hash::ObjectId::from_bytes_or_panic(oid.as_bytes()), + oid: *oid, }, ]; self.write_tree(gix_object::Tree { entries }) @@ -594,17 +594,16 @@ pub fn to_filter(op: Op) -> Filter { /// Materialize a node's content OID, building its tree (and, recursively via `build_op`'s child /// `Filter::id` calls, its children's). Called only from `Filter::id`, never on the optimizer /// hot path. -pub(crate) fn build_node_oid(node: &'static Node) -> git2::Oid { +pub(crate) fn build_node_oid(node: &'static Node) -> gix_hash::ObjectId { let mut builder = InMemoryBuilder::new(None); - let tree_id = builder.build_op(&node.op).expect("failed to build op"); - git2::Oid::from_bytes(tree_id.as_bytes()).unwrap() + builder.build_op(&node.op).expect("failed to build op") } /// Construct a sentinel filter: a unique leaked node whose OID is pre-seeded to `oid` (so it /// never goes through `build_op`) and whose op is `Nop`. Sentinels bypass interning, so each /// is a distinct node — pointer-identity equality keeps them distinct from the real `Nop` /// filter and from each other, while `to_op_ref` still yields `Nop`. -pub(crate) fn sentinel(oid: git2::Oid) -> Filter { +pub(crate) fn sentinel(oid: gix_hash::ObjectId) -> Filter { let node: &'static Node = Box::leak(Box::new(Node { op: Op::Nop, oid: OnceLock::new(), @@ -637,10 +636,10 @@ impl<'a> InMemoryBuilder<'a> { let oid = if dirty { self.build_op(op)? } else { - if !out.exists(&josh_gix_ext::gix_oid(filter.id())) { + if !out.exists(filter.id().as_ref()) { self.build_op(op)?; } - gix_hash::ObjectId::from_bytes_or_panic(filter.id().as_bytes()) + filter.id() }; self.persist_ids.insert(filter, oid); Ok(oid) @@ -650,12 +649,11 @@ impl<'a> InMemoryBuilder<'a> { pub fn as_tree( out: &(impl gix_object::FindHeader + gix_object::Exists + gix_object::Write), filter: Filter, -) -> anyhow::Result { +) -> anyhow::Result { let filter = crate::opt::optimize(filter); let mut builder = InMemoryBuilder::new(Some(out)); let root_oid = builder.build_persist(out, filter)?; - let root_oid = git2::Oid::from_bytes(root_oid.as_bytes())?; builder.staging.flush(out)?; @@ -755,8 +753,11 @@ impl Blob { } } -pub fn from_tree(src: &impl gix_object::Find, tree_oid: git2::Oid) -> anyhow::Result { - Ok(to_filter(from_tree2(src, josh_gix_ext::gix_oid(tree_oid))?)) +pub fn from_tree( + src: &impl gix_object::Find, + tree_oid: gix_hash::ObjectId, +) -> anyhow::Result { + Ok(to_filter(from_tree2(src, tree_oid)?)) } fn from_tree2(src: &impl gix_object::Find, tree_oid: gix_hash::ObjectId) -> anyhow::Result { @@ -918,7 +919,7 @@ fn from_tree2(src: &impl gix_object::Find, tree_oid: gix_hash::ObjectId) -> anyh if let Some(obj_entry) = inner.get_name("o") { return Ok(Op::Insert( std::path::PathBuf::from(path), - InsertContent::Oid(josh_gix_ext::git2_oid(&obj_entry.id())), + InsertContent::Oid(obj_entry.id()), )); } let kind_blob = Blob::read( @@ -1182,7 +1183,10 @@ fn from_tree2(src: &impl gix_object::Find, tree_oid: gix_hash::ObjectId) -> anyh // Parse match operator from key let (match_op, lazy_ref) = if key == "_" { // Default filter - no SHA needed - (RevMatch::Default, LazyRef::Resolved(git2::Oid::ZERO_SHA1)) + ( + RevMatch::Default, + LazyRef::Resolved(gix_hash::ObjectId::null(gix_hash::Kind::Sha1)), + ) } else if let Some(ref_str) = key.strip_prefix("<=") { (RevMatch::AncestorInclusive, LazyRef::parse(ref_str)?) } else if let Some(ref_str) = key.strip_prefix('<') { diff --git a/josh-git-serde/Cargo.toml b/josh-git-serde/Cargo.toml index 34b74f79c..be6ccdf50 100644 --- a/josh-git-serde/Cargo.toml +++ b/josh-git-serde/Cargo.toml @@ -10,13 +10,11 @@ edition = "2024" [dependencies] anyhow.workspace = true -git2.workspace = true gix-object.workspace = true +gix-hash.workspace = true percent-encoding = "2.3.2" serde.workspace = true thiserror.workspace = true -josh-gix-ext.workspace = true - [dev-dependencies] -gix-hash.workspace = true +josh-gix-ext.workspace = true diff --git a/josh-git-serde/src/store.rs b/josh-git-serde/src/store.rs index 22c60026b..376b6577e 100644 --- a/josh-git-serde/src/store.rs +++ b/josh-git-serde/src/store.rs @@ -16,9 +16,14 @@ use crate::value::GitValue; /// Tree entry names must be single path components: non-empty, no '/' or /// NUL, not "." or "..". Keys produced by the serializer always satisfy /// this; the check exists because [`GitValue`] is public. -pub fn to_tree_oid(out: &impl gix_object::Write, value: &GitValue) -> anyhow::Result { +pub fn to_tree_oid( + out: &impl gix_object::Write, + value: &GitValue, +) -> anyhow::Result { match value { - GitValue::Blob(data) => josh_gix_ext::write_blob(out, data), + GitValue::Blob(data) => out + .write_buf(gix_object::Kind::Blob, data) + .map_err(|e| anyhow::anyhow!("to_tree_oid: {e}")), GitValue::Tree(entries) => { let mut tree_entries = Vec::with_capacity(entries.len()); for (name, child) in entries { @@ -30,11 +35,14 @@ pub fn to_tree_oid(out: &impl gix_object::Write, value: &GitValue) -> anyhow::Re GitValue::Blob(_) => gix_object::tree::EntryKind::Blob.into(), }, filename: name.as_str().into(), - oid: josh_gix_ext::gix_oid(oid), + oid, }); } tree_entries.sort(); - josh_gix_ext::write_tree_now(out, tree_entries) + out.write(&gix_object::Tree { + entries: tree_entries, + }) + .map_err(|e| anyhow::anyhow!("to_tree_oid: {e}")) } } } @@ -50,7 +58,10 @@ pub fn to_tree_oid(out: &impl gix_object::Write, value: &GitValue) -> anyhow::Re /// Nesting deeper than [`MAX_TREE_DEPTH`] is rejected rather than risking a /// stack overflow on adversarial objects, as are duplicate entry names /// (possible only in hand-crafted trees). -pub fn from_tree_oid(source: &impl gix_object::Find, root: git2::Oid) -> anyhow::Result { +pub fn from_tree_oid( + source: &impl gix_object::Find, + root: gix_hash::ObjectId, +) -> anyhow::Result { read_tree_oid(source, root, 0) } @@ -65,7 +76,7 @@ fn validate_entry_name(name: &str) -> anyhow::Result<()> { fn read_tree_oid( source: &impl gix_object::Find, - root: git2::Oid, + root: gix_hash::ObjectId, depth: usize, ) -> anyhow::Result { if depth > MAX_TREE_DEPTH { @@ -76,7 +87,7 @@ fn read_tree_oid( let mut buf = Vec::new(); let data = source - .try_find(&josh_gix_ext::gix_oid(root), &mut buf) + .try_find(&root, &mut buf) .map_err(|e| anyhow::anyhow!("from_tree_oid: {e}"))? .ok_or_else(|| anyhow::anyhow!("from_tree_oid: object {root} not found"))?; let object_hash = data.object_hash; @@ -98,7 +109,7 @@ fn read_tree_oid( entry.mode )); } - let child = read_tree_oid(source, josh_gix_ext::git2_oid(entry.oid), depth + 1)?; + let child = read_tree_oid(source, entry.oid.to_owned(), depth + 1)?; if entries.insert(name.clone(), Box::new(child)).is_some() { return Err(anyhow::anyhow!( "from_tree_oid: duplicate entry name {name:?}" diff --git a/josh-git-serde/tests/store.rs b/josh-git-serde/tests/store.rs index f9e8b04d4..6e17e63e4 100644 --- a/josh-git-serde/tests/store.rs +++ b/josh-git-serde/tests/store.rs @@ -100,7 +100,11 @@ fn blob_root_roundtrips() { let stage = Stage::new(); let value = GitValue::blob_from_str("lone blob"); let root = to_tree_oid(&stage, &value).unwrap(); - assert_eq!(root, josh_gix_ext::hash_blob(b"lone blob")); + assert_eq!( + root, + gix_object::compute_hash(gix_hash::Kind::Sha1, gix_object::Kind::Blob, b"lone blob") + .unwrap() + ); let back = from_tree_oid(&*stage.0.borrow(), root).unwrap(); assert_eq!(value, back); } @@ -127,10 +131,7 @@ fn written_tree_is_in_canonical_order() { let staging = stage.0.borrow(); let mut buf = Vec::new(); let (kind, object_hash) = { - let data = staging - .try_find(&josh_gix_ext::gix_oid(root), &mut buf) - .unwrap() - .unwrap(); + let data = staging.try_find(&root, &mut buf).unwrap().unwrap(); (data.kind, data.object_hash) }; assert_eq!(kind, gix_object::Kind::Tree); @@ -161,7 +162,7 @@ fn written_tree_is_in_canonical_order() { let name: &[u8] = entry.filename.as_ref(); if name == b"foo" { assert_eq!(entry.mode.kind(), gix_object::tree::EntryKind::Tree); - let sub = from_tree_oid(&*staging, josh_gix_ext::git2_oid(entry.oid)).unwrap(); + let sub = from_tree_oid(&*staging, entry.oid.to_owned()).unwrap(); assert_eq!( sub, GitValue::Tree(BTreeMap::from([( @@ -184,7 +185,7 @@ fn commit_object_is_rejected() { let mut staging = StagingOdb::new(); // Contents are irrelevant: the kind alone must trigger the error. let commit = staging.write_raw(gix_object::Kind::Commit, b"tree".to_vec()); - let err = from_tree_oid(&staging, josh_gix_ext::git2_oid(&commit)).unwrap_err(); + let err = from_tree_oid(&staging, commit).unwrap_err(); assert!( err.to_string().contains("not a tree or a blob"), "unexpected error: {err}" @@ -194,7 +195,7 @@ fn commit_object_is_rejected() { #[test] fn missing_object_is_an_error() { let staging = StagingOdb::new(); - let missing = git2::Oid::from_str("0123456789012345678901234567890123456789").unwrap(); + let missing = gix_hash::ObjectId::from_bytes_or_panic(&[1; 20]); let err = from_tree_oid(&staging, missing).unwrap_err(); assert!( err.to_string().contains("not found"), @@ -222,7 +223,7 @@ fn executable_blob_entry_is_rejected() { gix_object::Kind::Tree, raw_tree(&[("100755", "exec", &blob)]), ); - let err = from_tree_oid(&staging, josh_gix_ext::git2_oid(&tree)).unwrap_err(); + let err = from_tree_oid(&staging, tree).unwrap_err(); assert!( err.to_string().contains("unsupported mode"), "unexpected error: {err}" @@ -238,7 +239,7 @@ fn duplicate_entry_names_are_rejected() { gix_object::Kind::Tree, raw_tree(&[("100644", "dup", &a), ("100644", "dup", &b)]), ); - let err = from_tree_oid(&staging, josh_gix_ext::git2_oid(&tree)).unwrap_err(); + let err = from_tree_oid(&staging, tree).unwrap_err(); assert!( err.to_string().contains("duplicate entry name"), "unexpected error: {err}" diff --git a/josh-gui/Cargo.lock b/josh-gui/Cargo.lock index fca0874ef..cd743e6ac 100644 --- a/josh-gui/Cargo.lock +++ b/josh-gui/Cargo.lock @@ -138,7 +138,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -149,7 +149,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -1768,7 +1768,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2001,7 +2001,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4265,7 +4265,6 @@ name = "josh-filter" version = "26.7.28" dependencies = [ "anyhow", - "git2", "gix-hash", "gix-object", "glob", @@ -4285,9 +4284,8 @@ name = "josh-git-serde" version = "26.7.28" dependencies = [ "anyhow", - "git2", + "gix-hash", "gix-object", - "josh-gix-ext", "percent-encoding", "serde", "thiserror 2.0.20", @@ -6044,7 +6042,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -6530,7 +6528,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -6587,7 +6585,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -7130,7 +7128,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -7534,10 +7532,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.3", + "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -8393,7 +8391,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] diff --git a/josh-memodb/src/pack.rs b/josh-memodb/src/pack.rs index ccf41ab5a..289c8bc84 100644 --- a/josh-memodb/src/pack.rs +++ b/josh-memodb/src/pack.rs @@ -6,6 +6,7 @@ //! on disk are filtered out first, so a pack contains only genuinely-new objects and the on-disk //! layout stays deterministic. +use anyhow::Context; use std::io::{Seek, Write}; use std::path::Path; use std::sync::atomic::AtomicBool; @@ -34,8 +35,7 @@ pub(crate) fn write_snapshot(objects_dir: &Path, snapshot: &Snapshot) -> anyhow: // the expected case below, and the default refresh mode re-lists the pack directory on every // miss — disable it; the first lookup still loads all indices present now, and loose-object // probes stat the filesystem directly either way. - let mut odb = gix_odb::at(objects_dir.to_owned()) - .map_err(|e| anyhow::anyhow!("mem-odb pack write failed: {e}"))?; + let mut odb = gix_odb::at(objects_dir.to_owned()).context("mem-odb pack write failed")?; odb.refresh = gix_odb::store::RefreshMode::Never; let to_pack: Vec<_> = snapshot @@ -46,12 +46,10 @@ pub(crate) fn write_snapshot(objects_dir: &Path, snapshot: &Snapshot) -> anyhow: if to_pack.is_empty() { return Ok(()); } - let num_entries = u32::try_from(to_pack.len()) - .map_err(|e| anyhow::anyhow!("mem-odb pack write failed: {e}"))?; + let num_entries = u32::try_from(to_pack.len()).context("mem-odb pack write failed")?; let pack_dir = objects_dir.join("pack"); - std::fs::create_dir_all(&pack_dir) - .map_err(|e| anyhow::anyhow!("mem-odb pack write failed: {e}"))?; + std::fs::create_dir_all(&pack_dir).context("mem-odb pack write failed")?; // Serialize the pack byte stream (header, compressed entries, checksum trailer) through an // anonymous spool file in the pack directory: objects are compressed one at a time as the @@ -59,8 +57,7 @@ pub(crate) fn write_snapshot(objects_dir: &Path, snapshot: &Snapshot) -> anyhow: // a snapshot's size is only *typically* bounded by the store's chunk limit (unbounded stores // exist, and the limit is an overflow trigger, not a cap). let mut spool = std::io::BufWriter::new( - tempfile::tempfile_in(&pack_dir) - .map_err(|e| anyhow::anyhow!("mem-odb pack write failed: {e}"))?, + tempfile::tempfile_in(&pack_dir).context("mem-odb pack write failed")?, ); let mut iter = output::bytes::FromEntriesIter::new( to_pack.iter().map(|(oid, kind, data)| { @@ -78,18 +75,12 @@ pub(crate) fn write_snapshot(objects_dir: &Path, snapshot: &Snapshot) -> anyhow: gix_hash::Kind::Sha1, ); for written in &mut iter { - written.map_err(|e| anyhow::anyhow!("mem-odb pack write failed: {e}"))?; + written.context("mem-odb pack write failed")?; } drop(iter); - spool - .flush() - .map_err(|e| anyhow::anyhow!("mem-odb pack write failed: {e}"))?; - let mut spool = spool - .into_inner() - .map_err(|e| anyhow::anyhow!("mem-odb pack write failed: {e}"))?; - spool - .rewind() - .map_err(|e| anyhow::anyhow!("mem-odb pack write failed: {e}"))?; + spool.flush().context("mem-odb pack write failed")?; + let mut spool = spool.into_inner().context("mem-odb pack write failed")?; + spool.rewind().context("mem-odb pack write failed")?; let outcome = gix_pack::Bundle::write_to_directory( &mut std::io::BufReader::new(spool), @@ -107,7 +98,7 @@ pub(crate) fn write_snapshot(objects_dir: &Path, snapshot: &Snapshot) -> anyhow: compression: gix_zlib::Compression::DEFAULT, }, ) - .map_err(|e| anyhow::anyhow!("mem-odb pack write failed: {e}"))?; + .context("mem-odb pack write failed")?; // gix marks the freshly-landed pack with a `.keep` file for the caller to remove once its // referencing refs exist. josh's flushes carry no such handshake (libgit2's packbuilder wrote // no `.keep` either), and a leftover one would exempt the pack from `git repack -d` forever. @@ -118,7 +109,7 @@ pub(crate) fn write_snapshot(objects_dir: &Path, snapshot: &Snapshot) -> anyhow: if let Some(data_path) = &outcome.data_path { if let Err(e) = std::fs::remove_file(data_path.with_extension("keep")) { if e.kind() != std::io::ErrorKind::NotFound { - return Err(anyhow::anyhow!("mem-odb pack write failed: {e}")); + return Err(e).context("mem-odb pack write failed"); } } } From a6c7fa5037a0c487f8e787bad6e8b0e89474a985 Mon Sep 17 00:00:00 2001 From: Christian Schilling Date: Wed, 26 Aug 2026 00:11:11 +0200 Subject: [PATCH 4/5] Move workspace object IDs to gitoxide Use gix_hash::ObjectId across core APIs, leaf crates, GUI, tests, and benchmarks. Keep explicit conversions only at libgit2 porcelain and fixture boundaries, and move pull history traversal onto Josh's native walkers. Remove mechanically ported cache key code, tighten migration comments, and keep tests focused on Josh-owned behavior and boundary contracts. Change: gix-workspace-oid-currency Assisted-By: openai-codex/gpt-5.6-sol --- Cargo.lock | 16 +- cq/josh-cq/Cargo.toml | 1 + cq/josh-cq/src/cq.rs | 2 +- cq/josh-cq/src/remote.rs | 7 +- devtools/josh-test-support/src/bench.rs | 16 +- .../josh-test-support/src/provision_repo.rs | 18 +- forges/josh-github-changes/Cargo.toml | 2 +- forges/josh-github-changes/src/prs.rs | 9 +- forges/josh-github-graphql/Cargo.toml | 2 +- .../src/operations/create_check_run.rs | 2 +- josh-changes/Cargo.toml | 1 + josh-changes/src/change.rs | 62 ++- josh-changes/src/comments.rs | 27 +- josh-changes/src/forges/gerrit.rs | 24 +- josh-changes/src/refs.rs | 3 +- josh-changes/src/revisions.rs | 2 +- josh-changes/src/stacked.rs | 6 +- josh-changes/src/store.rs | 42 +- josh-changes/src/votes.rs | 8 +- josh-cli/Cargo.toml | 4 + josh-cli/src/bin/josh-filter.rs | 36 +- josh-cli/src/bin/josh.rs | 6 +- josh-cli/src/commands/cache.rs | 5 +- josh-cli/src/commands/changes.rs | 8 +- josh-cli/src/commands/link.rs | 43 +- josh-cli/src/commands/pull.rs | 115 +++-- josh-cli/src/commands/push.rs | 9 +- josh-cli/src/commands/sync.rs | 8 +- josh-cli/src/forge/github/changes.rs | 19 +- josh-cli/src/porcelain.rs | 27 +- josh-cli/src/remote_ops.rs | 25 +- josh-compose/Cargo.toml | 2 +- josh-compose/src/archive.rs | 6 +- josh-compose/src/container.rs | 7 +- josh-compose/src/filter.rs | 6 +- josh-compose/src/image.rs | 8 +- josh-compose/src/lib.rs | 4 +- josh-compose/src/meta.rs | 34 +- josh-compose/src/naming.rs | 4 +- josh-compose/src/plan.rs | 47 +- josh-core/benches/deephistory_glob.rs | 33 +- josh-core/benches/deephistory_prefix_flush.rs | 17 +- josh-core/benches/deephistory_subdir.rs | 32 +- .../benches/deephistory_subdir_distributed.rs | 34 +- .../benches/deephistory_subdir_sparse.rs | 34 +- josh-core/benches/refs_filter_update.rs | 19 +- josh-core/benches/ultrawide_pin.rs | 20 +- josh-core/benches/ultrawide_pin_hook.rs | 39 +- josh-core/benches/unapply.rs | 43 +- josh-core/benches/widetree_glob.rs | 27 +- josh-core/src/cache/backend.rs | 8 +- josh-core/src/cache/distributed.rs | 44 +- josh-core/src/cache/history_graph.rs | 73 +-- josh-core/src/cache/sled.rs | 10 +- josh-core/src/cache/stack.rs | 8 +- josh-core/src/cache/transaction.rs | 321 +++++++----- josh-core/src/cache/tree_cache.rs | 50 +- josh-core/src/filter/mod.rs | 332 ++++++------ josh-core/src/filter/tree.rs | 487 +++++++++--------- josh-core/src/git.rs | 50 +- josh-core/src/history.rs | 168 +++--- josh-core/src/housekeeping.rs | 47 +- josh-core/src/lib.rs | 17 +- josh-core/src/link.rs | 2 +- josh-core/tests/cache_lock.rs | 3 +- josh-gix-ext/src/graph.rs | 110 +--- josh-gix-ext/src/lib.rs | 210 ++++---- josh-gix-ext/src/merge.rs | 93 ++-- josh-gix-ext/src/revwalk.rs | 214 ++++---- josh-graphql/Cargo.toml | 1 + josh-graphql/src/graphql.rs | 69 +-- josh-gui/Cargo.lock | 9 +- josh-gui/Cargo.toml | 1 + josh-gui/src/detail.rs | 17 +- josh-gui/src/diff.rs | 7 +- josh-gui/src/list.rs | 8 +- josh-gui/src/main.rs | 2 +- josh-link/Cargo.toml | 1 + josh-link/src/lib.rs | 24 +- josh-proxy/Cargo.toml | 1 + josh-proxy/src/lib.rs | 9 +- josh-proxy/src/service.rs | 16 +- josh-proxy/src/upstream.rs | 9 +- josh-search/Cargo.toml | 2 +- josh-search/benches/trigram.rs | 83 +-- josh-search/src/lib.rs | 219 ++++---- josh-starlark/Cargo.toml | 5 +- josh-starlark/src/evaluate.rs | 2 +- josh-starlark/src/filter.rs | 10 +- josh-starlark/src/tests.rs | 13 +- josh-starlark/src/tree.rs | 14 +- josh-templates/Cargo.toml | 2 +- josh-templates/src/templates.rs | 8 +- 93 files changed, 1999 insertions(+), 1751 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 88b0f1e79..557045a49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3313,6 +3313,7 @@ dependencies = [ "anyhow", "chrono", "git2", + "gix-hash", "josh-core", "josh-git-serde", "serde", @@ -3330,6 +3331,7 @@ dependencies = [ "dirs", "env_logger", "git2", + "gix-hash", "glob", "josh-changes", "josh-compose", @@ -3349,6 +3351,7 @@ dependencies = [ "reqwest-middleware", "serde", "serde_json", + "tempfile", "tokio", ] @@ -3368,7 +3371,7 @@ version = "26.7.28" dependencies = [ "anyhow", "defer", - "git2", + "gix-hash", "josh-compose-backend", "josh-core", "josh-filter", @@ -3448,6 +3451,7 @@ dependencies = [ "axum", "clap", "git2", + "gix-hash", "josh-core", "josh-github-webhooks", "josh-link", @@ -3535,7 +3539,7 @@ name = "josh-github-changes" version = "26.7.28" dependencies = [ "anyhow", - "git2", + "gix-hash", "josh-changes", "josh-core", "josh-git-serde", @@ -3567,7 +3571,7 @@ version = "26.7.28" dependencies = [ "anyhow", "async-trait", - "git2", + "gix-hash", "graphql_client", "http", "josh-github-auth", @@ -3639,6 +3643,7 @@ dependencies = [ "anyhow", "chrono", "git2", + "gix-hash", "glob", "josh-core", "josh-search", @@ -3657,6 +3662,7 @@ version = "26.7.28" dependencies = [ "anyhow", "git2", + "gix-hash", "josh-core", ] @@ -3693,6 +3699,7 @@ dependencies = [ "futures", "git2", "gix", + "gix-hash", "gix-packetline", "gix-transport", "hex", @@ -3782,6 +3789,7 @@ dependencies = [ "allocative", "anyhow", "git2", + "gix-hash", "gix-object", "josh-filter", "josh-gix-ext", @@ -3794,7 +3802,7 @@ version = "26.7.28" dependencies = [ "anyhow", "form_urlencoded", - "git2", + "gix-hash", "handlebars", "josh-core", "josh-graphql", diff --git a/cq/josh-cq/Cargo.toml b/cq/josh-cq/Cargo.toml index a9ae9da2b..a05a29d0d 100644 --- a/cq/josh-cq/Cargo.toml +++ b/cq/josh-cq/Cargo.toml @@ -14,6 +14,7 @@ anyhow.workspace = true serde.workspace = true serde_json.workspace = true git2.workspace = true +gix-hash.workspace = true axum.workspace = true tracing.workspace = true diff --git a/cq/josh-cq/src/cq.rs b/cq/josh-cq/src/cq.rs index ed4e7d1de..5dde8412b 100644 --- a/cq/josh-cq/src/cq.rs +++ b/cq/josh-cq/src/cq.rs @@ -68,7 +68,7 @@ pub fn handle_track( url, None, "HEAD", - fetched_commit, + josh_core::objects::gix_oid(fetched_commit), josh_core::objects::CommitData::read(transaction.odb(), head.commit)?.tree_id()?, link_mode, )? diff --git a/cq/josh-cq/src/remote.rs b/cq/josh-cq/src/remote.rs index e9c1c7ed5..33624a33a 100644 --- a/cq/josh-cq/src/remote.rs +++ b/cq/josh-cq/src/remote.rs @@ -1,5 +1,6 @@ use anyhow::Context; use anyhow::anyhow; +use std::str::FromStr; use std::collections::BTreeMap; use std::process::Command; @@ -7,7 +8,7 @@ use std::process::Command; /// List refs from a remote repository using git ls-remote /// /// Returns a map of ref names to their OIDs -pub fn list_refs(url: &str) -> anyhow::Result> { +pub fn list_refs(url: &str) -> anyhow::Result> { let output = Command::new("git") .args(["ls-remote", url]) .output() @@ -19,12 +20,12 @@ pub fn list_refs(url: &str) -> anyhow::Result> { } let stdout = String::from_utf8(output.stdout)?; - let refs: BTreeMap = stdout + let refs: BTreeMap = stdout .lines() .filter_map(|line| { let parts: Vec<&str> = line.split('\t').collect(); if parts.len() == 2 { - let oid = git2::Oid::from_str(parts[0]).ok()?; + let oid = gix_hash::ObjectId::from_str(parts[0]).ok()?; Some((parts[1].to_string(), oid)) } else { None diff --git a/devtools/josh-test-support/src/bench.rs b/devtools/josh-test-support/src/bench.rs index e116a25ae..34eb21859 100644 --- a/devtools/josh-test-support/src/bench.rs +++ b/devtools/josh-test-support/src/bench.rs @@ -33,12 +33,12 @@ pub fn random_string(rng: &mut StdRng, len: usize) -> String { pub fn build_index( repo: &git2::Repository, sig: &git2::Signature, - heads: &[git2::Oid], -) -> Result { + heads: &[gix::ObjectId], +) -> Result { let empty_tree = repo.find_tree(repo.treebuilder(None)?.write()?)?; let parents = heads .iter() - .map(|oid| repo.find_commit(*oid)) + .map(|oid| repo.find_commit(git2_oid(*oid))) .collect::, _>>()?; let parent_refs = parents.iter().collect::>(); let index = repo.commit( @@ -49,7 +49,7 @@ pub fn build_index( &empty_tree, &parent_refs, )?; - Ok(index) + Ok(gix_oid(index)) } /// Rebuild, with plain git2 tree walking (no josh code), the tree a pattern filter must produce: @@ -60,10 +60,10 @@ pub fn build_index( /// `require_literal_leading_dot`; a glob-based predicate is exact regardless). pub fn expected_tree( repo: &git2::Repository, - head: git2::Oid, + head: gix::ObjectId, keep: &dyn Fn(&str) -> bool, -) -> Result<(git2::Oid, usize)> { - let tree = repo.find_commit(head)?.tree()?; +) -> Result<(gix::ObjectId, usize)> { + let tree = repo.find_commit(git2_oid(head))?.tree()?; let mut kept: Vec<(String, git2::Oid, i32)> = vec![]; tree.walk(git2::TreeWalkMode::PreOrder, |root, entry| { if entry.kind() == Some(git2::ObjectType::Blob) { @@ -85,5 +85,5 @@ pub fn expected_tree( }; builder.upsert(path.as_str(), mode, gix_oid(*oid))?; } - Ok((git2_oid(builder.write()?.detach()), kept.len())) + Ok((builder.write()?.detach(), kept.len())) } diff --git a/devtools/josh-test-support/src/provision_repo.rs b/devtools/josh-test-support/src/provision_repo.rs index ff8c404ea..a97c7304e 100644 --- a/devtools/josh-test-support/src/provision_repo.rs +++ b/devtools/josh-test-support/src/provision_repo.rs @@ -20,7 +20,7 @@ pub struct ProvisionedRepo { pub repo: git2::Repository, /// The head oid the callback produced (always equal to the `expected` that /// was passed to [`provision_repo`]). - pub head: git2::Oid, + pub head: gix::ObjectId, } impl ProvisionedRepo { @@ -46,11 +46,11 @@ impl ProvisionedRepo { /// new `expected`. pub fn provision_repo( testcase: &str, - expected: &git2::Oid, + expected: &gix::ObjectId, callback: C, ) -> Result where - C: FnMut(&git2::Repository) -> Result, + C: FnMut(&git2::Repository) -> Result, { let expected = *expected; let cache_root = cache_root_for(testcase)?; @@ -71,17 +71,19 @@ fn cache_root_for(testcase: &str) -> Result { } /// A cached repo is reusable if it opens as a bare repo and contains `expected`. -fn cache_hit(cache_root: &Path, expected: git2::Oid) -> bool { +fn cache_hit(cache_root: &Path, expected: gix::ObjectId) -> bool { let Ok(repo) = git2::Repository::open_bare(cache_root) else { return false; }; - repo.odb().map(|odb| odb.exists(expected)).unwrap_or(false) + repo.odb() + .map(|odb| odb.exists(crate::bench::git2_oid(expected))) + .unwrap_or(false) } /// Build `testcase` from scratch into `cache_root`, erasing any prior cache. -fn rebuild(cache_root: &Path, expected: git2::Oid, callback: &mut C) -> Result<()> +fn rebuild(cache_root: &Path, expected: gix::ObjectId, callback: &mut C) -> Result<()> where - C: FnMut(&git2::Repository) -> Result, + C: FnMut(&git2::Repository) -> Result, { if cache_root.exists() { std::fs::remove_dir_all(cache_root) @@ -130,7 +132,7 @@ where } /// Copy the canonical cached repo into a fresh tempdir and open it. -fn copy_to_tempdir(cache_root: &Path, expected: git2::Oid) -> Result { +fn copy_to_tempdir(cache_root: &Path, expected: gix::ObjectId) -> Result { let tmp = tempfile::tempdir().context("creating tempdir for repo copy")?; copy_dir_recursive(cache_root, tmp.path()) .with_context(|| format!("copying {} to tempdir", cache_root.display()))?; diff --git a/forges/josh-github-changes/Cargo.toml b/forges/josh-github-changes/Cargo.toml index 8d1cb928d..987e15385 100644 --- a/forges/josh-github-changes/Cargo.toml +++ b/forges/josh-github-changes/Cargo.toml @@ -9,7 +9,7 @@ repository = "https://github.com/josh-project/josh" keywords = ["git", "github", "monorepo", "workflow"] [dependencies] -git2.workspace = true +gix-hash.workspace = true anyhow.workspace = true url.workspace = true serde.workspace = true diff --git a/forges/josh-github-changes/src/prs.rs b/forges/josh-github-changes/src/prs.rs index 15f5d6aca..438659e67 100644 --- a/forges/josh-github-changes/src/prs.rs +++ b/forges/josh-github-changes/src/prs.rs @@ -31,7 +31,7 @@ pub fn read_pr_data( pub struct PrInfo { pub head_branch: String, pub base_branch: String, - pub base_oid: git2::Oid, + pub base_oid: gix_hash::ObjectId, pub title: String, pub body: String, } @@ -46,8 +46,8 @@ pub fn collect_pr_infos( struct ByIdEntry { head_branch: Option, base_branch: Option, - head_oid: Option, - base_oid: Option, + head_oid: Option, + base_oid: Option, } fn branch_name(refname: &str) -> &str { @@ -313,6 +313,7 @@ pub async fn create_or_update_prs( #[cfg(test)] mod tests { use super::*; + use std::str::FromStr; const TIP: &str = "1111111111111111111111111111111111111111"; const OTHER: &str = "2222222222222222222222222222222222222222"; @@ -321,7 +322,7 @@ mod tests { PrInfo { head_branch: "@changes/main/a@b.com/feature".to_string(), base_branch: "@base/main/a@b.com/feature".to_string(), - base_oid: git2::Oid::from_str(base_oid).unwrap(), + base_oid: gix_hash::ObjectId::from_str(base_oid).unwrap(), title: "t".to_string(), body: "b".to_string(), } diff --git a/forges/josh-github-graphql/Cargo.toml b/forges/josh-github-graphql/Cargo.toml index 3a15fe1ac..1db9e7422 100644 --- a/forges/josh-github-graphql/Cargo.toml +++ b/forges/josh-github-graphql/Cargo.toml @@ -13,7 +13,7 @@ doctest = false [dependencies] http.workspace = true -git2.workspace = true +gix-hash.workspace = true serde.workspace = true serde_json.workspace = true reqwest.workspace = true diff --git a/forges/josh-github-graphql/src/operations/create_check_run.rs b/forges/josh-github-graphql/src/operations/create_check_run.rs index c874d0521..57d03d183 100644 --- a/forges/josh-github-graphql/src/operations/create_check_run.rs +++ b/forges/josh-github-graphql/src/operations/create_check_run.rs @@ -8,7 +8,7 @@ use crate::connection::GithubApiConnection; impl GithubApiConnection { pub async fn create_check_run( &self, - head_sha: &git2::Oid, + head_sha: &gix_hash::ObjectId, name: &str, repository_id: &str, status: create_check_run::RequestableCheckStatusState, diff --git a/josh-changes/Cargo.toml b/josh-changes/Cargo.toml index 3d0513b26..0a296ac9d 100644 --- a/josh-changes/Cargo.toml +++ b/josh-changes/Cargo.toml @@ -12,6 +12,7 @@ keywords = ["git", "monorepo", "workflow", "scm"] anyhow.workspace = true chrono.workspace = true git2.workspace = true +gix-hash.workspace = true serde.workspace = true josh-core.workspace = true diff --git a/josh-changes/src/change.rs b/josh-changes/src/change.rs index 69df9667d..6e42c3f61 100644 --- a/josh-changes/src/change.rs +++ b/josh-changes/src/change.rs @@ -3,20 +3,21 @@ use crate::store::store_diff_data; use anyhow::{Context, anyhow}; use josh_core::objects; use josh_core::trailers::{commit_change_meta, parse_change_meta}; +use std::str::FromStr; #[derive(Debug, Clone)] pub struct Change { pub(crate) author: String, pub(crate) id: Option, pub(crate) series: Vec, - pub(crate) commit: git2::Oid, - pub(crate) base: git2::Oid, + pub(crate) commit: gix_hash::ObjectId, + pub(crate) base: gix_hash::ObjectId, } impl Change { pub fn new( transaction: &josh_core::cache::Transaction, - commit: git2::Oid, + commit: gix_hash::ObjectId, ) -> anyhow::Result { Ok(Self::from_commit(&objects::CommitData::read( transaction.odb(), @@ -35,7 +36,7 @@ impl Change { id: None, series: Vec::new(), commit: commit.id(), - base: git2::Oid::ZERO_SHA1, + base: gix_hash::ObjectId::null(gix_hash::Kind::Sha1), }; let (id, series) = commit_change_meta(commit); change.id = id; @@ -56,29 +57,31 @@ impl Change { &self.series } - pub fn commit(&self) -> git2::Oid { + pub fn commit(&self) -> gix_hash::ObjectId { self.commit } - pub fn base(&self) -> git2::Oid { + pub fn base(&self) -> gix_hash::ObjectId { self.base } - pub fn set_base(&mut self, base: git2::Oid) { + pub fn set_base(&mut self, base: gix_hash::ObjectId) { self.base = base; } pub fn contributing( &self, transaction: &josh_core::cache::Transaction, - ) -> anyhow::Result> { + ) -> anyhow::Result> { // First-parent walk down to (but not including) the base. let odb = transaction.odb(); let mut walk = objects::RevWalk::new(odb); walk.simplify_first_parent(); walk.push(self.commit)?; let base = self.base; - let mut oids = walk.into_topo_vec(|oid| base != git2::Oid::ZERO_SHA1 && oid == base)?; + let mut oids = walk.into_topo_vec(|oid| { + base != gix_hash::ObjectId::null(gix_hash::Kind::Sha1) && oid == base + })?; oids.retain(|oid| *oid != base); if oids.first() == Some(&self.commit) { oids.remove(0); @@ -96,10 +99,10 @@ pub fn encode_change_id_path(id: &str) -> String { /// merge needed). Author and committer are copied from the head commit. pub fn create_synthetic_merge_commit( transaction: &josh_core::cache::Transaction, - pr_head: git2::Oid, - target_branch_tip: git2::Oid, + pr_head: gix_hash::ObjectId, + target_branch_tip: gix_hash::ObjectId, message: &str, -) -> anyhow::Result { +) -> anyhow::Result { let odb = transaction.odb(); let head = objects::CommitData::read(odb, pr_head)?; let tree = head.tree_id()?; @@ -115,17 +118,18 @@ pub fn create_synthetic_merge_commit( pub(crate) fn split_changes( transaction: &josh_core::cache::Transaction, - changes: std::collections::HashMap, + changes: std::collections::HashMap, ) -> anyhow::Result> { - if changes.values().next().map(|c| c.base) == Some(git2::Oid::ZERO_SHA1) { + if changes.values().next().map(|c| c.base) + == Some(gix_hash::ObjectId::null(gix_hash::Kind::Sha1)) + { return Ok(changes.into_values().collect()); } changes .into_values() .map(|c| { - let filter = - josh_core::filter::Filter::new().downstack(josh_core::objects::gix_oid(c.base)); + let filter = josh_core::filter::Filter::new().downstack(c.base); let new_oid = josh_core::filter::apply_to_commit(filter, c.commit, transaction)?; let mut result = c; result.commit = new_oid; @@ -136,14 +140,16 @@ pub(crate) fn split_changes( pub(crate) fn get_changes( transaction: &josh_core::cache::Transaction, - tip: git2::Oid, - base: git2::Oid, -) -> anyhow::Result> { + tip: gix_hash::ObjectId, + base: gix_hash::ObjectId, +) -> anyhow::Result> { let odb = transaction.odb(); let mut walk = objects::RevWalk::new(odb); walk.simplify_first_parent(); walk.push(tip)?; - let mut oids = walk.into_topo_vec(|oid| base != git2::Oid::ZERO_SHA1 && oid == base)?; + let mut oids = walk.into_topo_vec(|oid| { + base != gix_hash::ObjectId::null(gix_hash::Kind::Sha1) && oid == base + })?; oids.retain(|oid| *oid != base); oids.reverse(); @@ -163,8 +169,8 @@ pub(crate) fn get_changes( pub fn sync_changes( transaction: &josh_core::cache::Transaction, - tip: git2::Oid, - base: git2::Oid, + tip: gix_hash::ObjectId, + base: gix_hash::ObjectId, branch: &str, ) -> anyhow::Result> { let changes = get_changes(transaction, tip, base)?; @@ -202,9 +208,11 @@ pub fn list_changes( let mut changes = Vec::new(); for (change_id, data) in entries { - let tip_oid = git2::Oid::from_str(&data.commit).unwrap_or(git2::Oid::ZERO_SHA1); - let base_oid = git2::Oid::from_str(&data.base).unwrap_or(git2::Oid::ZERO_SHA1); - if tip_oid == git2::Oid::ZERO_SHA1 { + let tip_oid = gix_hash::ObjectId::from_str(&data.commit) + .unwrap_or(gix_hash::ObjectId::null(gix_hash::Kind::Sha1)); + let base_oid = gix_hash::ObjectId::from_str(&data.base) + .unwrap_or(gix_hash::ObjectId::null(gix_hash::Kind::Sha1)); + if tip_oid == gix_hash::ObjectId::null(gix_hash::Kind::Sha1) { continue; } let commit = match objects::CommitData::read(odb, tip_oid) { @@ -223,12 +231,12 @@ pub fn list_changes( pub fn resolve_change( transaction: &josh_core::cache::Transaction, - head: git2::Oid, + head: gix_hash::ObjectId, spec: &str, ) -> anyhow::Result { let odb = transaction.odb(); // Try as a full OID first. - if let Ok(oid) = git2::Oid::from_str(spec) { + if let Ok(oid) = gix_hash::ObjectId::from_str(spec) { if let Ok(commit) = objects::CommitData::read(odb, oid) { return Ok(Change::from_commit(&commit)); } diff --git a/josh-changes/src/comments.rs b/josh-changes/src/comments.rs index 907bea116..d7b2c0f4c 100644 --- a/josh-changes/src/comments.rs +++ b/josh-changes/src/comments.rs @@ -7,6 +7,7 @@ use josh_core::filter::tree; use josh_core::memodb::Odb; use josh_core::objects; use josh_git_serde::{from_tree_oid, from_value}; +use std::str::FromStr; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Location { @@ -113,7 +114,7 @@ fn write_comment_inner( let prefix = std::path::Path::new(path_prefix); let path = if let Some(ref file) = meta.file { let resolve_commit = match blob_commit_override { - Some(s) => git2::Oid::from_str(s)?, + Some(s) => gix_hash::ObjectId::from_str(s)?, None => change.commit(), }; let commit_tree = objects::CommitData::read(odb, resolve_commit)?.tree_id()?; @@ -243,10 +244,10 @@ pub fn comment_author( fn read_comment( odb: &Odb, id: &str, - entry_oid: git2::Oid, + entry_oid: gix_hash::ObjectId, file: Option, ) -> anyhow::Result { - let value = from_tree_oid(odb, objects::gix_oid(entry_oid))?; + let value = from_tree_oid(odb, entry_oid)?; let meta: CommentMeta = from_value(&value)?; Ok(Comment { id: id.to_string(), @@ -383,7 +384,7 @@ pub fn read_comments( fn collect_comments_at_prefix( transaction: &Transaction, odb: &Odb, - tree: git2::Oid, + tree: gix_hash::ObjectId, change_id: &str, prefix: &str, pending: bool, @@ -403,7 +404,7 @@ fn collect_comments_at_prefix( ) { for entry in tree::read_tree(transaction, odb, cid_tree)?.entries() { let id = String::from_utf8_lossy(entry.filename).into_owned(); - let mut c = read_comment(odb, &id, objects::git2_oid(&entry.oid), None) + let mut c = read_comment(odb, &id, entry.oid.to_owned(), None) .with_context(|| format!("undecodable comment {} on change {}", id, change_id))?; c.pending = pending; out.push(c); @@ -446,13 +447,13 @@ fn collect_comments_under_into( transaction: &Transaction, odb: &Odb, change_id: &str, - tree: git2::Oid, + tree: gix_hash::ObjectId, file_prefix: &std::path::Path, out: &mut Vec, ) -> anyhow::Result<()> { for entry in tree::read_tree(transaction, odb, tree)?.entries() { let name = std::str::from_utf8(entry.filename).unwrap_or(""); - let entry_oid = objects::git2_oid(&entry.oid); + let entry_oid = entry.oid.to_owned(); let file = if file_prefix.as_os_str().is_empty() { None } else { @@ -539,7 +540,13 @@ pub fn delete_outbox_comments( tree::get_path_entry(transaction, odb, tree, path), Ok(Some(_)) ) { - tree = tree::insert_oid(odb, tree, path, git2::Oid::ZERO_SHA1, 0)?; + tree = tree::insert_oid( + odb, + tree, + path, + gix_hash::ObjectId::null(gix_hash::Kind::Sha1), + 0, + )?; } } @@ -636,7 +643,7 @@ pub fn store_fetched_comments( fn collect_outbox_file_paths( transaction: &Transaction, odb: &Odb, - tree: git2::Oid, + tree: gix_hash::ObjectId, cur: &std::path::Path, want: &std::collections::HashSet<&str>, out: &mut Vec, @@ -651,7 +658,7 @@ fn collect_outbox_file_paths( collect_outbox_file_paths( transaction, odb, - objects::git2_oid(&entry.oid), + entry.oid.to_owned(), &cur.join(name), want, out, diff --git a/josh-changes/src/forges/gerrit.rs b/josh-changes/src/forges/gerrit.rs index f968d792a..1fc5a71c7 100644 --- a/josh-changes/src/forges/gerrit.rs +++ b/josh-changes/src/forges/gerrit.rs @@ -1,6 +1,5 @@ use crate::change::{Change, get_changes, split_changes}; use crate::stacked::PushRef; -use anyhow::anyhow; /// A valid Gerrit Change-Id is the letter `I` followed by 40 hex digits. fn is_gerrit_change_id(id: &str) -> bool { @@ -18,8 +17,7 @@ fn gerrit_change_id(josh_id: &str) -> anyhow::Result { if is_gerrit_change_id(josh_id) { return Ok(josh_id.to_string()); } - let oid = git2::Oid::hash_object(git2::ObjectType::Blob, josh_id.as_bytes()) - .map_err(|e| anyhow!("failed to derive Gerrit Change-Id: {}", e))?; + let oid = josh_core::objects::hash_blob(josh_id.as_bytes()); Ok(format!("I{}", oid)) } @@ -75,13 +73,13 @@ fn message_with_gerrit_change_id(message: &str, gerrit_id: &str) -> String { /// change rather than a duplicate. fn rewrite_chain_with_gerrit_ids( transaction: &josh_core::cache::Transaction, - tip: git2::Oid, - base: git2::Oid, -) -> anyhow::Result { + tip: gix_hash::ObjectId, + base: gix_hash::ObjectId, +) -> anyhow::Result { let odb = transaction.odb(); // Collect the chain from tip down to (but excluding) base, first parent only. - let mut chain: Vec = Vec::new(); + let mut chain: Vec = Vec::new(); let mut cur = tip; while cur != base { chain.push(cur); @@ -92,7 +90,7 @@ fn rewrite_chain_with_gerrit_ids( } chain.reverse(); - let mut new_parent = (base != git2::Oid::ZERO_SHA1).then_some(base); + let mut new_parent = (base != gix_hash::ObjectId::null(gix_hash::Kind::Sha1)).then_some(base); for oid in chain { let commit = josh_core::objects::CommitData::read(odb, oid)?; let (josh_id, _) = josh_core::trailers::commit_change_meta(&commit); @@ -135,8 +133,8 @@ fn rewrite_chain_with_gerrit_ids( pub fn build_gerrit_push( transaction: &josh_core::cache::Transaction, branch: &str, - tip: git2::Oid, - base: git2::Oid, + tip: gix_hash::ObjectId, + base: gix_hash::ObjectId, ) -> anyhow::Result> { if tip == base { return Ok(vec![]); @@ -165,8 +163,8 @@ pub fn build_gerrit_push( pub fn build_gerrit_independent_push( transaction: &josh_core::cache::Transaction, branch: &str, - tip: git2::Oid, - base: git2::Oid, + tip: gix_hash::ObjectId, + base: gix_hash::ObjectId, ) -> anyhow::Result> { let odb = transaction.odb(); let changes = get_changes(transaction, tip, base)?; @@ -188,7 +186,7 @@ pub fn build_gerrit_independent_push( let parent = josh_core::objects::CommitData::read(odb, change.commit)? .parent_ids() .next(); - let has_no_deps = if base == git2::Oid::ZERO_SHA1 { + let has_no_deps = if base == gix_hash::ObjectId::null(gix_hash::Kind::Sha1) { parent.is_none() } else { parent == Some(base) diff --git a/josh-changes/src/refs.rs b/josh-changes/src/refs.rs index 1107d09b4..1a3c7d1d2 100644 --- a/josh-changes/src/refs.rs +++ b/josh-changes/src/refs.rs @@ -256,10 +256,11 @@ impl ChangesRef { } /// Return the changes ref's target OID, if it exists. -pub fn read_ref_oid(repo: &git2::Repository, scope: &ChangesRef) -> Option { +pub fn read_ref_oid(repo: &git2::Repository, scope: &ChangesRef) -> Option { repo.find_reference(&scope.ref_name()) .ok() .and_then(|r| r.target()) + .map(josh_core::objects::gix_oid) } /// Read HEAD and return the current branch shorthand. Errors on a diff --git a/josh-changes/src/revisions.rs b/josh-changes/src/revisions.rs index cbdfdc8df..f72e7abcb 100644 --- a/josh-changes/src/revisions.rs +++ b/josh-changes/src/revisions.rs @@ -26,7 +26,7 @@ pub fn read_revisions( }; // The change's diffs subtree, when the commit has one. - let diffs_of = |tree: git2::Oid| -> Option { + let diffs_of = |tree: gix_hash::ObjectId| -> Option { crate::store::get_tree( transaction, odb, diff --git a/josh-changes/src/stacked.rs b/josh-changes/src/stacked.rs index a1c0cdd9f..407d8b169 100644 --- a/josh-changes/src/stacked.rs +++ b/josh-changes/src/stacked.rs @@ -16,7 +16,7 @@ pub enum PushMode { #[derive(Debug, Clone)] pub struct PushRef { pub ref_name: String, - pub oid: git2::Oid, + pub oid: gix_hash::ObjectId, pub change_id: String, } @@ -115,8 +115,8 @@ pub fn build_to_push( push_mode: &PushMode, baseref: &str, ref_with_options: &str, - oid_to_push: git2::Oid, - base_oid: git2::Oid, + oid_to_push: gix_hash::ObjectId, + base_oid: gix_hash::ObjectId, ) -> anyhow::Result> { match push_mode { PushMode::Publish(author) => { diff --git a/josh-changes/src/store.rs b/josh-changes/src/store.rs index 003855fa4..c6256435c 100644 --- a/josh-changes/src/store.rs +++ b/josh-changes/src/store.rs @@ -10,14 +10,14 @@ use josh_git_serde::GitValue; pub(crate) fn get_tree( transaction: &Transaction, odb: &Odb, - root: git2::Oid, + root: gix_hash::ObjectId, path: &std::path::Path, -) -> Option { +) -> Option { tree::get_path_entry(transaction, odb, root, path) .ok() .flatten() .filter(|entry| entry.mode.is_tree()) - .map(|entry| objects::git2_oid(&entry.oid)) + .map(|entry| entry.oid.to_owned()) } /// The tree of `scope`'s ref, or `None` when the ref does not exist. @@ -25,7 +25,7 @@ pub fn scope_tree( transaction: &Transaction, odb: &Odb, scope: &ChangesRef, -) -> anyhow::Result> { +) -> anyhow::Result> { match transaction.resolve_ref(&scope.ref_name())? { Some(oid) => Ok(Some(objects::CommitData::read(odb, oid)?.tree_id()?)), None => Ok(None), @@ -50,7 +50,7 @@ pub(crate) fn parse_timestamp(s: Option<&str>) -> git2::Time { pub fn write_changes_tree( transaction: &Transaction, path: &std::path::Path, - blob_oid: git2::Oid, + blob_oid: gix_hash::ObjectId, author: Option<&str>, timestamp: Option<&str>, scope: &ChangesRef, @@ -65,7 +65,7 @@ pub fn write_changes_tree( // Skip if the blob already exists at this path. if let Ok(Some(existing)) = tree::get_path_entry(transaction, odb, base_tree, path) { - if objects::git2_oid(&existing.oid) == blob_oid { + if existing.oid.to_owned() == blob_oid { return Ok(()); } } @@ -103,7 +103,7 @@ pub fn write_value( author: Option<&str>, timestamp: Option<&str>, scope: &ChangesRef, -) -> anyhow::Result { +) -> anyhow::Result { let (root, mode) = value_oid(transaction, data)?; place_oid(transaction, path, root, mode, author, timestamp, scope)?; Ok(root) @@ -116,7 +116,7 @@ pub fn write_value( pub fn value_oid( transaction: &Transaction, data: &T, -) -> anyhow::Result<(git2::Oid, i32)> { +) -> anyhow::Result<(gix_hash::ObjectId, i32)> { let odb = transaction.odb(); let value = josh_git_serde::to_value(data)?; let root = josh_git_serde::to_tree_oid(odb, &value)?; @@ -124,7 +124,7 @@ pub fn value_oid( GitValue::Tree(_) => 0o0040000, GitValue::Blob(_) => git2::FileMode::Blob.into(), }; - Ok((objects::git2_oid(&root), mode)) + Ok((root, mode)) } /// Place the already-written object `root` at `path` inside `scope`'s ref and @@ -132,7 +132,7 @@ pub fn value_oid( pub fn place_oid( transaction: &Transaction, path: &std::path::Path, - root: git2::Oid, + root: gix_hash::ObjectId, mode: i32, author: Option<&str>, timestamp: Option<&str>, @@ -147,7 +147,7 @@ pub fn place_oid( }; if let Ok(Some(existing)) = tree::get_path_entry(transaction, odb, base_tree, path) { - if objects::git2_oid(&existing.oid) == root { + if existing.oid.to_owned() == root { return Ok(()); } } @@ -197,7 +197,7 @@ pub fn read_filtered( filter, josh_core::filter::Rewrite::from_tree(root), )?; - let value = josh_git_serde::from_tree_oid(odb, objects::gix_oid(filtered.tree_id()))?; + let value = josh_git_serde::from_tree_oid(odb, filtered.tree_id())?; Ok(Some(josh_git_serde::from_value(&value)?)) } @@ -240,13 +240,7 @@ pub fn store_diff_data( } } - let tree = tree::insert_oid( - odb, - base_tree, - &path, - objects::git2_oid(&tree_oid), - 0o0040000, - )?; + let tree = tree::insert_oid(odb, base_tree, &path, tree_oid, 0o0040000)?; let sig = transaction.signature()?; @@ -260,7 +254,7 @@ pub fn store_diff_data( "josh\n", )?; - let mut parents: Vec = Vec::new(); + let mut parents: Vec = Vec::new(); parents.extend(prev_tip); parents.push(anchor_oid); let msg = format!("update {}\n", ref_name); @@ -321,7 +315,13 @@ pub fn delete_change( tree::get_path_entry(transaction, odb, tree, &path), Ok(Some(_)) ) { - tree = tree::insert_oid(odb, tree, &path, git2::Oid::ZERO_SHA1, 0)?; + tree = tree::insert_oid( + odb, + tree, + &path, + gix_hash::ObjectId::null(gix_hash::Kind::Sha1), + 0, + )?; } } diff --git a/josh-changes/src/votes.rs b/josh-changes/src/votes.rs index 1f3bb039b..0fb7fda37 100644 --- a/josh-changes/src/votes.rs +++ b/josh-changes/src/votes.rs @@ -190,7 +190,13 @@ pub fn delete_outbox_votes( tree::get_path_entry(transaction, odb, tree, &path), Ok(Some(_)) ) { - tree = tree::insert_oid(odb, tree, &path, git2::Oid::ZERO_SHA1, 0)?; + tree = tree::insert_oid( + odb, + tree, + &path, + gix_hash::ObjectId::null(gix_hash::Kind::Sha1), + 0, + )?; removed += 1; } } diff --git a/josh-cli/Cargo.toml b/josh-cli/Cargo.toml index 6499e4367..5c7864534 100644 --- a/josh-cli/Cargo.toml +++ b/josh-cli/Cargo.toml @@ -19,6 +19,7 @@ defer.workspace = true clap.workspace = true juniper.workspace = true git2.workspace = true +gix-hash.workspace = true tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } serde.workspace = true chrono = { workspace = true, features = ["clock"] } @@ -41,6 +42,9 @@ josh-graphql.workspace = true josh-link.workspace = true josh-templates.workspace = true +[dev-dependencies] +tempfile.workspace = true + [features] codesign = [] diff --git a/josh-cli/src/bin/josh-filter.rs b/josh-cli/src/bin/josh-filter.rs index 39bfffa80..f6cf37942 100644 --- a/josh-cli/src/bin/josh-filter.rs +++ b/josh-cli/src/bin/josh-filter.rs @@ -2,15 +2,16 @@ use anyhow::{Context, anyhow}; use std::fs::read_to_string; +use std::str::FromStr; fn resolve_input_ref( transaction: &josh_core::cache::Transaction, input_ref: &str, -) -> anyhow::Result<(String, git2::Oid)> { +) -> anyhow::Result<(String, gix_hash::ObjectId)> { let oid = josh_core::git::resolve_snapshot_input(transaction, input_ref)?; let ref_string = if input_ref == "+" || input_ref == "." { oid.to_string() - } else if git2::Oid::from_str(input_ref).is_ok() { + } else if gix_hash::ObjectId::from_str(input_ref).is_ok() { input_ref.to_string() } else if let Some(name) = transaction.expand_ref_name(input_ref)? { name @@ -144,7 +145,7 @@ struct GitNotesFilterHook { impl josh_core::cache::FilterHook for GitNotesFilterHook { fn filter_for_commit( &self, - commit_oid: git2::Oid, + commit_oid: gix_hash::ObjectId, arg: &str, ) -> anyhow::Result { let notes_ref = if arg.starts_with("refs/") { @@ -154,7 +155,10 @@ impl josh_core::cache::FilterHook for GitNotesFilterHook { }; let repo = self.repo.lock().unwrap(); let note = repo - .find_note(Some(notes_ref.as_str()), commit_oid) + .find_note( + Some(notes_ref.as_str()), + josh_core::objects::git2_oid(&commit_oid), + ) .context("missing git note for commit")?; let msg = note.message().context("empty git note")?; josh_core::filter::parse(msg) @@ -206,15 +210,13 @@ fn run_filter(args: Vec) -> anyhow::Result { }; transaction = transaction.with_filter_hook(std::sync::Arc::new(hook)); - let repo = transaction.git2_repo(); - // If the filter spec doesn't contain a colon and it's not from a file, // treat it as a SHA and read from tree let mut filterobj = if specstr.contains(':') || is_from_file { josh_core::filter::parse(&specstr)? } else { // Try to parse as SHA and read filter from tree - let tree_oid = git2::Oid::from_str(specstr.trim()) + let tree_oid = gix_hash::ObjectId::from_str(specstr.trim()) .with_context(|| format!("Invalid filter spec or SHA: {}", specstr))?; josh_core::filter::from_tree(&transaction, tree_oid)? }; @@ -242,11 +244,8 @@ fn run_filter(args: Vec) -> anyhow::Result { if !matcher.matches(name) { return Ok(()); } - let target = repo.find_object(oid, None)?.peel_to_commit()?.id(); - ids.push(( - josh_core::objects::gix_oid(target), - josh_core::filter::Filter::new().message(name), - )); + let target = josh_core::objects::peel_to_commit(transaction.odb(), oid)?; + ids.push((target, josh_core::filter::Filter::new().message(name))); refs.push((name.to_string(), target)); Ok(()) })?; @@ -261,12 +260,9 @@ fn run_filter(args: Vec) -> anyhow::Result { for line in reflist.lines() { let split = line.split(' ').collect::>(); if let [sha, name] = split.as_slice() { - let target = git2::Oid::from_str(sha)?; - let target = repo.find_object(target, None)?.peel_to_commit()?.id(); - ids.push(( - josh_core::objects::gix_oid(target), - josh_core::filter::Filter::new().message(name), - )); + let target = gix_hash::ObjectId::from_str(sha)?; + let target = josh_core::objects::peel_to_commit(transaction.odb(), target)?; + ids.push((target, josh_core::filter::Filter::new().message(name))); refs.push((name.to_string(), target)); } else if !split.is_empty() { eprintln!("Warning: malformed line: {:?}", line); @@ -332,7 +328,7 @@ fn run_filter(args: Vec) -> anyhow::Result { let old_oid = transaction .resolve_ref(target)? - .unwrap_or(git2::Oid::ZERO_SHA1); + .unwrap_or(gix_hash::ObjectId::null(gix_hash::Kind::Sha1)); let (mut updated_refs, errors) = josh_core::filter_refs(&transaction, filterobj, &refs); @@ -400,7 +396,7 @@ fn run_filter(args: Vec) -> anyhow::Result { // rev-parse reads them through the repository handle, which only sees disk. transaction.flush_mem_odb()?; - let rev = |spec: &str| -> anyhow::Result { + let rev = |spec: &str| -> anyhow::Result { transaction .rev_parse(spec)? .ok_or_else(|| anyhow!("no such revision: {}", spec)) diff --git a/josh-cli/src/bin/josh.rs b/josh-cli/src/bin/josh.rs index dd3c349eb..085c5225a 100644 --- a/josh-cli/src/bin/josh.rs +++ b/josh-cli/src/bin/josh.rs @@ -346,18 +346,14 @@ fn to_absolute_remote_url(url: &str) -> anyhow::Result { } } -/// Initial clone setup: create directory, init repo, add remote (no transaction needed) +/// Initialize a clone and configure its remote. fn clone_repo(args: &CloneArgs) -> anyhow::Result { - // Use the provided output directory let output_dir = args.out.clone(); - // Create the output directory first std::fs::create_dir_all(&output_dir)?; - // Initialize a new git repository inside the directory using git2 git2::Repository::init(&output_dir).context("Failed to initialize git repository")?; - // Use handle_remote_add to add the remote with the filter let remote_add_args = RemoteAddArgs { name: "origin".to_string(), url: to_absolute_remote_url(&args.url)?, diff --git a/josh-cli/src/commands/cache.rs b/josh-cli/src/commands/cache.rs index 867908a0f..ab1b4eb24 100644 --- a/josh-cli/src/commands/cache.rs +++ b/josh-cli/src/commands/cache.rs @@ -1,4 +1,5 @@ use std::collections::HashSet; +use std::str::FromStr; use anyhow::Context; @@ -96,7 +97,7 @@ fn handle_cache_build(args: &CacheBuildArgs, transaction: &Transaction) -> anyho let mut steps = Vec::new(); let mut ok = true; for id_str in ids.iter().rev() { - match git2::Oid::from_str(id_str) + match gix_hash::ObjectId::from_str(id_str) .map_err(anyhow::Error::from) .and_then(|oid| from_tree(transaction, oid)) { @@ -184,7 +185,7 @@ fn handle_cache_build(args: &CacheBuildArgs, transaction: &Transaction) -> anyho let mut next_commits = Vec::new(); for (branch_name, filtered_oid) in filtered { - if filtered_oid == git2::Oid::ZERO_SHA1 { + if filtered_oid == gix_hash::ObjectId::null(gix_hash::Kind::Sha1) { continue; } let filtered_ref = diff --git a/josh-cli/src/commands/changes.rs b/josh-cli/src/commands/changes.rs index a9368f59e..4d9d3d89b 100644 --- a/josh-cli/src/commands/changes.rs +++ b/josh-cli/src/commands/changes.rs @@ -177,10 +177,12 @@ pub fn handle_show( println!("Subject: {}", subject); println!(); - // PORT: the per-file line stats come out of libgit2's patch machinery; the change - // commit is behind a ref, so it is on disk. + // Line statistics still use libgit2's patch machinery. let repo = transaction.git2_repo(); - let files = file_stats(repo, &repo.find_commit(change.commit())?)?; + let files = file_stats( + repo, + &repo.find_commit(josh_core::objects::git2_oid(&change.commit()))?, + )?; let total_adds: usize = files.iter().map(|f| f.adds).sum(); let total_dels: usize = files.iter().map(|f| f.dels).sum(); println!( diff --git a/josh-cli/src/commands/link.rs b/josh-cli/src/commands/link.rs index 226f89928..106b3dd00 100644 --- a/josh-cli/src/commands/link.rs +++ b/josh-cli/src/commands/link.rs @@ -1,4 +1,5 @@ use anyhow::{Context, anyhow}; +use std::str::FromStr; use josh_link::make_signature; @@ -117,7 +118,7 @@ fn handle_link_add( .context("Failed to apply export filter")?; // If the export filter found no local content, fall back to fetching the remote. - let initial_oid = if export_oid != git2::Oid::ZERO_SHA1 { + let initial_oid = if export_oid != gix_hash::ObjectId::null(gix_hash::Kind::Sha1) { eprintln!( "Using local content at '{}' ({})", normalized_path, export_oid @@ -133,14 +134,14 @@ fn handle_link_add( .spawn_git(&["fetch", &args.url, target], &[]) .context("Failed to execute git fetch")?; - // PORT: FETCH_HEAD pseudo-ref read stays on the git2 handle until flag day - // (gix multi-entry FETCH_HEAD semantics still unresolved). - let fetched_oid = repo - .find_reference("FETCH_HEAD") - .context("Failed to find FETCH_HEAD after fetch")? - .peel_to_commit() - .context("Failed to peel FETCH_HEAD to commit")? - .id(); + // Preserve libgit2's multi-entry FETCH_HEAD resolution. + let fetched_oid = josh_core::objects::gix_oid( + repo.find_reference("FETCH_HEAD") + .context("Failed to find FETCH_HEAD after fetch")? + .peel_to_commit() + .context("Failed to peel FETCH_HEAD to commit")? + .id(), + ); eprintln!("Using fetched commit {}", fetched_oid); fetched_oid @@ -223,10 +224,10 @@ fn handle_link_fetch( let mut skipped = 0; for link_ref in &link_refs { - let oid = git2::Oid::from_str(&link_ref.commit) + let oid = gix_hash::ObjectId::from_str(&link_ref.commit) .with_context(|| format!("Invalid commit SHA in link file: {}", link_ref.commit))?; - if odb.exists(oid) { + if odb.exists(josh_core::objects::git2_oid(&oid)) { skipped += 1; continue; } @@ -280,7 +281,7 @@ fn handle_link_update( ); let filtered_oid = josh_core::filter_commit(transaction, roundtrip, head_commit) .context("Failed to apply filter")?; - if filtered_oid == git2::Oid::ZERO_SHA1 { + if filtered_oid == gix_hash::ObjectId::null(gix_hash::Kind::Sha1) { vec![] } else { let odb = transaction.odb(); @@ -320,14 +321,14 @@ fn handle_link_update( .spawn_git(&["fetch", &remote, &branch], &[]) .with_context(|| format!("git fetch failed for '{}'", path.display()))?; - // PORT: FETCH_HEAD pseudo-ref read stays on the git2 handle until flag day - // (gix multi-entry FETCH_HEAD semantics still unresolved). - let new_oid = repo - .find_reference("FETCH_HEAD") - .context("Failed to find FETCH_HEAD")? - .peel_to_commit() - .context("Failed to get FETCH_HEAD commit")? - .id(); + // Preserve libgit2's multi-entry FETCH_HEAD resolution. + let new_oid = josh_core::objects::gix_oid( + repo.find_reference("FETCH_HEAD") + .context("Failed to find FETCH_HEAD")? + .peel_to_commit() + .context("Failed to get FETCH_HEAD commit")? + .id(), + ); links_to_update.push((path.clone(), new_oid)); } @@ -397,7 +398,7 @@ fn handle_link_push( let exported_commit = josh_core::filter_commit(transaction, combined_filter, head_commit) .context("Failed to apply export filter")?; - if exported_commit == git2::Oid::ZERO_SHA1 { + if exported_commit == gix_hash::ObjectId::null(gix_hash::Kind::Sha1) { return Err(anyhow!("No content found at path '{}' to push", args.path)); } diff --git a/josh-cli/src/commands/pull.rs b/josh-cli/src/commands/pull.rs index 9a0c4f2ff..bd7374f63 100644 --- a/josh-cli/src/commands/pull.rs +++ b/josh-cli/src/commands/pull.rs @@ -39,7 +39,7 @@ pub enum IntegrateReport { }, } -fn short_oid(oid: git2::Oid) -> String { +fn short_oid(oid: gix_hash::ObjectId) -> String { oid.to_string()[..7].to_string() } @@ -47,7 +47,10 @@ fn short_oid(oid: git2::Oid) -> String { /// Used as an opportunistic content hash: a change that was merely restacked /// (rebased without content edits) keeps its patch-id even though the commit /// and tree oids all change. -fn patch_id(transaction: &josh_core::cache::Transaction, oid: git2::Oid) -> anyhow::Result { +fn patch_id( + transaction: &josh_core::cache::Transaction, + oid: gix_hash::ObjectId, +) -> anyhow::Result { let output = transaction .git_command( &[ @@ -95,8 +98,8 @@ fn patch_id(transaction: &josh_core::cache::Transaction, oid: git2::Oid) -> anyh /// changed). Without a transaction (tests), every update counts as changed. fn change_content_changed( transaction: Option<&josh_core::cache::Transaction>, - old: git2::Oid, - new: git2::Oid, + old: gix_hash::ObjectId, + new: gix_hash::ObjectId, ) -> bool { let Some(transaction) = transaction else { return true; @@ -228,20 +231,24 @@ fn resolve_upstream_ref( /// commits (e.g. by a merge queue) are detected as well. fn upstream_change_ids( transaction: &josh_core::cache::Transaction, - tip: git2::Oid, - base: git2::Oid, + tip: gix_hash::ObjectId, + base: gix_hash::ObjectId, ) -> anyhow::Result> { - let repo = transaction.git2_repo(); let odb = transaction.odb(); - let mut ids = std::collections::HashSet::new(); - let mut walk = repo.revwalk()?; - walk.push(tip)?; - if base != git2::Oid::ZERO_SHA1 { - walk.hide(base)?; - } + let commits = if base.is_null() { + let mut walk = josh_core::objects::RevWalk::new(odb); + walk.push(tip)?; + walk.into_topo_vec(|_| false)? + } else { + josh_core::objects::RangeWalk::new(odb, |oid| { + josh_core::cache::compute_sequence_number(transaction, oid) + }) + .into_topo_vec(tip, base)? + }; - for oid in walk { - let commit = josh_core::objects::CommitData::read(odb, oid?)?; + let mut ids = std::collections::HashSet::new(); + for oid in commits { + let commit = josh_core::objects::CommitData::read(odb, oid)?; if let (Some(id), _) = commit_change_meta(&commit) { ids.insert(id); } @@ -254,9 +261,9 @@ fn upstream_change_ids( /// Bails out on conflicts without writing any refs. fn restack_commits( transaction: &josh_core::cache::Transaction, - tip: git2::Oid, - commits: &[git2::Oid], -) -> anyhow::Result<(git2::Oid, usize)> { + tip: gix_hash::ObjectId, + commits: &[gix_hash::ObjectId], +) -> anyhow::Result<(gix_hash::ObjectId, usize)> { use josh_core::objects::CommitData; let odb = transaction.odb(); @@ -351,24 +358,21 @@ pub fn integrate( let (new_tip, report) = if merge_base == old { (new, IntegrateReport::FastForward { branch }) } else { - // Diverged: collect the local-only commits (linear stack expected). - let mut walk = repo.revwalk()?; - walk.set_sorting(git2::Sort::TOPOLOGICAL | git2::Sort::REVERSE)?; - walk.simplify_first_parent()?; + let odb = transaction.odb(); + let mut walk = josh_core::objects::RevWalk::new(odb); + walk.simplify_first_parent(); walk.push(old)?; - walk.hide(merge_base)?; + let mut local_commits = walk.into_topo_vec(|oid| oid == merge_base)?; + local_commits.reverse(); - let mut local_commits = Vec::new(); - for oid in walk { - let oid = oid?; - let commit = josh_core::objects::CommitData::read(transaction.odb(), oid)?; - if commit.parent_ids().count() > 1 { + for &oid in &local_commits { + let commit = josh_core::objects::CommitData::read(odb, oid)?; + if commit.parent_count() > 1 { anyhow::bail!( "local branch '{}' contains merge commits; cannot integrate automatically", branch ); } - local_commits.push(oid); } let applied = upstream_change_ids(transaction, new, merge_base)?; @@ -379,11 +383,7 @@ pub fn integrate( let commit = josh_core::objects::CommitData::read(transaction.odb(), oid)?; match commit_change_meta(&commit) { (Some(id), _) if applied.contains(&id) => skipped.push(id), - // A commit with a Change-Id not yet applied upstream, or one - // without a Change-Id at all, is a local-only commit we keep and - // restack. A missing Change-Id just means we cannot match it - // against the upstream stack; restacking cherry-picks it on top - // and drops it if its content already landed (becomes empty). + // Keep unmatched commits; restacking drops content that already landed. _ => remaining.push(oid), } } @@ -409,7 +409,7 @@ pub fn integrate( // Update the worktree first (safe: refuses to clobber conflicting local // modifications), only then move the branch ref. - let new_tip_commit = repo.find_commit(new_tip)?; + let new_tip_commit = repo.find_commit(josh_core::objects::git2_oid(&new_tip))?; repo.checkout_tree( new_tip_commit.as_object(), Some(git2::build::CheckoutBuilder::new().safe()), @@ -529,12 +529,13 @@ pub fn handle_pull( #[cfg(test)] mod tests { use super::*; + use std::str::FromStr; const OID_A: &str = "af180e6da554e60815593af48d419ac0e719c47a"; const OID_B: &str = "2bf1cefd82c96e5d7478ff834c59194d40e539c4"; - fn oid(s: &str) -> git2::Oid { - git2::Oid::from_str(s).unwrap() + fn oid(s: &str) -> gix_hash::ObjectId { + gix_hash::ObjectId::from_str(s).unwrap() } fn fast_forward(old: &str, new: &str, reference: &str) -> RefUpdate { @@ -552,6 +553,46 @@ mod tests { } } + #[test] + fn upstream_change_ids_walks_every_parent_between_base_and_tip() { + let dir = tempfile::tempdir().unwrap(); + git2::Repository::init_bare(dir.path()).unwrap(); + let context = josh_core::cache::TransactionContext::new( + dir.path(), + std::sync::Arc::new(josh_core::cache::CacheStack::new()), + ); + let transaction = context.open().unwrap(); + let signature = + git2::Signature::new("Test", "test@example.com", &git2::Time::new(0, 0)).unwrap(); + let commit = |parents: &[gix_hash::ObjectId], change: Option<&str>| { + let message = change + .map(|id| format!("Subject\n\nChange: {id}\n")) + .unwrap_or_else(|| "Root".to_string()); + josh_core::objects::write_commit( + transaction.odb(), + gix_hash::ObjectId::empty_tree(gix_hash::Kind::Sha1), + parents, + &signature, + &signature, + &message, + ) + .unwrap() + }; + + let root = commit(&[], None); + let left = commit(&[root], Some("left")); + let right = commit(&[root], Some("right")); + let merge = commit(&[left, right], Some("merge")); + + assert_eq!( + upstream_change_ids(&transaction, merge, root).unwrap(), + ["left", "right", "merge"] + .map(String::from) + .into_iter() + .collect() + ); + } + #[test] fn summary_renders_branch_updates() { let updates = vec![ diff --git a/josh-cli/src/commands/push.rs b/josh-cli/src/commands/push.rs index 58e5031b3..dfd35a2e9 100644 --- a/josh-cli/src/commands/push.rs +++ b/josh-cli/src/commands/push.rs @@ -142,12 +142,15 @@ fn prepare_push( let old_filtered = if let Some((_, filtered_oid)) = filtered_oids.first() { *filtered_oid } else { - git2::Oid::ZERO_SHA1 + gix_hash::ObjectId::null(gix_hash::Kind::Sha1) }; (dest_oid, old_filtered) } else { - (git2::Oid::ZERO_SHA1, git2::Oid::ZERO_SHA1) + ( + gix_hash::ObjectId::null(gix_hash::Kind::Sha1), + gix_hash::ObjectId::null(gix_hash::Kind::Sha1), + ) }; let original_target = if let Some(base) = base { @@ -180,7 +183,7 @@ fn prepare_push( .context("Failed to unapply filter")?; let unfiltered_oid = if merge { - if original_target == git2::Oid::ZERO_SHA1 { + if original_target == gix_hash::ObjectId::null(gix_hash::Kind::Sha1) { return Err(anyhow!( "--merge requires --base= or an existing destination ref" )); diff --git a/josh-cli/src/commands/sync.rs b/josh-cli/src/commands/sync.rs index eed3f35e3..90b3f36d9 100644 --- a/josh-cli/src/commands/sync.rs +++ b/josh-cli/src/commands/sync.rs @@ -38,10 +38,10 @@ pub fn handle_sync( let base_oid = if let Some(b) = &branch { match transaction.resolve_ref(&format!("refs/remotes/origin/{}", b))? { Some(oid) => josh_core::objects::peel_to_commit(transaction.odb(), oid)?, - None => git2::Oid::ZERO_SHA1, + None => gix_hash::ObjectId::null(gix_hash::Kind::Sha1), } } else { - git2::Oid::ZERO_SHA1 + gix_hash::ObjectId::null(gix_hash::Kind::Sha1) }; let resolved = args.scope.resolve(transaction)?; @@ -93,8 +93,8 @@ fn sync_local( args: &SyncArgs, transaction: &josh_core::cache::Transaction, local_branch: &str, - head_oid: git2::Oid, - base_oid: git2::Oid, + head_oid: gix_hash::ObjectId, + base_oid: gix_hash::ObjectId, ) -> anyhow::Result<()> { if args.push { return Err(anyhow!( diff --git a/josh-cli/src/forge/github/changes.rs b/josh-cli/src/forge/github/changes.rs index 63f2c85ca..3df88f070 100644 --- a/josh-cli/src/forge/github/changes.rs +++ b/josh-cli/src/forge/github/changes.rs @@ -3,6 +3,7 @@ //! feedback back to GitHub. use anyhow::{Context, anyhow}; +use std::str::FromStr; use josh_core::git::normalize_repo_path; use josh_github_graphql::connection::GithubApiConnection; @@ -268,7 +269,7 @@ struct GithubSyncCtx<'a> { owner: &'a str, repo_name: &'a str, remote_name: &'a str, - target_branch_shas: &'a std::collections::HashMap, + target_branch_shas: &'a std::collections::HashMap, } impl GithubSyncCtx<'_> { @@ -279,14 +280,14 @@ impl GithubSyncCtx<'_> { let (existing_change_id, _) = josh_core::trailers::parse_change_meta(&pr.head_commit_message); - let head_oid = - git2::Oid::from_str(&pr.head_oid).map_err(|e| anyhow!("bad head OID: {}", e))?; + let head_oid = gix_hash::ObjectId::from_str(&pr.head_oid) + .map_err(|e| anyhow!("bad head OID: {}", e))?; let odb = self.transaction.odb(); josh_core::objects::CommitData::read(odb, head_oid) .map_err(|_| anyhow!("head commit {} not available from GitHub", pr.head_oid))?; - let base_oid = - git2::Oid::from_str(&pr.base_ref_oid).map_err(|e| anyhow!("bad base OID: {}", e))?; + let base_oid = gix_hash::ObjectId::from_str(&pr.base_ref_oid) + .map_err(|e| anyhow!("bad base OID: {}", e))?; josh_core::objects::CommitData::read(odb, base_oid) .map_err(|_| anyhow!("base commit {} not available from GitHub", pr.base_ref_oid))?; @@ -405,11 +406,11 @@ struct PrMeta { /// `{owner}/{repo}/pull/{N}` id. change_id: String, remote_scope: josh_changes::ChangesRef, - head: git2::Oid, + head: gix_hash::ObjectId, /// Immediate base commit of the PR. - target: git2::Oid, + target: gix_hash::ObjectId, /// Resolved change base for change-id'd heads; `None` for synthetic merges. - change_base: Option, + change_base: Option, pr_data: josh_github_graphql::operations::pull_request::PrData, } @@ -828,7 +829,7 @@ fn fetch_sync_objects( owner: &str, repo_name: &str, prs: &[PrSummary], -) -> anyhow::Result> { +) -> anyhow::Result> { const SCRATCH: &str = "refs/josh/sync-tips"; let github_url = format!("https://github.com/{}/{}", owner, repo_name); diff --git a/josh-cli/src/porcelain.rs b/josh-cli/src/porcelain.rs index 14154d3ab..e3e21f12d 100644 --- a/josh-cli/src/porcelain.rs +++ b/josh-cli/src/porcelain.rs @@ -2,26 +2,33 @@ //! //! Each updated ref is reported as one ` ` //! line; rejected updates may carry a `(reason)` suffix after the ref name. +use std::str::FromStr; /// One ref update reported by `git fetch --porcelain`. #[derive(Debug, Clone, PartialEq, Eq)] pub enum RefUpdate { /// Fast-forward update (' '). FastForward { - old: git2::Oid, - new: git2::Oid, + old: gix_hash::ObjectId, + new: gix_hash::ObjectId, reference: String, }, /// Forced (non-fast-forward) update ('+'). Forced { - old: git2::Oid, - new: git2::Oid, + old: gix_hash::ObjectId, + new: gix_hash::ObjectId, reference: String, }, /// Newly created ref ('*'). - New { new: git2::Oid, reference: String }, + New { + new: gix_hash::ObjectId, + reference: String, + }, /// Deleted ref ('-'). - Deleted { old: git2::Oid, reference: String }, + Deleted { + old: gix_hash::ObjectId, + reference: String, + }, /// Update rejected by the remote ('!'), with an optional reason. Rejected { reference: String, @@ -192,8 +199,8 @@ pub fn parse_fetch_porcelain(output: &str) -> anyhow::Result> { let new = parts.next().ok_or_else(|| parse_error(line))?; let reference = parts.next().ok_or_else(|| parse_error(line))?; - let old = git2::Oid::from_str(old).map_err(|_| parse_error(line))?; - let new = git2::Oid::from_str(new).map_err(|_| parse_error(line))?; + let old = gix_hash::ObjectId::from_str(old).map_err(|_| parse_error(line))?; + let new = gix_hash::ObjectId::from_str(new).map_err(|_| parse_error(line))?; let reference = reference.to_string(); let update = match flag { @@ -243,8 +250,8 @@ mod tests { const OID_A: &str = "af180e6da554e60815593af48d419ac0e719c47a"; const OID_B: &str = "2bf1cefd82c96e5d7478ff834c59194d40e539c4"; - fn oid(s: &str) -> git2::Oid { - git2::Oid::from_str(s).unwrap() + fn oid(s: &str) -> gix_hash::ObjectId { + gix_hash::ObjectId::from_str(s).unwrap() } #[test] diff --git a/josh-cli/src/remote_ops.rs b/josh-cli/src/remote_ops.rs index 24901f663..efa4a5cd3 100644 --- a/josh-cli/src/remote_ops.rs +++ b/josh-cli/src/remote_ops.rs @@ -73,7 +73,7 @@ pub fn resolve_default_branch( pub fn get_backing_refs( transaction: &josh_core::cache::Transaction, remote_name: &str, -) -> anyhow::Result> { +) -> anyhow::Result> { let mut input_refs = Vec::new(); transaction.for_each_ref_prefixed( &format!("refs/josh/remotes/{}/", remote_name), @@ -125,16 +125,17 @@ pub fn apply_josh_filtering( let steps = flatten_chain(filter); // Seed with the raw backing refs - let mut current_commits: Vec<(String, git2::Oid)> = get_backing_refs(transaction, remote_name)? - .into_iter() - .map(|(refname, oid)| { - let branch = refname - .strip_prefix(&prefix) - .unwrap_or(&refname) - .to_string(); - (branch, oid) - }) - .collect(); + let mut current_commits: Vec<(String, gix_hash::ObjectId)> = + get_backing_refs(transaction, remote_name)? + .into_iter() + .map(|(refname, oid)| { + let branch = refname + .strip_prefix(&prefix) + .unwrap_or(&refname) + .to_string(); + (branch, oid) + }) + .collect(); // Apply each step, writing filtered refs along the way for (step_idx, step_filter) in steps.iter().enumerate() { @@ -152,7 +153,7 @@ pub fn apply_josh_filtering( let mut next_commits = Vec::new(); for (branch_name, filtered_oid) in &filtered { - if *filtered_oid == git2::Oid::ZERO_SHA1 { + if *filtered_oid == gix_hash::ObjectId::null(gix_hash::Kind::Sha1) { continue; } diff --git a/josh-compose/Cargo.toml b/josh-compose/Cargo.toml index 2e933b004..20dfcef73 100644 --- a/josh-compose/Cargo.toml +++ b/josh-compose/Cargo.toml @@ -12,7 +12,7 @@ version = "26.7.28" [dependencies] anyhow.workspace = true defer.workspace = true -git2.workspace = true +gix-hash.workspace = true josh-core.workspace = true josh-filter.workspace = true josh-compose-backend.workspace = true diff --git a/josh-compose/src/archive.rs b/josh-compose/src/archive.rs index eb89b7fa6..f93124d79 100644 --- a/josh-compose/src/archive.rs +++ b/josh-compose/src/archive.rs @@ -7,7 +7,7 @@ use josh_core::memodb; pub fn tree_to_tar( transaction: &cache::Transaction, odb: &memodb::Odb, - tree_oid: git2::Oid, + tree_oid: gix_hash::ObjectId, ) -> anyhow::Result> { let mut buf = Vec::new(); { @@ -21,7 +21,7 @@ pub fn tree_to_tar( fn append_tree( transaction: &cache::Transaction, odb: &memodb::Odb, - tree_oid: git2::Oid, + tree_oid: gix_hash::ObjectId, prefix: &str, builder: &mut tar::Builder, ) -> anyhow::Result<()> { @@ -33,7 +33,7 @@ fn append_tree( } else { format!("{prefix}/{name}") }; - let id = josh_core::objects::git2_oid(entry.oid); + let id = entry.oid.to_owned(); if entry.mode.is_link() { let content = tree::blob_bytes(odb, id) diff --git a/josh-compose/src/container.rs b/josh-compose/src/container.rs index dde0a6aee..b5ca7577e 100644 --- a/josh-compose/src/container.rs +++ b/josh-compose/src/container.rs @@ -1,5 +1,6 @@ use std::collections::HashSet; use std::path::Path; +use std::str::FromStr; use josh_core::cache; use josh_core::memodb; @@ -78,8 +79,8 @@ fn start_sidecar( pub fn run_container( transaction: &cache::Transaction, odb: &memodb::Odb, - ws_tree: git2::Oid, - attempted: &mut HashSet, + ws_tree: gix_hash::ObjectId, + attempted: &mut HashSet, extract_to_workdir: bool, runtime: &dyn Runtime, ) -> anyhow::Result<()> { @@ -117,7 +118,7 @@ pub fn run_container( let mut dep_volumes: Vec<(String, String, bool)> = vec![]; let mut dep_errors: Vec = vec![]; for (dep_name, dep_sha) in &input_entries { - let dep_tree = match git2::Oid::from_str(dep_sha.trim()) { + let dep_tree = match gix_hash::ObjectId::from_str(dep_sha.trim()) { Ok(oid) => oid, Err(_) => { dep_errors.push(format!("dependency {dep_name}: invalid SHA {dep_sha:?}")); diff --git a/josh-compose/src/filter.rs b/josh-compose/src/filter.rs index ee1c038e9..59206d1ac 100644 --- a/josh-compose/src/filter.rs +++ b/josh-compose/src/filter.rs @@ -5,7 +5,7 @@ use anyhow::Context; pub fn resolve_input( transaction: &josh_core::cache::Transaction, input_ref: &str, -) -> anyhow::Result { +) -> anyhow::Result { josh_core::git::resolve_snapshot_input(transaction, input_ref) .with_context(|| format!("failed to resolve input ref: {input_ref:?}")) } @@ -18,8 +18,8 @@ pub fn resolve_input( pub fn compute_ws_tree( transaction: &josh_core::cache::Transaction, filter_spec: &str, - source_commit: git2::Oid, -) -> anyhow::Result<(git2::Oid, String)> { + source_commit: gix_hash::ObjectId, +) -> anyhow::Result<(gix_hash::ObjectId, String)> { let full_filter = format!(":SQUASH{filter_spec}"); let filterobj = josh_core::filter::parse(&full_filter) diff --git a/josh-compose/src/image.rs b/josh-compose/src/image.rs index e830fc40f..4443f2724 100644 --- a/josh-compose/src/image.rs +++ b/josh-compose/src/image.rs @@ -1,10 +1,10 @@ use anyhow::Context; +use std::str::FromStr; use josh_compose_backend::{EnvRecipe, EnvironmentBackend}; use josh_core::cache; use josh_core::filter::tree; use josh_core::memodb; -use josh_core::objects; use crate::meta; use crate::naming; @@ -14,7 +14,7 @@ use crate::naming; pub fn ensure_image( transaction: &cache::Transaction, odb: &memodb::Odb, - build_tree: git2::Oid, + build_tree: gix_hash::ObjectId, runtime: &dyn EnvironmentBackend, ) -> anyhow::Result { let image_name = naming::env(build_tree); @@ -32,7 +32,7 @@ pub fn ensure_image( // Containerfile can reference it (e.g. ARG my_base; FROM $my_base). let base_entries = meta::read_blob_entries(transaction, odb, build_tree, "bases"); for (base_name, base_sha) in &base_entries { - let base_oid = git2::Oid::from_str(base_sha.trim()) + let base_oid = gix_hash::ObjectId::from_str(base_sha.trim()) .with_context(|| format!("invalid base SHA for {base_name}: {base_sha}"))?; let base_env = ensure_image(transaction, odb, base_oid, runtime)?; build_args.push((base_name.clone(), base_env)); @@ -44,7 +44,7 @@ pub fn ensure_image( let context_entry = tree::read_tree(transaction, odb, build_tree)? .entry(b"context") - .map(|e| objects::git2_oid(e.oid)) + .map(|e| e.oid.to_owned()) .context("workspace image tree missing 'context' subtree")?; let context = crate::archive::tree_to_tar(transaction, odb, context_entry)?; diff --git a/josh-compose/src/lib.rs b/josh-compose/src/lib.rs index ed022ad35..ea4e2d3ae 100644 --- a/josh-compose/src/lib.rs +++ b/josh-compose/src/lib.rs @@ -86,7 +86,7 @@ pub fn plan_images( opts: RunOptions, ignore_cache: bool, runtime: &dyn ArtifactBackend, -) -> anyhow::Result> { +) -> anyhow::Result> { josh_filter::check_experimental_features_enabled("josh compose images")?; let filter_spec = opts.filter_spec.trim().to_string(); @@ -110,7 +110,7 @@ pub fn plan_jobs( opts: RunOptions, ignore_cache: bool, runtime: &dyn ArtifactBackend, -) -> anyhow::Result> { +) -> anyhow::Result> { josh_filter::check_experimental_features_enabled("josh compose jobs")?; let filter_spec = opts.filter_spec.trim().to_string(); diff --git a/josh-compose/src/meta.rs b/josh-compose/src/meta.rs index 842772e5a..530e41c87 100644 --- a/josh-compose/src/meta.rs +++ b/josh-compose/src/meta.rs @@ -6,13 +6,13 @@ //! scheduler. use std::path::Path; +use std::str::FromStr; use crate::OutputMode; use josh_compose_backend::NetworkPolicy; use josh_core::cache; use josh_core::filter::tree; use josh_core::memodb; -use josh_core::objects; /// Specification for a sidecar service that runs alongside a workspace step. /// @@ -22,7 +22,7 @@ pub struct SidecarSpec { /// Logical name used for addressing and labeling. pub name: String, /// Build-tree OID of the image to run. - pub image: git2::Oid, + pub image: gix_hash::ObjectId, /// Static environment variables set by the workspace config. pub env: Vec<(String, String)>, /// Environment variable names to forward from the host process (e.g. API keys, CI @@ -45,10 +45,10 @@ pub struct WorkspaceMeta { pub cache: Option, pub network: NetworkPolicy, /// Tree OID of the image workspace. `None` for orchestrator-only workspaces. - pub image: Option, + pub image: Option, /// Tree OID of the workspace files mounted into the environment at `/worktree`. /// `None` for orchestrator-only workspaces. - pub worktree: Option, + pub worktree: Option, pub sidecars: Vec, } @@ -56,11 +56,11 @@ pub struct WorkspaceMeta { pub fn read_blob( transaction: &cache::Transaction, odb: &memodb::Odb, - tree_oid: git2::Oid, + tree_oid: gix_hash::ObjectId, path: &str, ) -> Option { let entry = tree::get_path_entry(transaction, odb, tree_oid, Path::new(path)).ok()??; - let content = tree::blob_bytes(odb, objects::git2_oid(&entry.oid))?; + let content = tree::blob_bytes(odb, entry.oid.to_owned())?; Some(std::str::from_utf8(&content).ok()?.trim().to_string()) } @@ -68,14 +68,14 @@ pub fn read_blob( pub fn read_tree_entries( transaction: &cache::Transaction, odb: &memodb::Odb, - tree_oid: git2::Oid, + tree_oid: gix_hash::ObjectId, prefix: &str, -) -> Vec<(String, git2::Oid)> { +) -> Vec<(String, gix_hash::ObjectId)> { let Ok(Some(entry)) = tree::get_path_entry(transaction, odb, tree_oid, Path::new(prefix)) else { return vec![]; }; - let Ok(subtree) = tree::read_tree(transaction, odb, objects::git2_oid(&entry.oid)) else { + let Ok(subtree) = tree::read_tree(transaction, odb, entry.oid.to_owned()) else { return vec![]; }; subtree @@ -83,7 +83,7 @@ pub fn read_tree_entries( .map(|e| { ( String::from_utf8_lossy(e.filename).into_owned(), - objects::git2_oid(e.oid), + e.oid.to_owned(), ) }) .collect() @@ -93,7 +93,7 @@ pub fn read_tree_entries( pub fn read_blob_entries( transaction: &cache::Transaction, odb: &memodb::Odb, - tree_oid: git2::Oid, + tree_oid: gix_hash::ObjectId, prefix: &str, ) -> Vec<(String, String)> { read_tree_entries(transaction, odb, tree_oid, prefix) @@ -113,7 +113,7 @@ pub fn read_blob_entries( pub fn read_meta( transaction: &cache::Transaction, odb: &memodb::Odb, - ws_tree: git2::Oid, + ws_tree: gix_hash::ObjectId, ) -> anyhow::Result { let label = read_blob(transaction, odb, ws_tree, "label") .filter(|s| !s.is_empty()) @@ -139,13 +139,13 @@ pub fn read_meta( let image = read_blob(transaction, odb, ws_tree, "image") .filter(|s| !s.is_empty()) .map(|sha| { - git2::Oid::from_str(&sha) + gix_hash::ObjectId::from_str(&sha) .map_err(|_| anyhow::anyhow!("invalid image SHA in workspace tree: {sha}")) }) .transpose()?; let tree = tree::read_tree(transaction, odb, ws_tree)?; - let worktree = tree.entry(b"worktree").map(|e| objects::git2_oid(e.oid)); + let worktree = tree.entry(b"worktree").map(|e| e.oid.to_owned()); let sidecars = read_sidecars(transaction, odb, ws_tree)?; @@ -164,16 +164,16 @@ pub fn read_meta( pub fn read_sidecars( transaction: &cache::Transaction, odb: &memodb::Odb, - ws_tree: git2::Oid, + ws_tree: gix_hash::ObjectId, ) -> anyhow::Result> { let mut out = vec![]; for (name, content) in read_blob_entries(transaction, odb, ws_tree, "sidecars") { - let sidecar_tree = git2::Oid::from_str(content.trim()) + let sidecar_tree = gix_hash::ObjectId::from_str(content.trim()) .map_err(|_| anyhow::anyhow!("sidecar {name}: invalid tree SHA {content:?}"))?; let image_sha = read_blob(transaction, odb, sidecar_tree, "image") .filter(|s| !s.is_empty()) .ok_or_else(|| anyhow::anyhow!("sidecar {name}: missing image"))?; - let image = git2::Oid::from_str(&image_sha) + let image = gix_hash::ObjectId::from_str(&image_sha) .map_err(|_| anyhow::anyhow!("sidecar {name}: invalid image SHA {image_sha:?}"))?; let port_str = read_blob(transaction, odb, sidecar_tree, "port") .filter(|s| !s.is_empty()) diff --git a/josh-compose/src/naming.rs b/josh-compose/src/naming.rs index 8d4a3ab70..af590dd3d 100644 --- a/josh-compose/src/naming.rs +++ b/josh-compose/src/naming.rs @@ -10,7 +10,7 @@ //! unambiguous and don't collide with anything else on the system. /// Output artifact for the workspace tree `ws_tree` (mounted at `/out`). -pub fn output(ws_tree: git2::Oid) -> String { +pub fn output(ws_tree: gix_hash::ObjectId) -> String { format!("{OUTPUT_PREFIX}{ws_tree}") } @@ -20,7 +20,7 @@ pub fn cache(cache_name: &str) -> String { } /// Environment key for the image built from `build_tree`. -pub fn env(build_tree: git2::Oid) -> String { +pub fn env(build_tree: gix_hash::ObjectId) -> String { format!("{ENV_PREFIX}{build_tree}") } diff --git a/josh-compose/src/plan.rs b/josh-compose/src/plan.rs index 625c4a375..817cc2a1b 100644 --- a/josh-compose/src/plan.rs +++ b/josh-compose/src/plan.rs @@ -1,4 +1,5 @@ use std::collections::HashSet; +use std::str::FromStr; use josh_core::cache; use josh_core::memodb; @@ -23,13 +24,13 @@ use crate::naming; pub fn collect_image_oids( transaction: &cache::Transaction, odb: &memodb::Odb, - ws_tree: git2::Oid, + ws_tree: gix_hash::ObjectId, ignore_cache: bool, runtime: &dyn ArtifactBackend, -) -> anyhow::Result> { - let mut out: Vec = vec![]; - let mut image_seen: HashSet = HashSet::new(); - let mut ws_seen: HashSet = HashSet::new(); +) -> anyhow::Result> { + let mut out: Vec = vec![]; + let mut image_seen: HashSet = HashSet::new(); + let mut ws_seen: HashSet = HashSet::new(); walk_workspace( transaction, odb, @@ -54,12 +55,12 @@ pub fn collect_image_oids( pub fn collect_job_hashes( transaction: &cache::Transaction, odb: &memodb::Odb, - ws_tree: git2::Oid, + ws_tree: gix_hash::ObjectId, ignore_cache: bool, runtime: &dyn ArtifactBackend, -) -> anyhow::Result> { - let mut out: Vec = vec![]; - let mut ws_seen: HashSet = HashSet::new(); +) -> anyhow::Result> { + let mut out: Vec = vec![]; + let mut ws_seen: HashSet = HashSet::new(); walk_workspace_jobs( transaction, odb, @@ -75,12 +76,12 @@ pub fn collect_job_hashes( fn walk_workspace( transaction: &cache::Transaction, odb: &memodb::Odb, - ws_tree: git2::Oid, + ws_tree: gix_hash::ObjectId, ignore_cache: bool, runtime: &dyn ArtifactBackend, - out: &mut Vec, - image_seen: &mut HashSet, - ws_seen: &mut HashSet, + out: &mut Vec, + image_seen: &mut HashSet, + ws_seen: &mut HashSet, ) -> anyhow::Result<()> { if !ws_seen.insert(ws_tree) { return Ok(()); @@ -97,7 +98,7 @@ fn walk_workspace( } for (dep_name, dep_sha) in meta::read_blob_entries(transaction, odb, ws_tree, "inputs") { - let dep_tree = git2::Oid::from_str(dep_sha.trim()) + let dep_tree = gix_hash::ObjectId::from_str(dep_sha.trim()) .map_err(|_| anyhow::anyhow!("dependency {dep_name}: invalid tree SHA {dep_sha:?}"))?; walk_workspace( transaction, @@ -124,11 +125,11 @@ fn walk_workspace( fn walk_workspace_jobs( transaction: &cache::Transaction, odb: &memodb::Odb, - ws_tree: git2::Oid, + ws_tree: gix_hash::ObjectId, ignore_cache: bool, runtime: &dyn ArtifactBackend, - out: &mut Vec, - ws_seen: &mut HashSet, + out: &mut Vec, + ws_seen: &mut HashSet, ) -> anyhow::Result<()> { if !ws_seen.insert(ws_tree) { return Ok(()); @@ -141,7 +142,7 @@ fn walk_workspace_jobs( } for (dep_name, dep_sha) in meta::read_blob_entries(transaction, odb, ws_tree, "inputs") { - let dep_tree = git2::Oid::from_str(dep_sha.trim()) + let dep_tree = gix_hash::ObjectId::from_str(dep_sha.trim()) .map_err(|_| anyhow::anyhow!("dependency {dep_name}: invalid tree SHA {dep_sha:?}"))?; walk_workspace_jobs( transaction, @@ -162,7 +163,7 @@ fn walk_workspace_jobs( /// require the output volume when the workspace produces one. A skippable workspace /// won't be executed by a run, so its image and sidecar images are not needed. fn workspace_is_skippable( - ws_tree: git2::Oid, + ws_tree: gix_hash::ObjectId, meta: &WorkspaceMeta, runtime: &dyn ArtifactBackend, ) -> anyhow::Result { @@ -185,16 +186,16 @@ fn workspace_is_skippable( fn collect_image_with_bases( transaction: &cache::Transaction, odb: &memodb::Odb, - image_oid: git2::Oid, - out: &mut Vec, - image_seen: &mut HashSet, + image_oid: gix_hash::ObjectId, + out: &mut Vec, + image_seen: &mut HashSet, ) -> anyhow::Result<()> { if image_seen.contains(&image_oid) { return Ok(()); } for (base_name, base_sha) in meta::read_blob_entries(transaction, odb, image_oid, "bases") { - let base_oid = git2::Oid::from_str(base_sha.trim()) + let base_oid = gix_hash::ObjectId::from_str(base_sha.trim()) .map_err(|_| anyhow::anyhow!("invalid base SHA for {base_name}: {base_sha:?}"))?; collect_image_with_bases(transaction, odb, base_oid, out, image_seen)?; } diff --git a/josh-core/benches/deephistory_glob.rs b/josh-core/benches/deephistory_glob.rs index 8613b1e32..65528967c 100644 --- a/josh-core/benches/deephistory_glob.rs +++ b/josh-core/benches/deephistory_glob.rs @@ -6,6 +6,7 @@ use josh_test_support::bench::{ }; use rand::prelude::*; use std::path::{Path, PathBuf}; +use std::str::FromStr; // The scaling parameter of this benchmark is history *length*, not tree width. `filter_commit` // walks and rewrites a commit's whole ancestry, so applying a filter to the head does O(history) @@ -90,7 +91,7 @@ const JOSH_BENCH_COMMIT_TIME: &str = "1700000000"; /// One history length and the head of its generated history. struct SizeCase { n_commits: usize, - head: git2::Oid, + head: gix_hash::ObjectId, } struct GlobBench { @@ -119,7 +120,8 @@ impl GlobBench { // stamp checked against `EXPECTED_HEAD`. let provisioned = josh_test_support::provision_repo::provision_repo( CACHE_NAME, - &git2::Oid::from_str(EXPECTED_HEAD).expect("EXPECTED_HEAD must be a valid oid"), + &gix_hash::ObjectId::from_str(EXPECTED_HEAD) + .expect("EXPECTED_HEAD must be a valid oid"), |repo| { let mut heads = vec![]; for &n_commits in HISTORY_SIZES { @@ -138,7 +140,10 @@ impl GlobBench { let repo = &provisioned.repo; for &n_commits in HISTORY_SIZES { let head = repo.refname_to_id(&format!("refs/heads/case_{n_commits}"))?; - cases.push(SizeCase { n_commits, head }); + cases.push(SizeCase { + n_commits, + head: gix_oid(head), + }); } } @@ -166,7 +171,7 @@ impl GlobBench { let repo = transaction.git2_repo(); // The tripwire only means something if the dotfiles actually exist in the raw tree. - let raw_tree = repo.find_commit(case.head)?.tree()?; + let raw_tree = repo.find_commit(git2_oid(case.head))?.tree()?; for path in ["dir_01/.hidden.rs", "dir_01/.hiddendir/inner.rs"] { anyhow::ensure!( raw_tree.get_path(Path::new(path)).is_ok(), @@ -180,14 +185,14 @@ impl GlobBench { // The gate compares against an independent git2 reference model, which only // sees what is on disk. transaction.flush_mem_odb()?; - let got = repo.find_commit(filtered)?.tree_id(); + let got = repo.find_commit(git2_oid(filtered))?.tree_id(); let (want, kept) = expected_tree(repo, case.head, &glob_pred(pattern))?; anyhow::ensure!( kept > 0, "`::{pattern}` gate kept no blobs -- benchmark would be a no-op" ); anyhow::ensure!( - got == want, + gix_oid(got) == want, "`::{pattern}` produced {got}, expected {want} -- wrong measurement" ); } @@ -198,7 +203,7 @@ impl GlobBench { // an identity/subtree fast path must reproduce bit-identically. let filter = Filter::new().pattern(PATTERN_PREFIX).expect("valid glob"); let filtered = josh_core::filter_commit(&transaction, filter, case.head)?; - let got_tree = repo.find_commit(filtered)?.tree()?; + let got_tree = repo.find_commit(git2_oid(filtered))?.tree()?; anyhow::ensure!( got_tree.len() == 1, "`::{PATTERN_PREFIX}` result must have exactly one top-level entry" @@ -245,7 +250,7 @@ fn glob_pred(pattern: &str) -> impl Fn(&str) -> bool { /// directories, then generate an `n_commits` history that churns ~`CHURN_FRACTION` of the files /// per commit. The tip is tagged with `refs/heads/case_` so the head is recoverable /// after the repo round-trips through the cache. -fn build_case(repo: &git2::Repository, n_commits: usize) -> anyhow::Result { +fn build_case(repo: &git2::Repository, n_commits: usize) -> anyhow::Result { use rand::RngExt; let gix_repo = gix::open(repo.path())?; @@ -333,7 +338,7 @@ fn build_case(repo: &git2::Repository, n_commits: usize) -> anyhow::Result anyhow::Result { - let parent = repo.find_commit(parent)?; +) -> anyhow::Result { + let parent = repo.find_commit(git2_oid(parent))?; let tree = parent.tree()?; let gix_repo = gix::open(repo.path())?; let mut builder = gix_repo.edit_tree(gix_oid(tree.id()))?; @@ -376,14 +381,14 @@ fn make_edit_commit( } let new_tree = repo.find_tree(git2_oid(builder.write()?.detach()))?; let sig = josh_commit_signature()?; - Ok(repo.commit( + Ok(gix_oid(repo.commit( None, &sig, &sig, &format!("local edit {n}"), &new_tree, &[&parent], - )?) + )?)) } fn deephistory_glob(c: &mut Criterion) { diff --git a/josh-core/benches/deephistory_prefix_flush.rs b/josh-core/benches/deephistory_prefix_flush.rs index 844130b61..0dcd8d8a0 100644 --- a/josh-core/benches/deephistory_prefix_flush.rs +++ b/josh-core/benches/deephistory_prefix_flush.rs @@ -4,6 +4,7 @@ use josh_core::git::josh_commit_signature; use josh_test_support::bench::{EntryKind, build_index, git2_oid, gix_oid, random_string}; use rand::prelude::*; use std::path::{Path, PathBuf}; +use std::str::FromStr; // This bench measures the object *write* path: unlike `:/` (which only selects existing // subtrees, so the filtered output reuses existing tree objects), a `:prefix=` filter writes @@ -71,7 +72,7 @@ const JOSH_BENCH_COMMIT_TIME: &str = "1700000000"; /// One history length and the head of its generated history. struct SizeCase { n_commits: usize, - head: git2::Oid, + head: gix_hash::ObjectId, } struct PrefixFlushBench { @@ -101,7 +102,8 @@ impl PrefixFlushBench { // stamp checked against `EXPECTED_HEAD`. let provisioned = josh_test_support::provision_repo::provision_repo( CACHE_NAME, - &git2::Oid::from_str(EXPECTED_HEAD).expect("EXPECTED_HEAD must be a valid oid"), + &gix_hash::ObjectId::from_str(EXPECTED_HEAD) + .expect("EXPECTED_HEAD must be a valid oid"), |repo| { let mut heads = vec![]; for &n_commits in HISTORY_SIZES { @@ -120,7 +122,10 @@ impl PrefixFlushBench { let repo = &provisioned.repo; for &n_commits in HISTORY_SIZES { let head = repo.refname_to_id(&format!("refs/heads/case_{n_commits}"))?; - cases.push(SizeCase { n_commits, head }); + cases.push(SizeCase { + n_commits, + head: gix_oid(head), + }); } } @@ -148,7 +153,7 @@ impl PrefixFlushBench { let filtered_tree = josh_core::objects::CommitData::read(odb, filtered)?.tree_id()?; let nested_tree = josh_core::objects::path_entry(odb, filtered_tree, Path::new(PREFIX))? - .map(|entry| josh_core::objects::git2_oid(&entry.oid)) + .map(|entry| entry.oid) .ok_or_else(|| anyhow::anyhow!("prefix filter produced no `{PREFIX}` entry"))?; let raw_tree = josh_core::objects::CommitData::read(odb, case.head)?.tree_id()?; anyhow::ensure!( @@ -172,7 +177,7 @@ impl PrefixFlushBench { /// directories, then generate an `n_commits` history that churns ~`CHURN_FRACTION` of the files per /// commit. The tip is tagged with `refs/heads/case_` so the head is recoverable after /// the repo round-trips through the cache. -fn build_case(repo: &git2::Repository, n_commits: usize) -> anyhow::Result { +fn build_case(repo: &git2::Repository, n_commits: usize) -> anyhow::Result { use rand::RngExt; let gix_repo = gix::open(repo.path())?; @@ -240,7 +245,7 @@ fn build_case(repo: &git2::Repository, n_commits: usize) -> anyhow::Result String { /// directories, then generate an `n_commits` history that churns ~`CHURN_FRACTION` of the files per /// commit. The tip is tagged with `refs/heads/case_` so the head is recoverable after the /// repo round-trips through the cache. -fn build_case(repo: &git2::Repository, n_commits: usize) -> anyhow::Result { +fn build_case(repo: &git2::Repository, n_commits: usize) -> anyhow::Result { use rand::RngExt; // Deterministic root tree: file `i` lives at `dir_{i % N_DIRS}/file_{i}`, so files are spread @@ -231,18 +236,21 @@ fn build_case(repo: &git2::Repository, n_commits: usize) -> anyhow::Result anyhow::Result { +fn build_index( + repo: &git2::Repository, + heads: &[gix_hash::ObjectId], +) -> anyhow::Result { let sig = josh_commit_signature()?; let empty_tree = repo.find_tree(repo.treebuilder(None)?.write()?)?; let parents = heads .iter() - .map(|oid| repo.find_commit(*oid)) + .map(|oid| repo.find_commit(git2_oid(*oid))) .collect::, _>>()?; let parent_refs = parents.iter().collect::>(); let index = repo.commit( @@ -253,7 +261,7 @@ fn build_index(repo: &git2::Repository, heads: &[git2::Oid]) -> anyhow::Result anyhow::Result { +fn count_history(repo: &git2::Repository, head: gix_hash::ObjectId) -> anyhow::Result { let mut walk = repo.revwalk()?; - walk.push(head)?; + walk.push(git2_oid(head))?; Ok(walk.count()) } @@ -181,7 +186,7 @@ fn random_string(rng: &mut StdRng, len: usize) -> String { /// `CHURN_PER_COMMIT` files outside it, and only with probability `SUBDIR_CHANGE_PROB` also touches a /// `dir_00` file. The tip is tagged `refs/heads/case_` so the head survives the cache /// round-trip. -fn build_case(repo: &git2::Repository, n_commits: usize) -> anyhow::Result { +fn build_case(repo: &git2::Repository, n_commits: usize) -> anyhow::Result { use rand::RngExt; // Deterministic root tree: file `i` lives at `dir_{i % N_DIRS}/file_{i}`; split the paths into the @@ -262,27 +267,30 @@ fn build_case(repo: &git2::Repository, n_commits: usize) -> anyhow::Result anyhow::Result { +fn build_index( + repo: &git2::Repository, + heads: &[gix_hash::ObjectId], +) -> anyhow::Result { let sig = josh_commit_signature()?; let empty_tree = repo.find_tree(repo.treebuilder(None)?.write()?)?; let parents = heads .iter() - .map(|oid| repo.find_commit(*oid)) + .map(|oid| repo.find_commit(git2_oid(*oid))) .collect::, _>>()?; let parent_refs = parents.iter().collect::>(); - Ok(repo.commit( + Ok(gix_oid(repo.commit( Some("refs/heads/bench-index"), &sig, &sig, "bench index", &empty_tree, &parent_refs, - )?) + )?)) } fn deephistory_subdir_distributed(c: &mut Criterion) { diff --git a/josh-core/benches/deephistory_subdir_sparse.rs b/josh-core/benches/deephistory_subdir_sparse.rs index 01319c5eb..64144ecba 100644 --- a/josh-core/benches/deephistory_subdir_sparse.rs +++ b/josh-core/benches/deephistory_subdir_sparse.rs @@ -4,6 +4,7 @@ use josh_core::git::josh_commit_signature; use josh_test_support::bench::{EntryKind, git2_oid, gix_oid}; use rand::prelude::*; use std::path::{Path, PathBuf}; +use std::str::FromStr; // The sparse counterpart to the `deephistory_subdir` bench. Both apply a plain `:/dir_00` subdir // filter to the head of a long, fixed-width history, but this one makes the extracted subdir change @@ -54,7 +55,7 @@ const JOSH_BENCH_COMMIT_TIME: &str = "1700000000"; /// One history length and the head of its generated history. struct SizeCase { n_commits: usize, - head: git2::Oid, + head: gix_hash::ObjectId, } struct SubdirBench { @@ -80,7 +81,8 @@ impl SubdirBench { let provisioned = josh_test_support::provision_repo::provision_repo( "deephistory_subdir_sparse", - &git2::Oid::from_str(EXPECTED_HEAD).expect("EXPECTED_HEAD must be a valid oid"), + &gix_hash::ObjectId::from_str(EXPECTED_HEAD) + .expect("EXPECTED_HEAD must be a valid oid"), |repo| { let mut heads = vec![]; for &n_commits in HISTORY_SIZES { @@ -99,7 +101,10 @@ impl SubdirBench { let repo = &provisioned.repo; for &n_commits in HISTORY_SIZES { let head = repo.refname_to_id(&format!("refs/heads/case_{n_commits}"))?; - cases.push(SizeCase { n_commits, head }); + cases.push(SizeCase { + n_commits, + head: gix_oid(head), + }); } } @@ -125,9 +130,9 @@ impl SubdirBench { // sees what is on disk. transaction.flush_mem_odb()?; let repo = transaction.git2_repo(); - let filtered_tree = repo.find_commit(filtered)?.tree()?.id(); + let filtered_tree = repo.find_commit(git2_oid(filtered))?.tree()?.id(); let raw_subdir_tree = repo - .find_commit(case.head)? + .find_commit(git2_oid(case.head))? .tree()? .get_path(Path::new(SUBDIR))? .id(); @@ -159,9 +164,9 @@ impl SubdirBench { } /// Number of commits reachable from `head`. -fn count_history(repo: &git2::Repository, head: git2::Oid) -> anyhow::Result { +fn count_history(repo: &git2::Repository, head: gix_hash::ObjectId) -> anyhow::Result { let mut walk = repo.revwalk()?; - walk.push(head)?; + walk.push(git2_oid(head))?; Ok(walk.count()) } @@ -180,7 +185,7 @@ fn random_string(rng: &mut StdRng, len: usize) -> String { /// `CHURN_PER_COMMIT` files outside it, and only with probability `SUBDIR_CHANGE_PROB` also touches a /// `dir_00` file. The tip is tagged `refs/heads/case_` so the head survives the cache /// round-trip. -fn build_case(repo: &git2::Repository, n_commits: usize) -> anyhow::Result { +fn build_case(repo: &git2::Repository, n_commits: usize) -> anyhow::Result { use rand::RngExt; // Deterministic root tree: file `i` lives at `dir_{i % N_DIRS}/file_{i}`; split the paths into the @@ -261,27 +266,30 @@ fn build_case(repo: &git2::Repository, n_commits: usize) -> anyhow::Result anyhow::Result { +fn build_index( + repo: &git2::Repository, + heads: &[gix_hash::ObjectId], +) -> anyhow::Result { let sig = josh_commit_signature()?; let empty_tree = repo.find_tree(repo.treebuilder(None)?.write()?)?; let parents = heads .iter() - .map(|oid| repo.find_commit(*oid)) + .map(|oid| repo.find_commit(git2_oid(*oid))) .collect::, _>>()?; let parent_refs = parents.iter().collect::>(); - Ok(repo.commit( + Ok(gix_oid(repo.commit( Some("refs/heads/bench-index"), &sig, &sig, "bench index", &empty_tree, &parent_refs, - )?) + )?)) } fn deephistory_subdir_sparse(c: &mut Criterion) { diff --git a/josh-core/benches/refs_filter_update.rs b/josh-core/benches/refs_filter_update.rs index 26148e66e..8c39898ac 100644 --- a/josh-core/benches/refs_filter_update.rs +++ b/josh-core/benches/refs_filter_update.rs @@ -4,6 +4,7 @@ use josh_core::git::josh_commit_signature; use josh_test_support::bench::{EntryKind, build_index, git2_oid, gix_oid, random_string}; use rand::prelude::*; use std::path::{Path, PathBuf}; +use std::str::FromStr; // This bench measures the *reference* surface: enumerating refs by prefix, resolving each to an oid, // looking up the (cache-hot) filtered result per ref, and force-writing the filtered refs back. The @@ -60,7 +61,7 @@ const JOSH_BENCH_COMMIT_TIME: &str = "1700000000"; /// One ref-count case and the head of its generated history. struct SizeCase { n_refs: usize, - head: git2::Oid, + head: gix_hash::ObjectId, } struct RefsBench { @@ -91,7 +92,8 @@ impl RefsBench { // checked against `EXPECTED_HEAD`. let provisioned = josh_test_support::provision_repo::provision_repo( CACHE_NAME, - &git2::Oid::from_str(EXPECTED_HEAD).expect("EXPECTED_HEAD must be a valid oid"), + &gix_hash::ObjectId::from_str(EXPECTED_HEAD) + .expect("EXPECTED_HEAD must be a valid oid"), |repo| { let mut heads = vec![]; for &n_refs in REF_COUNTS { @@ -110,7 +112,10 @@ impl RefsBench { let repo = &provisioned.repo; for &n_refs in REF_COUNTS { let head = repo.refname_to_id(&format!("refs/heads/case_{n_refs}_tip"))?; - cases.push(SizeCase { n_refs, head }); + cases.push(SizeCase { + n_refs, + head: gix_oid(head), + }); } } @@ -134,9 +139,9 @@ impl RefsBench { // sees what is on disk. transaction.flush_mem_odb()?; let repo = transaction.git2_repo(); - let filtered_tree = repo.find_commit(filtered)?.tree()?.id(); + let filtered_tree = repo.find_commit(git2_oid(filtered))?.tree()?.id(); let raw_subdir_tree = repo - .find_commit(case.head)? + .find_commit(git2_oid(case.head))? .tree()? .get_path(Path::new(SUBDIR))? .id(); @@ -169,7 +174,7 @@ impl RefsBench { /// directories, then generate `n_refs` churn commits, pointing `refs/heads/case_/change_` at /// each. The tip additionally gets `refs/heads/case__tip` so the head is recoverable after /// the repo round-trips through the cache. -fn build_case(repo: &git2::Repository, n_refs: usize) -> anyhow::Result { +fn build_case(repo: &git2::Repository, n_refs: usize) -> anyhow::Result { use rand::RngExt; let gix_repo = gix::open(repo.path())?; @@ -243,7 +248,7 @@ fn build_case(repo: &git2::Repository, n_refs: usize) -> anyhow::Result josh_filter::Filter { fn build_initial_state( repo: &git2::Repository, -) -> anyhow::Result<(git2::Oid, Vec)> { +) -> anyhow::Result<(gix_hash::ObjectId, Vec)> { const PATH_COMPONENT_LENGTH: usize = 15; // Create multiple nested subfolders in the benchmark repo; aiming for a uniform @@ -178,14 +180,14 @@ fn build_initial_state( &[], )?; - Ok((head, all_paths)) + Ok((gix_oid(head), all_paths)) } fn build_history( repo: &git2::Repository, paths: &[std::path::PathBuf], - mut head: git2::Oid, -) -> anyhow::Result { + mut head: gix_hash::ObjectId, +) -> anyhow::Result { use rand::RngExt; // In every commit, we update 10% files in the repo @@ -212,7 +214,7 @@ fn build_history( let mut pinned = std::collections::BTreeSet::::new(); for i_commit in 0..N_COMMITS { - let parent = repo.find_commit(head)?; + let parent = repo.find_commit(git2_oid(head))?; let tree = parent.tree()?; let mut builder = gix_repo.edit_tree(gix_oid(tree.id()))?; @@ -265,14 +267,14 @@ fn build_history( let new_tree = repo.find_tree(new_tree)?; let sig = josh_commit_signature()?; - head = repo.commit( + head = gix_oid(repo.commit( Some("refs/heads/main"), &sig, &sig, &format!("commit {i_commit}"), &new_tree, &[&parent], - )?; + )?); } Ok(head) diff --git a/josh-core/benches/ultrawide_pin_hook.rs b/josh-core/benches/ultrawide_pin_hook.rs index 9ce75a56c..f03719857 100644 --- a/josh-core/benches/ultrawide_pin_hook.rs +++ b/josh-core/benches/ultrawide_pin_hook.rs @@ -4,6 +4,7 @@ use josh_test_support::bench::{EntryKind, git2_oid, gix_oid}; use rand::prelude::*; use std::collections::HashMap; use std::path::PathBuf; +use std::str::FromStr; use std::sync::Arc; // Tree sizes (number of files) benchmarked. Kept small in debug builds so `--test` runs stay fast. @@ -73,14 +74,14 @@ const JOSH_BENCH_COMMIT_TIME: &str = "1700000000"; // pin through a stored `workspace.josh`, which pays a filter parse+legalize per commit; the hook // serves a pre-built per-commit filter by oid instead, isolating the pin evaluation itself. struct BenchPinHook { - per_path: HashMap, - one_tree: HashMap, + per_path: HashMap, + one_tree: HashMap, } impl josh_core::cache::FilterHook for BenchPinHook { fn filter_for_commit( &self, - commit_oid: git2::Oid, + commit_oid: gix_hash::ObjectId, arg: &str, ) -> anyhow::Result { let map = match arg { @@ -98,7 +99,7 @@ impl josh_core::cache::FilterHook for BenchPinHook { /// shared hook. struct SizeCase { size: usize, - head: git2::Oid, + head: gix_hash::ObjectId, } struct PinBench { @@ -133,7 +134,8 @@ impl PinBench { // `EXPECTED_HEAD`. let provisioned = josh_test_support::provision_repo::provision_repo( "ultrawide_pin_hook", - &git2::Oid::from_str(EXPECTED_HEAD).expect("EXPECTED_HEAD must be a valid oid"), + &gix_hash::ObjectId::from_str(EXPECTED_HEAD) + .expect("EXPECTED_HEAD must be a valid oid"), |repo| { let mut heads = vec![]; for &size in SIZES { @@ -156,7 +158,7 @@ impl PinBench { { let repo = &provisioned.repo; for &size in SIZES { - let head = repo.refname_to_id(&format!("refs/heads/case_{size}"))?; + let head = gix_oid(repo.refname_to_id(&format!("refs/heads/case_{size}"))?); record_case(repo, head, &mut per_path, &mut one_tree)?; cases.push(SizeCase { size, head }); } @@ -299,7 +301,7 @@ fn pin_filter_tree( /// per commit. The tip is tagged with `refs/heads/case_` so the head is recoverable after the /// repo round-trips through the cache; the pinned set and per-commit pin filters are not recorded here /// -- they are reconstructed from tree diffs in `record_case`. -fn build_case(repo: &git2::Repository, size: usize) -> anyhow::Result { +fn build_case(repo: &git2::Repository, size: usize) -> anyhow::Result { use rand::RngExt; // Distribute files uniformly across nested subfolders. @@ -383,18 +385,21 @@ fn build_case(repo: &git2::Repository, size: usize) -> anyhow::Result "bench case tip", )?; - Ok(head) + Ok(gix_oid(head)) } /// Aggregate every case tip under one index commit. Its oid changes whenever any case head changes, /// making it a faithful content-addressed cache stamp for the entire repo, and it keeps all cases /// reachable so provision_repo's `git prune` retains the full history. It is never filtered. -fn build_index(repo: &git2::Repository, heads: &[git2::Oid]) -> anyhow::Result { +fn build_index( + repo: &git2::Repository, + heads: &[gix_hash::ObjectId], +) -> anyhow::Result { let sig = josh_commit_signature()?; let empty_tree = repo.find_tree(repo.treebuilder(None)?.write()?)?; let parents = heads .iter() - .map(|oid| repo.find_commit(*oid)) + .map(|oid| repo.find_commit(git2_oid(*oid))) .collect::, _>>()?; let parent_refs = parents.iter().collect::>(); let index = repo.commit( @@ -405,7 +410,7 @@ fn build_index(repo: &git2::Repository, heads: &[git2::Oid]) -> anyhow::Result anyhow::Result, - one_tree: &mut HashMap, + head: gix_hash::ObjectId, + per_path: &mut HashMap, + one_tree: &mut HashMap, ) -> anyhow::Result<()> { // Collect the linear history oldest-first so the pinned set can be folded forward across commits. let mut chain = vec![]; let mut oid = head; loop { - let commit = repo.find_commit(oid)?; + let commit = repo.find_commit(git2_oid(oid))?; chain.push(oid); if commit.parent_count() == 0 { break; } - oid = commit.parent_id(0)?; + oid = gix_oid(commit.parent_id(0)?); } chain.reverse(); let mut pinned = std::collections::BTreeSet::::new(); for (commit_index, &oid) in chain.iter().enumerate() { - let commit = repo.find_commit(oid)?; + let commit = repo.find_commit(git2_oid(oid))?; // Re-roll the hold status of every churned path; unchurned paths keep the status they carried // over. The root commit has no parent, so its churned set is empty and the pinned set stays diff --git a/josh-core/benches/unapply.rs b/josh-core/benches/unapply.rs index bca1eac11..8b2ebd204 100644 --- a/josh-core/benches/unapply.rs +++ b/josh-core/benches/unapply.rs @@ -5,6 +5,7 @@ use josh_core::history::{OrphansMode, unapply_filter}; use josh_test_support::bench::{EntryKind, git2_oid, gix_oid}; use rand::prelude::*; use std::path::PathBuf; +use std::str::FromStr; use std::sync::atomic::{AtomicUsize, Ordering}; // Benches the unapply (push) path of josh: a client pushes commits built on the FILTERED @@ -47,11 +48,11 @@ const JOSH_BENCH_COMMIT_TIME: &str = "1700000000"; struct SizeCase { n_commits: usize, - head: git2::Oid, - filtered_head: git2::Oid, + head: gix_hash::ObjectId, + filtered_head: gix_hash::ObjectId, /// A filtered commit roughly in the middle of the filtered history (first-parent /// chase), the tip of the simulated new-branch push. - filtered_mid: git2::Oid, + filtered_mid: gix_hash::ObjectId, } struct UnapplyBench { @@ -72,7 +73,8 @@ impl UnapplyBench { let provisioned = josh_test_support::provision_repo::provision_repo( "unapply", - &git2::Oid::from_str(EXPECTED_HEAD).expect("EXPECTED_HEAD must be a valid oid"), + &gix_hash::ObjectId::from_str(EXPECTED_HEAD) + .expect("EXPECTED_HEAD must be a valid oid"), |repo| { let mut heads = vec![]; for &n_commits in HISTORY_SIZES { @@ -101,7 +103,7 @@ impl UnapplyBench { let transaction = context.open()?; let repo = transaction.git2_repo(); for &n_commits in HISTORY_SIZES { - let head = repo.refname_to_id(&format!("refs/heads/case_{n_commits}"))?; + let head = gix_oid(repo.refname_to_id(&format!("refs/heads/case_{n_commits}"))?); let filtered_head = josh_core::filter_commit(&transaction, filter, head)?; // The first-parent chase below reads the filtered commits through the // repository handle, which only sees what is on disk. @@ -110,9 +112,9 @@ impl UnapplyBench { // First-parent chase to the middle of the filtered history. let mut filtered_mid = filtered_head; for _ in 0..n_commits / 2 { - let commit = repo.find_commit(filtered_mid)?; + let commit = repo.find_commit(git2_oid(filtered_mid))?; match commit.parent_id(0) { - Ok(p) => filtered_mid = p, + Ok(p) => filtered_mid = gix_oid(p), Err(_) => break, } } @@ -147,7 +149,7 @@ fn random_string(rng: &mut StdRng, len: usize) -> String { /// deephistory-style case builder: fixed tree, `n_commits` of ~10% churn, tip tagged as /// `refs/heads/case_`. -fn build_case(repo: &git2::Repository, n_commits: usize) -> anyhow::Result { +fn build_case(repo: &git2::Repository, n_commits: usize) -> anyhow::Result { use rand::RngExt; let gix_repo = gix::open(repo.path())?; @@ -210,15 +212,18 @@ fn build_case(repo: &git2::Repository, n_commits: usize) -> anyhow::Result anyhow::Result { +fn build_index( + repo: &git2::Repository, + heads: &[gix_hash::ObjectId], +) -> anyhow::Result { let sig = josh_commit_signature()?; let empty_tree = repo.find_tree(repo.treebuilder(None)?.write()?)?; let parents = heads .iter() - .map(|oid| repo.find_commit(*oid)) + .map(|oid| repo.find_commit(git2_oid(*oid))) .collect::, _>>()?; let parent_refs = parents.iter().collect::>(); let index = repo.commit( @@ -229,7 +234,7 @@ fn build_index(repo: &git2::Repository, heads: &[git2::Oid]) -> anyhow::Result anyhow::Result anyhow::Result { +) -> anyhow::Result { let sig = josh_commit_signature()?; let mut head = base; let gix_repo = gix::open(repo.path())?; for i in 0..PUSH_LEN { - let parent = repo.find_commit(head)?; + let parent = repo.find_commit(git2_oid(head))?; let tree = parent.tree()?; let mut builder = gix_repo.edit_tree(gix_oid(tree.id()))?; let blob = repo.blob(format!("push {salt} {i}").as_bytes())?; builder.upsert("file_0000", EntryKind::Blob, gix_oid(blob))?; let new_tree = repo.find_tree(git2_oid(builder.write()?.detach()))?; - head = repo.commit( + head = gix_oid(repo.commit( None, &sig, &sig, &format!("push {salt} {i}"), &new_tree, &[&parent], - )?; + )?); } Ok(head) } @@ -350,7 +355,7 @@ fn unapply_new_branch(c: &mut Criterion) { &transaction, bench.filter, case.head, - git2::Oid::ZERO_SHA1, + gix_hash::ObjectId::null(gix_hash::Kind::Sha1), case.filtered_mid, OrphansMode::Fail, None, @@ -380,7 +385,7 @@ fn unapply_new_branch(c: &mut Criterion) { &transaction, bench.filter, case.head, - git2::Oid::ZERO_SHA1, + gix_hash::ObjectId::null(gix_hash::Kind::Sha1), case.filtered_mid, OrphansMode::Fail, None, diff --git a/josh-core/benches/widetree_glob.rs b/josh-core/benches/widetree_glob.rs index f0f77c2e2..7abaecff7 100644 --- a/josh-core/benches/widetree_glob.rs +++ b/josh-core/benches/widetree_glob.rs @@ -6,6 +6,7 @@ use josh_test_support::bench::{ }; use rand::prelude::*; use std::path::{Path, PathBuf}; +use std::str::FromStr; // The scaling parameter of this benchmark is tree *width*, not history length. History stays a // fixed, short N_COMMITS while the number of files grows per case; each `filter_commit` apply of a @@ -74,7 +75,7 @@ const JOSH_BENCH_COMMIT_TIME: &str = "1700000000"; /// One tree width and the head of its generated history. struct SizeCase { n_files: usize, - head: git2::Oid, + head: gix_hash::ObjectId, } struct GlobBench { @@ -103,7 +104,8 @@ impl GlobBench { // `EXPECTED_HEAD`. let provisioned = josh_test_support::provision_repo::provision_repo( CACHE_NAME, - &git2::Oid::from_str(EXPECTED_HEAD).expect("EXPECTED_HEAD must be a valid oid"), + &gix_hash::ObjectId::from_str(EXPECTED_HEAD) + .expect("EXPECTED_HEAD must be a valid oid"), |repo| { let mut heads = vec![]; for &n_files in TREE_SIZES { @@ -122,7 +124,10 @@ impl GlobBench { let repo = &provisioned.repo; for &n_files in TREE_SIZES { let head = repo.refname_to_id(&format!("refs/heads/case_{n_files}"))?; - cases.push(SizeCase { n_files, head }); + cases.push(SizeCase { + n_files, + head: gix_oid(head), + }); } } @@ -152,11 +157,11 @@ impl GlobBench { // The gate compares against an independent git2 reference model, which only // sees what is on disk. transaction.flush_mem_odb()?; - let got = repo.find_commit(filtered)?.tree_id(); + let got = repo.find_commit(git2_oid(filtered))?.tree_id(); let (want, kept) = expected_tree(repo, case.head, &|p| p.ends_with(".rs"))?; anyhow::ensure!(kept > 0, "recursive gate kept no blobs -- would be a no-op"); anyhow::ensure!( - got == want, + gix_oid(got) == want, "`::{PATTERN_RECURSIVE}` produced {got}, expected {want} (n_files {})", case.n_files ); @@ -168,13 +173,13 @@ impl GlobBench { let filter = Filter::new().pattern(PATTERN_PREFIX).expect("valid glob"); let filtered = josh_core::filter_commit(&transaction, filter, case.head)?; transaction.flush_mem_odb()?; - let got_tree = repo.find_commit(filtered)?.tree()?; + let got_tree = repo.find_commit(git2_oid(filtered))?.tree()?; anyhow::ensure!( got_tree.len() == 1, "`::{PATTERN_PREFIX}` result must have exactly one top-level entry" ); let raw_subtree = repo - .find_commit(case.head)? + .find_commit(git2_oid(case.head))? .tree()? .get_path(Path::new(PREFIX_DIR))? .id(); @@ -187,14 +192,14 @@ impl GlobBench { let filter = Filter::new().pattern(PATTERN_SPARSE).expect("valid glob"); let filtered = josh_core::filter_commit(&transaction, filter, case.head)?; transaction.flush_mem_odb()?; - let got = repo.find_commit(filtered)?.tree_id(); + let got = repo.find_commit(git2_oid(filtered))?.tree_id(); let (want, kept) = expected_tree(repo, case.head, &|p| p.ends_with(".toml"))?; anyhow::ensure!( kept == N_SPARSE, "sparse gate kept {kept} blobs, expected exactly {N_SPARSE}" ); anyhow::ensure!( - got == want, + gix_oid(got) == want, "`::{PATTERN_SPARSE}` produced {got}, expected {want} (n_files {})", case.n_files ); @@ -227,7 +232,7 @@ fn file_path(i: usize) -> PathBuf { /// CHURN_PER_COMMIT files at deterministic indices. The tip is tagged with /// `refs/heads/case_` so the head is recoverable after the repo round-trips through the /// cache. -fn build_case(repo: &git2::Repository, n_files: usize) -> anyhow::Result { +fn build_case(repo: &git2::Repository, n_files: usize) -> anyhow::Result { let gix_repo = gix::open(repo.path())?; let baseline = repo.treebuilder(None)?.write()?; let mut builder = gix_repo.edit_tree(gix_oid(baseline))?; @@ -298,7 +303,7 @@ fn build_case(repo: &git2::Repository, n_files: usize) -> anyhow::Result anyhow::Result>; + ) -> anyhow::Result>; fn write( &self, filter: crate::filter::Filter, - from: git2::Oid, - to: git2::Oid, + from: gix_hash::ObjectId, + to: gix_hash::ObjectId, hint: HistoryGraphHint, tree_keyed: bool, ) -> anyhow::Result<()>; diff --git a/josh-core/src/cache/distributed.rs b/josh-core/src/cache/distributed.rs index 5f16decf1..ccd1842b6 100644 --- a/josh-core/src/cache/distributed.rs +++ b/josh-core/src/cache/distributed.rs @@ -11,7 +11,8 @@ use std::collections::HashMap; const FLUSH_AFTER: usize = 1000; pub struct DistributedCacheBackend { - new_entries: std::sync::Mutex>>, + new_entries: + std::sync::Mutex>>, repo: std::sync::Mutex, // Whether this backend accepts writes. The default ([`Self::new`]) is read-only: regular // sessions consume the fetched cache but should not each grow the shard chains with a @@ -30,7 +31,7 @@ pub struct DistributedCacheBackend { // in `mem_odb` only, so the refs are published exclusively by a forced flush, after a drain // has made every buffered object durable: a ref on disk must never point to objects that // only exist in memory. - pending_refs: std::sync::Mutex>, + pending_refs: std::sync::Mutex>, // Filter -> persisted tree id (`as_tree`), used to name cache refs. `as_tree` resolves // insert OIDs, so ref names always reference persisted, reachable filter trees even when // the filter passed in still contains unresolved ones. @@ -103,14 +104,11 @@ impl DistributedCacheBackend { } let rp = ref_path(self.tree_id(odb, *filter)?, *shard); - // Base the update on the newest unpublished commit for this ref when one exists: - // basing on the published tip would drop the entries of earlier unpublished - // batches. + // Include earlier unpublished batches. let base = if let Some(oid) = pending.get(&rp) { Some(*oid) } else if let Ok(r) = repo.revparse_single(&rp) { - // PORT: resolves a ref -- stays on git2 until flag day. - Some(r.peel_to_commit()?.id()) + Some(objects::gix_oid(r.peel_to_commit()?.id())) } else { None }; @@ -119,7 +117,7 @@ impl DistributedCacheBackend { let root = match base { Some(commit) => { let tree = objects::CommitData::read(odb, commit)?.tree_id()?; - gix_object::FindExt::find_tree(odb, &objects::gix_oid(tree), &mut buf)?.into() + gix_object::FindExt::find_tree(odb, &tree, &mut buf)?.into() } None => gix_object::Tree::default(), }; @@ -132,7 +130,7 @@ impl DistributedCacheBackend { // in tree entries -- so it is encoded as a blob entry pointing at the empty blob; // the entry mode disambiguates on read. for (from, to) in &mut *m { - let (kind, target) = if *to == git2::Oid::ZERO_SHA1 { + let (kind, target) = if *to == gix_hash::ObjectId::null(gix_hash::Kind::Sha1) { ( gix_object::tree::EntryKind::Blob, objects::write_blob(odb, &[])?, @@ -140,7 +138,7 @@ impl DistributedCacheBackend { } else { (gix_object::tree::EntryKind::Commit, *to) }; - editor.upsert(fanout(*from), kind, objects::gix_oid(target))?; + editor.upsert(fanout(*from), kind, target)?; } let updated = editor.write(|tree| { @@ -151,7 +149,7 @@ impl DistributedCacheBackend { let signature = crate::git::josh_commit_signature()?; let commit = objects::write_commit( odb, - objects::git2_oid(&updated), + updated, base.as_slice(), &signature, &signature, @@ -182,7 +180,7 @@ impl DistributedCacheBackend { self.mem_odb.flush()?; for (rp, commit) in pending.drain() { - repo.reference(&rp, commit, true, "cache")?; + repo.reference(&rp, objects::git2_oid(&commit), true, "cache")?; } Ok(()) @@ -213,7 +211,7 @@ fn ref_path(filter_tree_id: gix_hash::ObjectId, shard: u64) -> String { // rewrites subtrees that grow with the accumulated shard. A single 2-hex level goes quadratic on // dense shards for exactly that reason, while a third level only adds one more tree write per // entry without making any subtree meaningfully smaller. -fn fanout(commit: git2::Oid) -> [gix_object::bstr::BString; 3] { +fn fanout(commit: gix_hash::ObjectId) -> [gix_object::bstr::BString; 3] { let commit = commit.to_string(); [commit[..2].into(), commit[2..5].into(), commit[5..].into()] } @@ -222,10 +220,10 @@ impl CacheBackend for DistributedCacheBackend { fn read( &self, filter: Filter, - from: git2::Oid, + from: gix_hash::ObjectId, hint: HistoryGraphHint, tree_keyed: bool, - ) -> anyhow::Result> { + ) -> anyhow::Result> { if filter == filter::sequence_number() || filter == filter::reachable_roots() { return Ok(None); } @@ -252,21 +250,19 @@ impl CacheBackend for DistributedCacheBackend { let odb = self.odb.lock().unwrap(); let odb = &*odb; let rp = ref_path(self.tree_id(odb, filter)?, shard); - // Flushed-but-unpublished entries live in a pending commit (see `flush`), not behind - // the ref yet; prefer it so in-process reads keep seeing everything ever flushed. + // Prefer unpublished entries from this process. let pending = self.pending_refs.lock().unwrap(); let tree = if let Some(oid) = pending.get(&rp) { objects::CommitData::read(odb, *oid)?.tree_id()? } else if let Ok(r) = repo.revparse_single(&rp) { - // PORT: resolves a ref -- stays on git2 until flag day. - r.peel_to_tree()?.id() + objects::gix_oid(r.peel_to_tree()?.id()) } else { return Ok(None); }; std::mem::drop(pending); let mut buf = Vec::new(); - let root = gix_object::FindExt::find_tree_iter(odb, &objects::gix_oid(tree), &mut buf)?; + let root = gix_object::FindExt::find_tree_iter(odb, &tree, &mut buf)?; let mut entry_buf = Vec::new(); let entry = root .lookup_entry(odb, &mut entry_buf, fanout(from)) @@ -283,16 +279,16 @@ impl CacheBackend for DistributedCacheBackend { // Gitlink entries carry the target oid directly; any other mode is the empty-blob // encoding of `Oid::ZERO_SHA1` (see `flush`). if e.mode.kind() == gix_object::tree::EntryKind::Commit { - return Ok(Some(objects::git2_oid(&e.oid))); + return Ok(Some(e.oid)); } - Ok(Some(git2::Oid::ZERO_SHA1)) + Ok(Some(gix_hash::ObjectId::null(gix_hash::Kind::Sha1))) } fn write( &self, filter: Filter, - from: git2::Oid, - to: git2::Oid, + from: gix_hash::ObjectId, + to: gix_hash::ObjectId, hint: HistoryGraphHint, // Writes stay eligibility-gated for tree-keyed records too: subtrees recur // across commits, so a stable subtree is still caught at some sampled commit. diff --git a/josh-core/src/cache/history_graph.rs b/josh-core/src/cache/history_graph.rs index 4296d9e63..f7753ee40 100644 --- a/josh-core/src/cache/history_graph.rs +++ b/josh-core/src/cache/history_graph.rs @@ -29,7 +29,7 @@ use super::transaction::Transaction; #[derive(Debug, Clone)] pub struct HistoryGraphInfo { pub sequence_number: u64, - pub reachable_roots: Vec, + pub reachable_roots: Vec, } /// Returns just the sequence number for `input`. @@ -38,7 +38,10 @@ pub struct HistoryGraphInfo { /// sequence number is available directly from the cached hint, so callers that /// only compare sequence numbers avoid a per-commit `find_blob` + parse that /// would otherwise be discarded. -pub fn compute_sequence_number(transaction: &Transaction, input: git2::Oid) -> anyhow::Result { +pub fn compute_sequence_number( + transaction: &Transaction, + input: gix_hash::ObjectId, +) -> anyhow::Result { Ok(ensure_hint_cached(transaction, input)?.0.sequence_number) } @@ -47,7 +50,7 @@ pub fn compute_sequence_number(transaction: &Transaction, input: git2::Oid) -> a /// eligibility decisions without any commit read. pub fn compute_history_hint( transaction: &Transaction, - input: git2::Oid, + input: gix_hash::ObjectId, ) -> anyhow::Result { Ok(ensure_hint_cached(transaction, input)?.0) } @@ -60,7 +63,7 @@ pub fn compute_history_hint( /// commit reuses its parent's blob OID, avoiding read/write entirely. pub fn collect_history_graph_info( transaction: &Transaction, - input: git2::Oid, + input: gix_hash::ObjectId, ) -> anyhow::Result { let (hint, blob) = ensure_hint_cached(transaction, input)?; @@ -78,16 +81,20 @@ pub fn collect_history_graph_info( /// `Ok(false)` (matching `merge_base_many`'s error behavior on invalid input). pub fn parents_share_root( transaction: &Transaction, - parent_ids: &[git2::Oid], + parent_ids: &[gix_hash::ObjectId], ) -> anyhow::Result { - if parent_ids.is_empty() || parent_ids.iter().any(|x| *x == git2::Oid::ZERO_SHA1) { + if parent_ids.is_empty() + || parent_ids + .iter() + .any(|x| *x == gix_hash::ObjectId::null(gix_hash::Kind::Sha1)) + { return Ok(false); } // Ensure each parent's graph info is cached, then collect the cached blob // OIDs. If all parents reference the same blob, their root sets are // identical — they trivially share every root without reading any blob. - let parent_blobs: Vec = parent_ids + let parent_blobs: Vec = parent_ids .iter() .map(|p| Ok(ensure_hint_cached(transaction, *p)?.1)) .collect::>>()?; @@ -98,7 +105,7 @@ pub fn parents_share_root( } // Parents disagree on the roots blob: read each blob and intersect. - let mut common: std::collections::BTreeSet = + let mut common: std::collections::BTreeSet = read_roots_blob(transaction.odb(), first_blob)? .into_iter() .collect(); @@ -122,21 +129,21 @@ pub fn parents_share_root( /// and writes. fn ensure_hint_cached( transaction: &Transaction, - input: git2::Oid, -) -> anyhow::Result<(HistoryGraphHint, git2::Oid)> { + input: gix_hash::ObjectId, +) -> anyhow::Result<(HistoryGraphHint, gix_hash::ObjectId)> { if let Some(hint) = try_read_cached_hint(transaction, input)? { return Ok(hint); } let odb = transaction.odb(); - if !odb.contains(crate::objects::gix_oid(input)) { + if !odb.contains(input) { return Err(anyhow!("ensure_hint_cached: input does not exist")); } let parent_ids = crate::git::read_parent_ids(odb, input)?; // Fast path: every parent already has both pieces cached. - let parents_hint: Option> = parent_ids + let parents_hint: Option> = parent_ids .iter() .map(|p| { Ok(try_read_cached_hint(transaction, *p)? @@ -170,7 +177,7 @@ fn ensure_hint_cached( })?; for &oid in sorted.iter().rev() { - let parents_hint: Vec<(u64, git2::Oid)> = crate::git::read_parent_ids(odb, oid)? + let parents_hint: Vec<(u64, gix_hash::ObjectId)> = crate::git::read_parent_ids(odb, oid)? .into_iter() .map(|p| { try_read_cached_hint(transaction, p)? @@ -192,9 +199,9 @@ fn ensure_hint_cached( /// OID (or, for the root case, writes a single-element blob). fn derive_from_parents( odb: &josh_memodb::Odb, - self_oid: git2::Oid, - parents_hint: &[(u64, git2::Oid)], -) -> anyhow::Result<(HistoryGraphHint, git2::Oid)> { + self_oid: gix_hash::ObjectId, + parents_hint: &[(u64, gix_hash::ObjectId)], +) -> anyhow::Result<(HistoryGraphHint, gix_hash::ObjectId)> { if parents_hint.is_empty() { // Parentless: this commit *is* its own only reachable root. return Ok(( @@ -230,7 +237,7 @@ fn derive_from_parents( let roots_blob = if parents_hint.iter().all(|(_, b)| *b == first_blob) { first_blob } else { - let mut set: std::collections::BTreeSet = Default::default(); + let mut set: std::collections::BTreeSet = Default::default(); for (_, blob_oid) in parents_hint { set.extend(read_roots_blob(odb, *blob_oid)?); } @@ -251,8 +258,8 @@ fn derive_from_parents( fn try_read_cached_hint( transaction: &Transaction, - input: git2::Oid, -) -> anyhow::Result> { + input: gix_hash::ObjectId, +) -> anyhow::Result> { let Some(seq) = transaction.get(crate::filter::sequence_number(), input)? else { return Ok(None); }; @@ -264,8 +271,8 @@ fn try_read_cached_hint( fn store_hint( transaction: &Transaction, - input: git2::Oid, - hint: (HistoryGraphHint, git2::Oid), + input: gix_hash::ObjectId, + hint: (HistoryGraphHint, gix_hash::ObjectId), ) -> anyhow::Result<()> { let (hint, roots_blob) = hint; transaction.insert( @@ -278,18 +285,22 @@ fn store_hint( Ok(()) } -fn write_roots_blob(odb: &josh_memodb::Odb, roots: &[git2::Oid]) -> anyhow::Result { +fn write_roots_blob( + odb: &josh_memodb::Odb, + roots: &[gix_hash::ObjectId], +) -> anyhow::Result { let mut bytes = Vec::with_capacity(roots.len() * 20); for r in roots { bytes.extend_from_slice(r.as_bytes()); } - Ok(crate::objects::git2_oid( - &odb.write(gix_object::Kind::Blob, &bytes), - )) + Ok(odb.write(gix_object::Kind::Blob, &bytes)) } -fn read_roots_blob(odb: &josh_memodb::Odb, oid: git2::Oid) -> anyhow::Result> { - let (kind, content) = odb.read(crate::objects::gix_oid(oid))?; +fn read_roots_blob( + odb: &josh_memodb::Odb, + oid: gix_hash::ObjectId, +) -> anyhow::Result> { + let (kind, content) = odb.read(oid)?; if kind != gix_object::Kind::Blob { return Err(anyhow!("reachable_roots object {} is not a blob", oid)); } @@ -303,7 +314,7 @@ fn read_roots_blob(odb: &josh_memodb::Odb, oid: git2::Oid) -> anyhow::Result anyhow::Result git2::Oid { +pub(crate) fn oid_from_hint(hint: HistoryGraphHint) -> gix_hash::ObjectId { let mut bytes = [0u8; 20]; bytes[10] = ((hint.jump_is_second as u8) << 7) | hint.jump_delta; bytes[11] = hint.parent_count; // place the 8 integer bytes at the end (big-endian) bytes[20 - 8..].copy_from_slice(&hint.sequence_number.to_be_bytes()); // Safe: length is exactly 20 - git2::Oid::from_bytes(&bytes).expect("20-byte OID construction cannot fail") + gix_hash::ObjectId::from_bytes_or_panic(&bytes) } /// Decode a hint from an OID encoded by `oid_from_hint`. -pub(crate) fn hint_from_oid(oid: git2::Oid) -> HistoryGraphHint { +pub(crate) fn hint_from_oid(oid: gix_hash::ObjectId) -> HistoryGraphHint { let b = oid.as_bytes(); let mut n = [0u8; 8]; n.copy_from_slice(&b[20 - 8..]); // take the last 8 bytes diff --git a/josh-core/src/cache/sled.rs b/josh-core/src/cache/sled.rs index 7446173a1..9c872a167 100644 --- a/josh-core/src/cache/sled.rs +++ b/josh-core/src/cache/sled.rs @@ -102,14 +102,14 @@ impl CacheBackend for SledCacheBackend { fn read( &self, filter: Filter, - from: git2::Oid, + from: gix_hash::ObjectId, _hint: HistoryGraphHint, // Sled stores records by filter alone, so the key kind needs no special handling. _tree_keyed: bool, - ) -> anyhow::Result> { + ) -> anyhow::Result> { let tree = STATE.lock().unwrap().tree(filter)?; if let Some(oid) = tree.get(from.as_bytes())? { - Ok(Some(git2::Oid::from_bytes(&oid)?)) + Ok(Some(gix_hash::ObjectId::try_from(&oid[..])?)) } else { Ok(None) } @@ -118,8 +118,8 @@ impl CacheBackend for SledCacheBackend { fn write( &self, filter: Filter, - from: git2::Oid, - to: git2::Oid, + from: gix_hash::ObjectId, + to: gix_hash::ObjectId, _hint: HistoryGraphHint, _tree_keyed: bool, ) -> anyhow::Result<()> { diff --git a/josh-core/src/cache/stack.rs b/josh-core/src/cache/stack.rs index a0072c7b8..ce4cd23d7 100644 --- a/josh-core/src/cache/stack.rs +++ b/josh-core/src/cache/stack.rs @@ -31,8 +31,8 @@ impl CacheStack { pub fn write_all( &self, filter: filter::Filter, - from: git2::Oid, - to: git2::Oid, + from: gix_hash::ObjectId, + to: gix_hash::ObjectId, hint: HistoryGraphHint, tree_keyed: bool, ) -> anyhow::Result<()> { @@ -66,10 +66,10 @@ impl CacheStack { pub fn read_propagate( &self, filter: filter::Filter, - from: git2::Oid, + from: gix_hash::ObjectId, hint: HistoryGraphHint, tree_keyed: bool, - ) -> anyhow::Result> { + ) -> anyhow::Result> { let values = self .backends .iter() diff --git a/josh-core/src/cache/transaction.rs b/josh-core/src/cache/transaction.rs index cb0acc230..461bde238 100644 --- a/josh-core/src/cache/transaction.rs +++ b/josh-core/src/cache/transaction.rs @@ -10,7 +10,7 @@ use std::sync::{LazyLock, RwLock}; pub trait FilterHook { fn filter_for_commit( &self, - commit_oid: git2::Oid, + commit_oid: gix_hash::ObjectId, arg: &str, ) -> anyhow::Result; } @@ -21,9 +21,9 @@ pub struct Head { /// itself when detached. Either way, this is the ref to update to move HEAD. pub reference: String, /// The unpeeled target of [`Head::reference`], for guarding an update against it. - pub target: git2::Oid, + pub target: gix_hash::ObjectId, /// The commit HEAD resolves to, annotated tags peeled. - pub commit: git2::Oid, + pub commit: gix_hash::ObjectId, } impl Head { @@ -51,7 +51,7 @@ pub enum Expected { /// The ref must not exist yet. Absent, /// The ref must currently point at exactly this oid. - At(git2::Oid), + At(gix_hash::ObjectId), } /// Parse `refname` as the fully qualified name every ref API method requires it to be. @@ -81,32 +81,33 @@ fn previous_value(expected: Expected) -> gix::refs::transaction::PreviousValue { match expected { Expected::Any => PreviousValue::Any, Expected::Absent => PreviousValue::MustNotExist, - Expected::At(old) => PreviousValue::MustExistAndMatch(gix::refs::Target::Object( - crate::objects::gix_oid(old), - )), + Expected::At(old) => PreviousValue::MustExistAndMatch(gix::refs::Target::Object(old)), } } -static REF_CACHE: LazyLock>>> = - LazyLock::new(Default::default); +static REF_CACHE: LazyLock< + RwLock>>, +> = LazyLock::new(Default::default); -static POPULATE_MAP: LazyLock>> = - LazyLock::new(Default::default); +static POPULATE_MAP: LazyLock< + RwLock>, +> = LazyLock::new(Default::default); // Keyed by (input tree, pattern key, NFA state mask). The state mask makes entries independent // of the path a subtree was reached through; the legacy full-path fallback folds its root path // into a synthetic pattern key and uses mask 0. -static GLOB_MAP: LazyLock>> = - LazyLock::new(Default::default); +static GLOB_MAP: LazyLock< + RwLock>, +> = LazyLock::new(Default::default); // Path-projection memoization for `:PATHS` and its inverse, keyed by (input tree oid, root path). // Both are pure functions of the input tree, and workspace filters walk commits parent-first, so a // child commit reuses the projections its parent just computed for the subtrees they share, which // an in-process map captures. -static PATHS_MAP: LazyLock>> = +static PATHS_MAP: LazyLock>> = LazyLock::new(Default::default); -static INVERT_MAP: LazyLock>> = +static INVERT_MAP: LazyLock>> = LazyLock::new(Default::default); /// Placeholder hint for tree-keyed records with no commit context. Sequence 0 is @@ -239,23 +240,25 @@ impl TransactionContext { #[allow(unused)] struct Transaction2 { - commit_map: HashMap>, - apply_map: HashMap>, - subtract_map: HashMap<(git2::Oid, git2::Oid), git2::Oid>, - intersect_map: HashMap<(git2::Oid, git2::Oid), git2::Oid>, - overlay_map: HashMap<(git2::Oid, git2::Oid), git2::Oid>, - unapply_map: HashMap>, - legalize_map: HashMap<(crate::filter::Filter, git2::Oid), crate::filter::Filter>, - downstack_deps_map: HashMap>, - merge_trees_map: HashMap<(git2::Oid, git2::Oid, git2::Oid), git2::Oid>, - last_written_commit: Option<(git2::Oid, git2::Oid)>, + commit_map: HashMap>, + apply_map: HashMap>, + subtract_map: HashMap<(gix_hash::ObjectId, gix_hash::ObjectId), gix_hash::ObjectId>, + intersect_map: HashMap<(gix_hash::ObjectId, gix_hash::ObjectId), gix_hash::ObjectId>, + overlay_map: HashMap<(gix_hash::ObjectId, gix_hash::ObjectId), gix_hash::ObjectId>, + unapply_map: HashMap>, + legalize_map: HashMap<(crate::filter::Filter, gix_hash::ObjectId), crate::filter::Filter>, + downstack_deps_map: + HashMap>, + merge_trees_map: + HashMap<(gix_hash::ObjectId, gix_hash::ObjectId, gix_hash::ObjectId), gix_hash::ObjectId>, + last_written_commit: Option<(gix_hash::ObjectId, gix_hash::ObjectId)>, tree_cache: TreeCache, cache: std::sync::Arc, // In-transaction memoization of the trigram index (source tree -> index tree); the // cache backend behind it holds the durable, cross-transaction copy. - index_map: HashMap, - missing: Vec<(usize, crate::filter::Filter, git2::Oid)>, + index_map: HashMap, + missing: Vec<(usize, crate::filter::Filter, gix_hash::ObjectId)>, misses: usize, nesting_level: usize, } @@ -446,12 +449,12 @@ impl Transaction { pub fn read_tree_bytes( &self, odb: &josh_memodb::Odb, - oid: git2::Oid, + oid: gix_hash::ObjectId, ) -> anyhow::Result> { if let Some(bytes) = self.t2.borrow().tree_cache.get(oid) { return Ok(Some(TreeBytes::Cached(bytes))); } - let (kind, bytes) = odb.read(crate::objects::gix_oid(oid))?; + let (kind, bytes) = odb.read(oid)?; if kind != gix_object::Kind::Tree { return Ok(None); } @@ -588,7 +591,7 @@ impl Transaction { /// partial-name DWIM. `Ok(None)` if the ref (or the end of a symbolic chain) does not /// exist. The target is not peeled: for an annotated tag ref this is the tag oid, /// peeling is an object-store concern. - pub fn resolve_ref(&self, refname: &str) -> anyhow::Result> { + pub fn resolve_ref(&self, refname: &str) -> anyhow::Result> { // Parsing as a full name is what rejects `master`: gix's find would resolve it. let name = full_ref_name(refname)?; let Some(reference) = self.find_ref(&name)? else { @@ -623,12 +626,12 @@ impl Transaction { fn follow_symrefs( &self, mut reference: gix::refs::Reference, - ) -> anyhow::Result> { + ) -> anyhow::Result> { // git's own limit on how far a symbolic ref may point. for _ in 0..5 { let next = match &reference.target { gix::refs::Target::Object(id) => { - let target = crate::objects::git2_oid(id); + let target = id.to_owned(); return Ok(Some((reference.name, target))); } gix::refs::Target::Symbolic(next) => self.find_ref(next)?, @@ -682,10 +685,10 @@ impl Transaction { /// like `master~2` -- to the object it names, unpeeled. `Ok(None)` when it resolves to /// nothing, which covers both a malformed spec and one naming something absent: a /// revision a user typed is input, not a contract. - pub fn rev_parse(&self, spec: &str) -> anyhow::Result> { + pub fn rev_parse(&self, spec: &str) -> anyhow::Result> { let repo = self.repo(); match repo.rev_parse_single(spec) { - Ok(id) => Ok(Some(crate::objects::git2_oid(&id))), + Ok(id) => Ok(Some(id.into())), Err(gix::revision::spec::parse::single::Error::RangedRev { .. }) => Ok(None), Err(gix::revision::spec::parse::single::Error::Parse(error)) => { let operational_error = error.sources().any(|source| { @@ -792,7 +795,7 @@ impl Transaction { &self, refname: &str, expected: Expected, - target: git2::Oid, + target: gix_hash::ObjectId, log_message: &str, ) -> anyhow::Result<()> { let name = full_ref_name(refname)?; @@ -807,7 +810,7 @@ impl Transaction { }; if guard_allows_unchanged && let Some(reference) = self.find_ref(&name)? - && reference.target == gix::refs::Target::Object(crate::objects::gix_oid(target)) + && reference.target == gix::refs::Target::Object(target) { return Ok(()); } @@ -821,7 +824,7 @@ impl Transaction { message: log_message.into(), }, expected: previous_value(expected), - new: gix::refs::Target::Object(crate::objects::gix_oid(target)), + new: gix::refs::Target::Object(target), }, name, deref: false, @@ -914,9 +917,7 @@ impl Transaction { let name = full_ref_name(refname)?; if let Expected::At(old) = expected { match self.find_ref(&name)? { - Some(reference) - if reference.target - == gix::refs::Target::Object(crate::objects::gix_oid(old)) => {} + Some(reference) if reference.target == gix::refs::Target::Object(old) => {} _ => { return Err(anyhow!( "ref '{}' does not point at the expected value {old}", @@ -973,7 +974,7 @@ impl Transaction { pub fn for_each_ref_prefixed( &self, prefix: &str, - mut cb: impl FnMut(&str, git2::Oid) -> anyhow::Result<()>, + mut cb: impl FnMut(&str, gix_hash::ObjectId) -> anyhow::Result<()>, ) -> anyhow::Result<()> { // Glob metacharacters (*?[\) are all invalid in refnames, so a caller passing one // means to match, not to prefix. @@ -1008,7 +1009,7 @@ impl Transaction { if !name.starts_with(prefix) { continue; } - refs.insert(name.to_owned(), crate::objects::git2_oid(target)); + refs.insert(name.to_owned(), target.to_owned()); } for edit in self.pending_refs.borrow().iter() { let Ok(name) = std::str::from_utf8(edit.name.as_bstr()) else { @@ -1022,7 +1023,7 @@ impl Transaction { new: gix::refs::Target::Object(target), .. } => { - refs.insert(name.to_owned(), crate::objects::git2_oid(target)); + refs.insert(name.to_owned(), target.to_owned()); } gix::refs::transaction::Change::Update { new: gix::refs::Target::Symbolic(_), @@ -1053,7 +1054,12 @@ impl Transaction { prev } - pub fn insert_apply(&self, filter: crate::filter::Filter, from: git2::Oid, to: git2::Oid) { + pub fn insert_apply( + &self, + filter: crate::filter::Filter, + from: gix_hash::ObjectId, + to: gix_hash::ObjectId, + ) { let mut t2 = self.t2.borrow_mut(); t2.apply_map .entry(filter.id()) @@ -1061,7 +1067,11 @@ impl Transaction { .insert(from, to); } - pub fn get_apply(&self, filter: crate::filter::Filter, from: git2::Oid) -> Option { + pub fn get_apply( + &self, + filter: crate::filter::Filter, + from: gix_hash::ObjectId, + ) -> Option { let t2 = self.t2.borrow_mut(); if let Some(m) = t2.apply_map.get(&filter.id()) { return m.get(&from).cloned(); @@ -1071,7 +1081,7 @@ impl Transaction { pub(crate) fn insert_downstack_deps( &self, - oid: git2::Oid, + oid: gix_hash::ObjectId, deps: std::collections::HashSet, ) { let mut t2 = self.t2.borrow_mut(); @@ -1080,7 +1090,7 @@ impl Transaction { pub(crate) fn get_downstack_deps( &self, - oid: git2::Oid, + oid: gix_hash::ObjectId, ) -> Option> { let t2 = self.t2.borrow_mut(); t2.downstack_deps_map.get(&oid).cloned() @@ -1088,8 +1098,8 @@ impl Transaction { pub(crate) fn insert_merge_trees( &self, - key: (git2::Oid, git2::Oid, git2::Oid), - result: git2::Oid, + key: (gix_hash::ObjectId, gix_hash::ObjectId, gix_hash::ObjectId), + result: gix_hash::ObjectId, ) { let mut t2 = self.t2.borrow_mut(); t2.merge_trees_map.insert(key, result); @@ -1097,38 +1107,59 @@ impl Transaction { pub(crate) fn get_merge_trees( &self, - key: (git2::Oid, git2::Oid, git2::Oid), - ) -> Option { + key: (gix_hash::ObjectId, gix_hash::ObjectId, gix_hash::ObjectId), + ) -> Option { let t2 = self.t2.borrow_mut(); t2.merge_trees_map.get(&key).copied() } - pub fn insert_subtract(&self, from: (git2::Oid, git2::Oid), to: git2::Oid) { + pub fn insert_subtract( + &self, + from: (gix_hash::ObjectId, gix_hash::ObjectId), + to: gix_hash::ObjectId, + ) { let mut t2 = self.t2.borrow_mut(); t2.subtract_map.insert(from, to); } - pub fn get_subtract(&self, from: (git2::Oid, git2::Oid)) -> Option { + pub fn get_subtract( + &self, + from: (gix_hash::ObjectId, gix_hash::ObjectId), + ) -> Option { let t2 = self.t2.borrow_mut(); t2.subtract_map.get(&from).cloned() } - pub fn insert_intersect(&self, from: (git2::Oid, git2::Oid), to: git2::Oid) { + pub fn insert_intersect( + &self, + from: (gix_hash::ObjectId, gix_hash::ObjectId), + to: gix_hash::ObjectId, + ) { let mut t2 = self.t2.borrow_mut(); t2.intersect_map.insert(from, to); } - pub fn get_intersect(&self, from: (git2::Oid, git2::Oid)) -> Option { + pub fn get_intersect( + &self, + from: (gix_hash::ObjectId, gix_hash::ObjectId), + ) -> Option { let t2 = self.t2.borrow_mut(); t2.intersect_map.get(&from).cloned() } - pub fn insert_overlay(&self, from: (git2::Oid, git2::Oid), to: git2::Oid) { + pub fn insert_overlay( + &self, + from: (gix_hash::ObjectId, gix_hash::ObjectId), + to: gix_hash::ObjectId, + ) { let mut t2 = self.t2.borrow_mut(); t2.overlay_map.insert(from, to); } - pub fn get_overlay(&self, from: (git2::Oid, git2::Oid)) -> Option { + pub fn get_overlay( + &self, + from: (gix_hash::ObjectId, gix_hash::ObjectId), + ) -> Option { let t2 = self.t2.borrow_mut(); t2.overlay_map.get(&from).cloned() } @@ -1137,17 +1168,17 @@ impl Transaction { /// walk processes a commit right after writing its parent, so this single slot answers the /// common "what tree does my filtered parent have" lookup without re-parsing the parent from the /// odb -- and without retaining every written commit the way a map would. - pub fn set_last_written_commit(&self, commit: git2::Oid, tree: git2::Oid) { + pub fn set_last_written_commit(&self, commit: gix_hash::ObjectId, tree: gix_hash::ObjectId) { self.t2.borrow_mut().last_written_commit = Some((commit, tree)); } - pub fn last_written_commit(&self) -> Option<(git2::Oid, git2::Oid)> { + pub fn last_written_commit(&self) -> Option<(gix_hash::ObjectId, gix_hash::ObjectId)> { self.t2.borrow().last_written_commit } pub fn insert_legalize( &self, - from: (crate::filter::Filter, git2::Oid), + from: (crate::filter::Filter, gix_hash::ObjectId), to: crate::filter::Filter, ) { let mut t2 = self.t2.borrow_mut(); @@ -1156,13 +1187,18 @@ impl Transaction { pub fn get_legalize( &self, - from: (crate::filter::Filter, git2::Oid), + from: (crate::filter::Filter, gix_hash::ObjectId), ) -> Option { let t2 = self.t2.borrow_mut(); t2.legalize_map.get(&from).cloned() } - pub fn insert_unapply(&self, filter: crate::filter::Filter, from: git2::Oid, to: git2::Oid) { + pub fn insert_unapply( + &self, + filter: crate::filter::Filter, + from: gix_hash::ObjectId, + to: gix_hash::ObjectId, + ) { let mut t2 = self.t2.borrow_mut(); t2.unapply_map .entry(filter.id()) @@ -1170,25 +1206,24 @@ impl Transaction { .insert(from, to); } - pub fn insert_paths(&self, tree: (git2::Oid, String), result: git2::Oid) { + pub fn insert_paths(&self, tree: (gix_hash::ObjectId, String), result: gix_hash::ObjectId) { PATHS_MAP.write().unwrap().entry(tree).or_insert(result); } - pub fn get_paths(&self, tree: (git2::Oid, String)) -> Option { + pub fn get_paths(&self, tree: (gix_hash::ObjectId, String)) -> Option { PATHS_MAP.read().unwrap().get(&tree).cloned() } - pub fn insert_invert(&self, tree: (git2::Oid, String), result: git2::Oid) { + pub fn insert_invert(&self, tree: (gix_hash::ObjectId, String), result: gix_hash::ObjectId) { INVERT_MAP.write().unwrap().entry(tree).or_insert(result); } - pub fn get_invert(&self, tree: (git2::Oid, String)) -> Option { + pub fn get_invert(&self, tree: (gix_hash::ObjectId, String)) -> Option { INVERT_MAP.read().unwrap().get(&tree).cloned() } - /// Build a josh-search index cache that shards records by `commit`'s history-graph - /// position. Pass [`git2::Oid::ZERO_SHA1`] for a bare tree with no commit context. - pub fn trigram_index_cache(&self, commit: git2::Oid) -> TrigramIndexCache<'_> { + /// Cache indexes under the commit's history shard. A null ID means no commit context. + pub fn trigram_index_cache(&self, commit: gix_hash::ObjectId) -> TrigramIndexCache<'_> { TrigramIndexCache { transaction: self, hint: self.tree_keyed_hint(commit), @@ -1198,14 +1233,19 @@ impl Transaction { /// Trees carry no history position of their own, so tree-keyed records use the hint /// of the commit being indexed. Falls back to the placeholder when there is no commit /// context or the hint cannot be computed. - fn tree_keyed_hint(&self, commit: git2::Oid) -> HistoryGraphHint { - if commit == git2::Oid::ZERO_SHA1 { + fn tree_keyed_hint(&self, commit: gix_hash::ObjectId) -> HistoryGraphHint { + if commit == gix_hash::ObjectId::null(gix_hash::Kind::Sha1) { return TREE_KEYED_FALLBACK_HINT; } compute_history_hint(self, commit).unwrap_or(TREE_KEYED_FALLBACK_HINT) } - fn insert_trigram_index(&self, tree: git2::Oid, result: git2::Oid, hint: HistoryGraphHint) { + fn insert_trigram_index( + &self, + tree: gix_hash::ObjectId, + result: gix_hash::ObjectId, + hint: HistoryGraphHint, + ) { let filter = crate::filter::index(); let mut t2 = self.t2.borrow_mut(); t2.index_map.entry(tree).or_insert(result); @@ -1214,7 +1254,11 @@ impl Transaction { } } - fn get_trigram_index(&self, tree: git2::Oid, hint: HistoryGraphHint) -> Option { + fn get_trigram_index( + &self, + tree: gix_hash::ObjectId, + hint: HistoryGraphHint, + ) -> Option { let filter = crate::filter::index(); let t2 = self.t2.borrow_mut(); if let Some(oid) = t2.index_map.get(&tree).cloned() { @@ -1224,30 +1268,49 @@ impl Transaction { let oid = t2.cache.read_propagate(filter, tree, hint, true).ok()??; // Per-subtree index trees are anchored by no ref, so gc may have pruned a // cached one; treat a dangling hit as a miss and reindex. - if self.odb().contains(crate::objects::gix_oid(oid)) { + if self.odb().contains(oid) { Some(oid) } else { None } } - pub fn insert_populate(&self, tree: (git2::Oid, git2::Oid), result: git2::Oid) { + pub fn insert_populate( + &self, + tree: (gix_hash::ObjectId, gix_hash::ObjectId), + result: gix_hash::ObjectId, + ) { POPULATE_MAP.write().unwrap().entry(tree).or_insert(result); } - pub fn get_populate(&self, tree: (git2::Oid, git2::Oid)) -> Option { + pub fn get_populate( + &self, + tree: (gix_hash::ObjectId, gix_hash::ObjectId), + ) -> Option { POPULATE_MAP.read().unwrap().get(&tree).cloned() } - pub fn insert_glob(&self, tree: (git2::Oid, git2::Oid, u64), result: git2::Oid) { + pub fn insert_glob( + &self, + tree: (gix_hash::ObjectId, gix_hash::ObjectId, u64), + result: gix_hash::ObjectId, + ) { GLOB_MAP.write().unwrap().entry(tree).or_insert(result); } - pub fn get_glob(&self, tree: (git2::Oid, git2::Oid, u64)) -> Option { + pub fn get_glob( + &self, + tree: (gix_hash::ObjectId, gix_hash::ObjectId, u64), + ) -> Option { GLOB_MAP.read().unwrap().get(&tree).cloned() } - pub fn insert_ref(&self, filter: crate::filter::Filter, from: git2::Oid, to: git2::Oid) { + pub fn insert_ref( + &self, + filter: crate::filter::Filter, + from: gix_hash::ObjectId, + to: gix_hash::ObjectId, + ) { REF_CACHE .write() .unwrap() @@ -1256,17 +1319,25 @@ impl Transaction { .insert(from, to); } - pub fn get_ref(&self, filter: crate::filter::Filter, from: git2::Oid) -> Option { + pub fn get_ref( + &self, + filter: crate::filter::Filter, + from: gix_hash::ObjectId, + ) -> Option { if let Some(m) = REF_CACHE.read().unwrap().get(&filter.id()) && let Some(oid) = m.get(&from) - && self.odb().contains(crate::objects::gix_oid(*oid)) + && self.odb().contains(*oid) { return Some(*oid); } None } - pub fn get_unapply(&self, filter: crate::filter::Filter, from: git2::Oid) -> Option { + pub fn get_unapply( + &self, + filter: crate::filter::Filter, + from: gix_hash::ObjectId, + ) -> Option { let t2 = self.t2.borrow_mut(); if let Some(m) = t2.unapply_map.get(&filter.id()) { return m.get(&from).cloned(); @@ -1277,7 +1348,7 @@ impl Transaction { pub fn lookup_filter_hook( &self, hook: &str, - from: git2::Oid, + from: gix_hash::ObjectId, ) -> anyhow::Result { if let Some(h) = &self.filter_hook { return h.filter_for_commit(from, hook); @@ -1293,8 +1364,8 @@ impl Transaction { pub fn insert( &self, filter: crate::filter::Filter, - from: git2::Oid, - to: git2::Oid, + from: gix_hash::ObjectId, + to: gix_hash::ObjectId, store: bool, ) -> anyhow::Result<()> { let hint = if filter != crate::filter::sequence_number() @@ -1326,7 +1397,9 @@ impl Transaction { Ok(()) } - pub fn get_missing(&self) -> anyhow::Result> { + pub fn get_missing( + &self, + ) -> anyhow::Result> { let missing = self.t2.borrow().missing.clone(); let mut retained = Vec::with_capacity(missing.len()); for (level, f, i) in missing { @@ -1341,15 +1414,19 @@ impl Transaction { Ok(retained) } - pub fn known(&self, filter: crate::filter::Filter, from: git2::Oid) -> anyhow::Result { + pub fn known( + &self, + filter: crate::filter::Filter, + from: gix_hash::ObjectId, + ) -> anyhow::Result { Ok(self.get2(filter, from)?.is_some()) } pub fn get( &self, filter: crate::filter::Filter, - from: git2::Oid, - ) -> anyhow::Result> { + from: gix_hash::ObjectId, + ) -> anyhow::Result> { if let Some(x) = self.get2(filter, from)? { Ok(Some(x)) } else { @@ -1364,8 +1441,8 @@ impl Transaction { fn get2( &self, filter: crate::filter::Filter, - from: git2::Oid, - ) -> anyhow::Result> { + from: gix_hash::ObjectId, + ) -> anyhow::Result> { if filter.is_nop() { return Ok(Some(from)); } @@ -1391,14 +1468,14 @@ impl Transaction { let oid = t2.cache.read_propagate(filter, from, hint, false)?; if let Some(oid) = oid { - if oid == git2::Oid::ZERO_SHA1 { + if oid == gix_hash::ObjectId::null(gix_hash::Kind::Sha1) { return Ok(Some(oid)); } if filter == crate::filter::sequence_number() { return Ok(Some(oid)); } - if self.odb().contains(crate::objects::gix_oid(oid)) { + if self.odb().contains(oid) { // Only report an object as cached if it exists in the object database. // This forces a rebuild in case the object was garbage collected. return Ok(Some(oid)); @@ -1427,11 +1504,11 @@ pub struct TrigramIndexCache<'a> { } impl josh_search::IndexCache for TrigramIndexCache<'_> { - fn get_index(&self, tree: git2::Oid) -> Option { + fn get_index(&self, tree: gix_hash::ObjectId) -> Option { self.transaction.get_trigram_index(tree, self.hint) } - fn set_index(&self, tree: git2::Oid, index: git2::Oid) { + fn set_index(&self, tree: gix_hash::ObjectId, index: gix_hash::ObjectId) { self.transaction .insert_trigram_index(tree, index, self.hint) } @@ -1454,13 +1531,13 @@ mod tests { (dir, transaction) } - fn commit(transaction: &Transaction, msg: &str) -> git2::Oid { + fn commit(transaction: &Transaction, msg: &str) -> gix_hash::ObjectId { let repo = transaction.git2_repo(); let tree = repo .find_tree(repo.treebuilder(None).unwrap().write().unwrap()) .unwrap(); let sig = git2::Signature::new("t", "t@example.com", &git2::Time::new(0, 0)).unwrap(); - repo.commit(None, &sig, &sig, msg, &tree, &[]).unwrap() + crate::objects::gix_oid(repo.commit(None, &sig, &sig, msg, &tree, &[]).unwrap()) } #[test] @@ -1550,7 +1627,7 @@ mod tests { .find_reference("refs/heads/main") .unwrap() .target(), - Some(a) + Some(crate::objects::git2_oid(&a)) ); } @@ -1857,10 +1934,11 @@ mod tests { .find_tree(repo.treebuilder(None).unwrap().write().unwrap()) .unwrap(); let sig = git2::Signature::new("t", "t@example.com", &git2::Time::new(0, 0)).unwrap(); - let parent_commit = repo.find_commit(parent).unwrap(); - let tip = repo - .commit(None, &sig, &sig, "tip", &tree, &[&parent_commit]) - .unwrap(); + let parent_commit = repo.find_commit(crate::objects::git2_oid(&parent)).unwrap(); + let tip = crate::objects::gix_oid( + repo.commit(None, &sig, &sig, "tip", &tree, &[&parent_commit]) + .unwrap(), + ); transaction .update_ref("refs/heads/main", Expected::Any, tip, "test") .unwrap(); @@ -1940,7 +2018,11 @@ mod tests { let log = std::fs::read_to_string(dir.path().join(".git/logs/refs/heads/main")).unwrap(); let lines: Vec<&str> = log.lines().collect(); assert_eq!(lines.len(), 2, "{}", log); - assert!(lines[0].starts_with(&format!("{} {}", git2::Oid::ZERO_SHA1, a))); + assert!(lines[0].starts_with(&format!( + "{} {}", + gix_hash::ObjectId::null(gix_hash::Kind::Sha1), + a + ))); assert!(lines[0].ends_with("\tfirst")); assert!(lines[1].starts_with(&format!("{} {}", a, b))); assert!(lines[1].ends_with("\tsecond")); @@ -2193,11 +2275,9 @@ mod tests { let oid = { let transaction = context.open().unwrap(); - let oid = crate::objects::git2_oid( - &transaction - .odb() - .write(gix_object::Kind::Blob, b"published"), - ); + let oid = transaction + .odb() + .write(gix_object::Kind::Blob, b"published"); transaction .update_ref("refs/josh/blob", Expected::Absent, oid, "test") .unwrap(); @@ -2209,16 +2289,22 @@ mod tests { ); let on_disk = git2::Repository::open(dir.path()).unwrap(); assert!(on_disk.find_reference("refs/josh/blob").is_err()); - assert!(on_disk.find_blob(oid).is_err()); + assert!(on_disk.find_blob(crate::objects::git2_oid(&oid)).is_err()); oid }; let on_disk = git2::Repository::open(dir.path()).unwrap(); assert_eq!( on_disk.find_reference("refs/josh/blob").unwrap().target(), - Some(oid) + Some(crate::objects::git2_oid(&oid)) + ); + assert_eq!( + on_disk + .find_blob(crate::objects::git2_oid(&oid)) + .unwrap() + .content(), + b"published" ); - assert_eq!(on_disk.find_blob(oid).unwrap().content(), b"published"); } /// An explicit disk-reader boundary has the same ordering as drop: object first, ref second. @@ -2226,8 +2312,7 @@ mod tests { fn flush_mem_odb_publishes_pending_refs_after_objects() { let (dir, context) = test_context(); let transaction = context.open().unwrap(); - let oid = - crate::objects::git2_oid(&transaction.odb().write(gix_object::Kind::Blob, b"boundary")); + let oid = transaction.odb().write(gix_object::Kind::Blob, b"boundary"); transaction .update_ref("refs/josh/blob", Expected::Absent, oid, "test") .unwrap(); @@ -2237,9 +2322,15 @@ mod tests { let on_disk = git2::Repository::open(dir.path()).unwrap(); assert_eq!( on_disk.find_reference("refs/josh/blob").unwrap().target(), - Some(oid) + Some(crate::objects::git2_oid(&oid)) + ); + assert_eq!( + on_disk + .find_blob(crate::objects::git2_oid(&oid)) + .unwrap() + .content(), + b"boundary" ); - assert_eq!(on_disk.find_blob(oid).unwrap().content(), b"boundary"); } #[test] diff --git a/josh-core/src/cache/tree_cache.rs b/josh-core/src/cache/tree_cache.rs index 5f7c6399d..d8d35d720 100644 --- a/josh-core/src/cache/tree_cache.rs +++ b/josh-core/src/cache/tree_cache.rs @@ -18,29 +18,14 @@ impl std::ops::Deref for TreeBytes { } } -/// Cache key routing the oid digest into [`PassthroughHasher`]'s single-`write` path: -/// `git2::Oid`'s own `Hash` impl hashes its bytes as a slice, whose `write_usize` length -/// prefix the hasher rejects. -#[derive(PartialEq, Eq)] -struct OidKey(git2::Oid); +type OidMap = + std::collections::HashMap>; +type OidSet = std::collections::HashSet>; -impl std::hash::Hash for OidKey { - fn hash(&self, state: &mut H) { - state.write(self.0.as_bytes()); - } -} - -type OidMap = std::collections::HashMap>; -type OidSet = std::collections::HashSet>; - -/// Per-transaction cache of raw tree bytes, standing in for the parsed-object cache libgit2 -/// kept behind `find_tree` (which the gix-object tree ops no longer go through). See -/// [`super::Transaction::read_tree_bytes`] for the read path. +/// Raw tree bytes cached after their second read. /// -/// Policy: a tree is only copied into the cache on its second read -/// ([`should_promote`](Self::should_promote)), so once-read trees cost nothing extra; trees are -/// content-addressed so cached entries never go stale; the cache is dropped wholesale when it -/// outgrows [`LIMIT`](Self::LIMIT). +/// Entries never go stale because trees are content-addressed. The cache resets when it +/// exceeds [`LIMIT`](Self::LIMIT). #[derive(Default)] pub(crate) struct TreeCache { map: OidMap>, @@ -51,23 +36,22 @@ pub(crate) struct TreeCache { impl TreeCache { const LIMIT: usize = 64 * 1024 * 1024; - pub(crate) fn get(&self, oid: git2::Oid) -> Option> { - self.map.get(&OidKey(oid)).cloned() + pub(crate) fn get(&self, oid: gix_hash::ObjectId) -> Option> { + self.map.get(&oid).cloned() } - /// Whether `oid` is being read for the second time and should be copied into the cache - /// now; the first read is only recorded, so it can hand out the odb's buffer as it is. - pub(crate) fn should_promote(&mut self, oid: git2::Oid) -> bool { - !self.seen.insert(OidKey(oid)) + /// Return true from the second read onward. + pub(crate) fn should_promote(&mut self, oid: gix_hash::ObjectId) -> bool { + !self.seen.insert(oid) } - pub(crate) fn insert(&mut self, oid: git2::Oid, bytes: std::sync::Arc<[u8]>) { + pub(crate) fn insert(&mut self, oid: gix_hash::ObjectId, bytes: std::sync::Arc<[u8]>) { if self.bytes > Self::LIMIT { self.map.clear(); self.bytes = 0; } self.bytes += bytes.len(); - self.map.insert(OidKey(oid), bytes); + self.map.insert(oid, bytes); } } @@ -75,12 +59,10 @@ impl TreeCache { mod tests { use super::*; - fn oid(n: u8) -> git2::Oid { - git2::Oid::from_bytes(&[n; 20]).unwrap() + fn oid(n: u8) -> gix_hash::ObjectId { + gix_hash::ObjectId::from_bytes_or_panic(&[n; 20]) } - // The first read is only recorded; the second read promotes, and only then does the cache - // hold the bytes. #[test] fn promotes_on_second_read_only() { let mut cache = TreeCache::default(); @@ -91,8 +73,6 @@ mod tests { assert_eq!(&*cache.get(oid(1)).unwrap(), b"tree bytes"); } - // Growing past the budget drops the whole cache before the next insert, which itself - // stays cached. #[test] fn clears_wholesale_over_limit() { let mut cache = TreeCache::default(); diff --git a/josh-core/src/filter/mod.rs b/josh-core/src/filter/mod.rs index 7f6b3bb20..db69410db 100644 --- a/josh-core/src/filter/mod.rs +++ b/josh-core/src/filter/mod.rs @@ -5,6 +5,7 @@ use josh_filter::check_experimental_features_enabled; pub use josh_filter::experimental_features_enabled; use std::path::Path; +use std::str::FromStr; use std::sync::LazyLock; // Re-export from josh-filter @@ -23,21 +24,30 @@ pub use josh_filter::{as_file, pretty, spec}; pub mod text; pub mod tree; -pub fn as_tree(transaction: &cache::Transaction, filter: Filter) -> anyhow::Result { - Ok(objects::git2_oid(&josh_filter::persist::as_tree( - transaction.odb(), - filter, - )?)) +pub fn as_tree( + transaction: &cache::Transaction, + filter: Filter, +) -> anyhow::Result { + josh_filter::persist::as_tree(transaction.odb(), filter) } -pub fn from_tree(transaction: &cache::Transaction, tree_oid: git2::Oid) -> anyhow::Result { - josh_filter::persist::from_tree(transaction.odb(), objects::gix_oid(tree_oid)) +pub fn from_tree( + transaction: &cache::Transaction, + tree_oid: gix_hash::ObjectId, +) -> anyhow::Result { + josh_filter::persist::from_tree(transaction.odb(), tree_oid) } -static WORKSPACES: LazyLock>> = - LazyLock::new(Default::default); +static WORKSPACES: LazyLock< + std::sync::Mutex>, +> = LazyLock::new(Default::default); static ANCESTORS: LazyLock< - std::sync::Mutex>>, + std::sync::Mutex< + std::collections::HashMap< + gix_hash::ObjectId, + std::collections::HashSet, + >, + >, > = LazyLock::new(Default::default); /// Clear the process-global workspace and ancestor caches. @@ -71,26 +81,26 @@ pub enum SigRewrite { /// when (and only when) they need its contents. #[derive(Debug, Clone)] pub struct Rewrite { - tree: git2::Oid, - commit: git2::Oid, + tree: gix_hash::ObjectId, + commit: gix_hash::ObjectId, pub author: Option, pub committer: Option, pub message: Option, } impl Rewrite { - pub fn from_tree(tree: git2::Oid) -> Self { + pub fn from_tree(tree: gix_hash::ObjectId) -> Self { Rewrite { tree, author: None, - commit: git2::Oid::ZERO_SHA1, + commit: gix_hash::ObjectId::null(gix_hash::Kind::Sha1), committer: None, message: None, } } pub fn from_tree_with_metadata( - tree: git2::Oid, + tree: gix_hash::ObjectId, author: Option, committer: Option, message: Option, @@ -98,7 +108,7 @@ impl Rewrite { Rewrite { tree, author, - commit: git2::Oid::ZERO_SHA1, + commit: gix_hash::ObjectId::null(gix_hash::Kind::Sha1), committer, message, } @@ -140,11 +150,11 @@ impl Rewrite { } } - pub fn with_tree(self, tree: git2::Oid) -> Self { + pub fn with_tree(self, tree: gix_hash::ObjectId) -> Self { Rewrite { tree, ..self } } - pub fn tree_id(&self) -> git2::Oid { + pub fn tree_id(&self) -> gix_hash::ObjectId { self.tree } } @@ -212,11 +222,14 @@ fn lazy_refs2(op: &Op) -> Vec { lr } -pub fn resolve_refs(refs: &std::collections::HashMap, filter: Filter) -> Filter { +pub fn resolve_refs( + refs: &std::collections::HashMap, + filter: Filter, +) -> Filter { to_filter(resolve_refs2(refs, &to_op(filter))) } -fn resolve_refs2(refs: &std::collections::HashMap, op: &Op) -> Op { +fn resolve_refs2(refs: &std::collections::HashMap, op: &Op) -> Op { match op { Op::Compose(filters) => { Op::Compose(filters.iter().map(|f| resolve_refs(refs, *f)).collect()) @@ -233,7 +246,7 @@ fn resolve_refs2(refs: &std::collections::HashMap, op: &Op) - let f = resolve_refs(refs, *f); let resolved_r = if let LazyRef::Lazy(s) = r { if let Some(res) = refs.get(s) { - LazyRef::Resolved(objects::gix_oid(*res)) + LazyRef::Resolved(*res) } else { r.clone() } @@ -251,7 +264,7 @@ fn resolve_refs2(refs: &std::collections::HashMap, op: &Op) - .map(|(r, m)| { if let LazyRef::Lazy(s) = r { if let Some(res) = refs.get(s) { - (LazyRef::Resolved(objects::gix_oid(*res)), *m) + (LazyRef::Resolved(*res), *m) } else { (r.clone(), *m) } @@ -264,7 +277,7 @@ fn resolve_refs2(refs: &std::collections::HashMap, op: &Op) - } Op::Downstack(LazyRef::Lazy(s)) => { if let Some(res) = refs.get(s) { - Op::Downstack(LazyRef::Resolved(objects::gix_oid(*res))) + Op::Downstack(LazyRef::Resolved(*res)) } else { op.clone() } @@ -359,9 +372,9 @@ fn propagate_meta(filter: Filter, meta: &std::collections::BTreeMap anyhow::Result { +) -> anyhow::Result { let filter = opt::optimize(filter); loop { let filtered = apply_to_commit2(filter, commit, transaction)?; @@ -416,7 +429,7 @@ fn resolve_workspace_redirect( fn get_workspace( transaction: &cache::Transaction, odb: &josh_memodb::Odb, - tree: git2::Oid, + tree: gix_hash::ObjectId, reader: &tree::TreeReader, path: &Path, ) -> Filter { @@ -435,7 +448,7 @@ fn get_workspace( fn get_stored( transaction: &cache::Transaction, odb: &josh_memodb::Odb, - tree: git2::Oid, + tree: gix_hash::ObjectId, reader: &tree::TreeReader, path: &Path, ) -> Filter { @@ -450,7 +463,7 @@ fn get_stored( fn get_starlark( transaction: &cache::Transaction, odb: &josh_memodb::Odb, - tree: git2::Oid, + tree: gix_hash::ObjectId, reader: &tree::TreeReader, path: &Path, subfilter: Filter, @@ -476,7 +489,7 @@ fn get_starlark( fn get_filter( transaction: &cache::Transaction, odb: &josh_memodb::Odb, - tree: git2::Oid, + tree: gix_hash::ObjectId, reader: &tree::TreeReader, path: &Path, ) -> Filter { @@ -484,7 +497,7 @@ fn get_filter( // One descent resolves both the WORKSPACES cache key (the workspace.josh entry oid) // and the blob to parse. let ws_id = match tree::get_path_entry_at(transaction, odb, reader, &ws_path) { - Ok(Some(entry)) => objects::git2_oid(&entry.oid), + Ok(Some(entry)) => entry.oid, _ => { return to_filter(Op::Empty); } @@ -521,7 +534,7 @@ fn read_josh_link( let link_entry = tree::get_path_entry_at(transaction, odb, reader, &link_path) .ok() .flatten()?; - let link_blob = tree::blob_bytes(odb, objects::git2_oid(&link_entry.oid))?; + let link_blob = tree::blob_bytes(odb, link_entry.oid)?; let b = std::str::from_utf8(&link_blob) .with_context(|| format!("invalid utf8 in {}", filename)) .ok()?; @@ -541,19 +554,17 @@ fn read_josh_link( fn get_rev_filter( transaction: &cache::Transaction, - commit_id: git2::Oid, + commit_id: gix_hash::ObjectId, filters: &[(RevMatch, LazyRef, Filter)], ) -> anyhow::Result { // First match wins - iterate in order for (match_op, filter_tip_ref, startfilter) in filters.iter() { let filter_tip = if let LazyRef::Resolved(filter_tip) = filter_tip_ref { - objects::git2_oid(filter_tip.as_ref()) + filter_tip.to_owned() } else { return Err(anyhow!("unresolved lazy ref")); }; - if match_op != &RevMatch::Default - && !transaction.odb().contains(objects::gix_oid(filter_tip)) - { + if match_op != &RevMatch::Default && !transaction.odb().contains(filter_tip) { return Err(anyhow!("`:rev(...)` with nonexistent OID: {}", filter_tip)); } let matches = match match_op { @@ -589,9 +600,9 @@ fn get_rev_filter( pub fn apply_to_commit2( filter: Filter, - commit_id: git2::Oid, + commit_id: gix_hash::ObjectId, transaction: &cache::Transaction, -) -> anyhow::Result> { +) -> anyhow::Result> { let op = peel_op(filter); if filter == Filter::new() { @@ -601,12 +612,12 @@ pub fn apply_to_commit2( // First match: oid-only fast paths, no object loads. The commit bytes are only read // after the memo gate below, so memo hits stay zero-I/O. match &op { - Op::Empty => return Ok(Some(git2::Oid::ZERO_SHA1)), + Op::Empty => return Ok(Some(gix_hash::ObjectId::null(gix_hash::Kind::Sha1))), Op::Chain(_) => { let mut current_oid = commit_id; for f in flatten_chain(filter) { - if current_oid == git2::Oid::ZERO_SHA1 { + if current_oid == gix_hash::ObjectId::null(gix_hash::Kind::Sha1) { break; } let r = some_or!(apply_to_commit2(f, current_oid, transaction)?, { @@ -619,7 +630,7 @@ pub fn apply_to_commit2( Op::Squash(None) => { let odb = transaction.odb(); let commit = objects::CommitData::read(odb, commit_id)?; - odb.read_header(objects::gix_oid(commit.tree_id()?))?; + odb.read_header(commit.tree_id()?)?; return Some(history::rewrite_commit( odb, &commit, @@ -633,7 +644,7 @@ pub fn apply_to_commit2( if let Some(oid) = transaction.get(filter, commit_id)? { return Ok(Some(oid)); } - let new_oid = downstack(transaction, commit_id, objects::git2_oid(base.as_ref()))?; + let new_oid = downstack(transaction, commit_id, base.to_owned())?; transaction.insert(filter, commit_id, new_oid, false)?; return Ok(Some(new_oid)); } @@ -654,11 +665,11 @@ pub fn apply_to_commit2( // no parse). Without this gate, an apply over a partially unreadable input can buffer // fresh objects into the store before a later read aborts the walk, and the partial // write would change the next flush's pack. - odb.read_header(objects::gix_oid(commit.tree_id()?))?; + odb.read_header(commit.tree_id()?)?; let rewrite_data = match &op { Op::Squash(Some(ids)) => { - if let Some(sq) = ids.get(&LazyRef::Resolved(objects::gix_oid(commit.id()))) { + if let Some(sq) = ids.get(&LazyRef::Resolved(commit.id())) { let oid = if let Some(oid) = apply_to_commit2( filter::Filter::new().squash(None).chain(*sq), commit_id, @@ -732,7 +743,7 @@ pub fn apply_to_commit2( .collect::>>()? }; - let mut filtered_parent_ids: Vec = + let mut filtered_parent_ids: Vec = some_or!(filtered_parent_ids, { return Ok(None) }); // TODO: remove all parents that don't have a .link.josh @@ -763,7 +774,7 @@ pub fn apply_to_commit2( ".link.josh", ) { if let Some(commit_str) = link_file.get_meta("commit") { - if let Ok(commit_oid) = git2::Oid::from_str(&commit_str) { + if let Ok(commit_oid) = gix_hash::ObjectId::from_str(&commit_str) { if filtered_parent_ids.contains(&commit_oid) { while filtered_parent_ids[0] != commit_oid { filtered_parent_ids.rotate_right(1); @@ -793,13 +804,13 @@ pub fn apply_to_commit2( .collect::>>()? }; - let mut filtered_parent_ids: Vec = + let mut filtered_parent_ids: Vec = some_or!(filtered_parent_ids, { return Ok(None) }); let mut link_parents = vec![]; for (link_path, link_file) in find_link_files(odb, commit.tree_id()?)?.into_iter() { if let Some(commit_str) = link_file.get_meta("commit") { - if let Ok(commit_oid) = git2::Oid::from_str(&commit_str) { + if let Ok(commit_oid) = gix_hash::ObjectId::from_str(&commit_str) { if let Some(cmt) = transaction.get(to_filter(Op::Prefix(link_path)), commit_oid)? { @@ -875,7 +886,7 @@ pub fn apply_to_commit2( let normal_parents = commit .parent_ids() .map(|parent| transaction.get(filter, parent)) - .collect::>>>()?; + .collect::>>>()?; let normal_parents = some_or!(normal_parents, { return Ok(None) }); @@ -1010,7 +1021,7 @@ pub fn apply_to_commit2( let filtered_parent_ids = some_or!(filtered_parent_ids, { return Ok(None) }); - let trees: Vec = filtered_parent_ids + let trees: Vec = filtered_parent_ids .iter() .map(|x| history::filtered_parent_tree_id(transaction, *x)) .collect::>()?; @@ -1040,7 +1051,7 @@ pub fn apply_to_commit2( check_experimental_features_enabled("unapply filter")?; if let LazyRef::Resolved(target) = target { /* dbg!(target); */ - let target = objects::CommitData::read(odb, objects::git2_oid(target.as_ref()))?; + let target = objects::CommitData::read(odb, target.to_owned())?; // Only a root commit (no first parent) skips link detection; a // first parent that is present must be readable. if let Some(parent_id) = target.first_parent_id() { @@ -1058,12 +1069,10 @@ pub fn apply_to_commit2( ".link.josh", ) { if let Some(commit_str) = link.get_meta("commit") { - if let Ok(link_commit) = git2::Oid::from_str(&commit_str) { + if let Ok(link_commit) = gix_hash::ObjectId::from_str(&commit_str) { if commit.id() == link_commit { - let unapply = to_filter(Op::Unapply( - LazyRef::Resolved(objects::gix_oid(parent.id())), - *uf, - )); + let unapply = + to_filter(Op::Unapply(LazyRef::Resolved(parent.id()), *uf)); let r = some_or!(transaction.get(unapply, link_commit)?, { return Ok(None); }); @@ -1093,12 +1102,9 @@ pub fn apply_to_commit2( let tree_reader = tree::read_tree(transaction, odb, tree)?; if let Some(link) = read_josh_link(transaction, odb, &tree_reader, path, ".link.josh") { let subdir = filter::invert(link.peel())?; - let unapply = to_filter(Op::Unapply( - LazyRef::Resolved(objects::gix_oid(commit.id())), - subdir, - )); + let unapply = to_filter(Op::Unapply(LazyRef::Resolved(commit.id()), subdir)); if let Some(commit_str) = link.get_meta("commit") { - if let Ok(commit_oid) = git2::Oid::from_str(&commit_str) { + if let Ok(commit_oid) = gix_hash::ObjectId::from_str(&commit_str) { let r = some_or!(transaction.get(unapply, commit_oid)?, { return Ok(None); }); @@ -1107,7 +1113,7 @@ pub fn apply_to_commit2( } } } - return Ok(Some(git2::Oid::ZERO_SHA1)); + return Ok(Some(gix_hash::ObjectId::null(gix_hash::Kind::Sha1))); } _ => apply(transaction, filter, Rewrite::from_commit_data(&commit)?)?, @@ -1137,11 +1143,11 @@ pub fn apply_to_commit2( fn extract_submodule_commits( transaction: &cache::Transaction, odb: &josh_memodb::Odb, - tree: git2::Oid, + tree: gix_hash::ObjectId, ) -> anyhow::Result< std::collections::BTreeMap< std::path::PathBuf, - (git2::Oid, crate::submodules::ParsedSubmoduleEntry), + (gix_hash::ObjectId, crate::submodules::ParsedSubmoduleEntry), >, > { use crate::submodules::{ParsedSubmoduleEntry, parse_gitmodules}; @@ -1172,7 +1178,7 @@ fn extract_submodule_commits( let mut submodule_commits: std::collections::BTreeMap< std::path::PathBuf, - (git2::Oid, ParsedSubmoduleEntry), + (gix_hash::ObjectId, ParsedSubmoduleEntry), > = std::collections::BTreeMap::new(); for parsed in submodule_entries { @@ -1181,7 +1187,7 @@ fn extract_submodule_commits( tree::get_path_entry_at(transaction, odb, &tree_reader, &submodule_path) { if entry.mode.is_commit() { - let commit_oid = objects::git2_oid(&entry.oid); + let commit_oid = entry.oid; submodule_commits.insert(submodule_path, (commit_oid, parsed)); } } @@ -1193,7 +1199,7 @@ fn extract_submodule_commits( fn get_link_roots( transaction: &cache::Transaction, odb: &josh_memodb::Odb, - tree: git2::Oid, + tree: gix_hash::ObjectId, ) -> anyhow::Result> { let link_filter = to_filter(Op::pattern("**/.link.josh")?); let link_tree = apply_impl(transaction, odb, link_filter, Rewrite::from_tree(tree))?; @@ -1261,7 +1267,7 @@ fn apply_impl( Op::Message(m, r) => { // Rewriting a message leaves the tree alone, so with neither a commit nor a // message to transform this is identity, like the other history-only filters. - if x.commit == git2::Oid::ZERO_SHA1 && x.message.is_none() { + if x.commit == gix_hash::ObjectId::null(gix_hash::Kind::Sha1) && x.message.is_none() { return Ok(x); } @@ -1319,8 +1325,8 @@ fn apply_impl( .ok() .flatten() }) - .map(|e| objects::git2_oid(&e.oid)) - .unwrap_or(git2::Oid::ZERO_SHA1) + .map(|e| e.oid) + .unwrap_or(gix_hash::ObjectId::null(gix_hash::Kind::Sha1)) .to_string(), ), _ => None, @@ -1354,9 +1360,7 @@ fn apply_impl( odb, result_tree, &submodule_path.join(".link.josh"), - objects::git2_oid( - &odb.write(gix_object::Kind::Blob, link_content.as_bytes()), - ), + odb.write(gix_object::Kind::Blob, link_content.as_bytes()), 0o0100644, )?; } @@ -1366,7 +1370,7 @@ fn apply_impl( odb, result_tree, std::path::Path::new(".gitmodules"), - git2::Oid::ZERO_SHA1, + gix_hash::ObjectId::null(gix_hash::Kind::Sha1), 0o0100644, )?; } @@ -1381,7 +1385,7 @@ fn apply_impl( odb, tree, std::path::Path::new(".link.josh"), - git2::Oid::ZERO_SHA1, + gix_hash::ObjectId::null(gix_hash::Kind::Sha1), 0o0100644, )?)) } @@ -1390,8 +1394,13 @@ fn apply_impl( use crate::link::find_link_files; let mut result_tree = x.tree; for (link_path, link_file) in find_link_files(odb, result_tree)?.iter() { - result_tree = - tree::insert_oid(odb, result_tree, link_path, git2::Oid::ZERO_SHA1, 0o0100644)?; + result_tree = tree::insert_oid( + odb, + result_tree, + link_path, + gix_hash::ObjectId::null(gix_hash::Kind::Sha1), + 0o0100644, + )?; // The link_file is already a filter with metadata, just serialize it let link_content = as_file(*link_file, 0); @@ -1400,7 +1409,7 @@ fn apply_impl( odb, result_tree, &link_path.join(".link.josh"), - objects::git2_oid(&odb.write(gix_object::Kind::Blob, link_content.as_bytes())), + odb.write(gix_object::Kind::Blob, link_content.as_bytes()), 0o0100644, )?; } @@ -1419,7 +1428,7 @@ fn apply_impl( // Get commit from metadata let commit_oid = link_file .get_meta("commit") - .and_then(|s| git2::Oid::from_str(&s).ok()) + .and_then(|s| gix_hash::ObjectId::from_str(&s).ok()) .ok_or_else(|| anyhow!("Link file missing commit metadata"))?; let submodule_tree = git::read_tree_id(odb, commit_oid)?; @@ -1446,7 +1455,7 @@ fn apply_impl( odb, result_tree, &root.join(".link.josh"), - objects::git2_oid(&odb.write(gix_object::Kind::Blob, link_content.as_bytes())), + odb.write(gix_object::Kind::Blob, link_content.as_bytes()), 0o0100644, )?; } @@ -1464,7 +1473,7 @@ fn apply_impl( Op::Pattern(cp) => { let input = x.tree_id(); - let key = objects::git2_oid(peel_filter(filter).id().as_ref()); + let key = peel_filter(filter).id(); let t = if cp.fallback { // More components than the NFA state mask can hold: match full paths. tree::remove_pred( @@ -1490,23 +1499,19 @@ fn apply_impl( Op::Insert(dest_path, content) => { let (oid, mode, is_tree) = match content { InsertContent::Inline(s) => ( - objects::git2_oid(&odb.write(gix_object::Kind::Blob, s.as_bytes())), + odb.write(gix_object::Kind::Blob, s.as_bytes()), git2::FileMode::Blob.into(), false, ), // The kind comes from the header alone; a missing oid folds into the // "neither" arm below. InsertContent::Oid(oid) => match odb.try_kind(*oid) { - Ok(Some(gix_object::Kind::Blob)) => ( - objects::git2_oid(oid.as_ref()), - git2::FileMode::Blob.into(), - false, - ), - Ok(Some(gix_object::Kind::Tree)) => ( - objects::git2_oid(oid.as_ref()), - git2::FileMode::Tree.into(), - true, - ), + Ok(Some(gix_object::Kind::Blob)) => { + (oid.to_owned(), git2::FileMode::Blob.into(), false) + } + Ok(Some(gix_object::Kind::Tree)) => { + (oid.to_owned(), git2::FileMode::Tree.into(), true) + } _ => { return Err(anyhow::anyhow!( "insert: {} is neither a blob nor a tree", @@ -1539,8 +1544,11 @@ fn apply_impl( // entry; lookup failures fold into the fallback. let (file, mode) = match tree::get_path_entry(transaction, odb, x.tree_id(), source_path) { - Ok(Some(e)) => (objects::git2_oid(&e.oid), e.mode.value() as i32), - _ => (git2::Oid::ZERO_SHA1, git2::FileMode::Blob.into()), + Ok(Some(e)) => (e.oid, e.mode.value() as i32), + _ => ( + gix_hash::ObjectId::null(gix_hash::Kind::Sha1), + git2::FileMode::Blob.into(), + ), }; Ok(x.with_tree(tree::insert_oid( odb, @@ -1555,7 +1563,7 @@ fn apply_impl( // A missing path, a non-tree entry (e.g. a blob), or a failed lookup has no // subtree. let subtree = match tree::get_path_entry(transaction, odb, x.tree_id(), path) { - Ok(Some(entry)) if entry.mode.is_tree() => objects::git2_oid(&entry.oid), + Ok(Some(entry)) if entry.mode.is_tree() => entry.oid, _ => tree::empty_id(), }; Ok(x.with_tree(subtree)) @@ -1641,9 +1649,8 @@ fn apply_impl( } Op::ObjectRef(path) => { if let Ok(Some(entry)) = tree::get_path_entry(transaction, odb, x.tree_id(), path) { - let oid_str = objects::git2_oid(&entry.oid).to_string(); - let blob_oid = - objects::git2_oid(&odb.write(gix_object::Kind::Blob, oid_str.as_bytes())); + let oid_str = entry.oid.to_string(); + let blob_oid = odb.write(gix_object::Kind::Blob, oid_str.as_bytes()); Ok(x.with_tree(tree::insert_oid( odb, tree::empty_id(), @@ -1662,7 +1669,7 @@ fn apply_impl( _ => return Ok(x), }; // Path exists: read OID string from blob content. - let oid_str = if let Some(blob) = tree::blob_bytes(odb, objects::git2_oid(&entry.oid)) { + let oid_str = if let Some(blob) = tree::blob_bytes(odb, entry.oid) { std::str::from_utf8(&blob)? .lines() .next() @@ -1672,10 +1679,10 @@ fn apply_impl( } else { String::new() }; - if let Ok(oid) = git2::Oid::from_str(&oid_str) { + if let Ok(oid) = gix_hash::ObjectId::from_str(&oid_str) { // Kind by header, never by `contains`: `read_header`'s disk fallback // virtualizes the empty tree, `exists` does not. - let (oid, mode) = match odb.try_kind(objects::gix_oid(oid)) { + let (oid, mode) = match odb.try_kind(oid) { Ok(Some(gix_object::Kind::Tree)) => (oid, git2::FileMode::Tree.into()), Ok(Some(gix_object::Kind::Blob)) => (oid, git2::FileMode::Blob.into()), _ => { @@ -1685,7 +1692,7 @@ fn apply_impl( Ok(x.with_tree(tree::insert_oid(odb, tree::empty_id(), path, oid, mode)?)) } else { // Content is not a valid OID: insert empty blob at path. - let empty_blob = objects::git2_oid(&odb.write(gix_object::Kind::Blob, b"")); + let empty_blob = odb.write(gix_object::Kind::Blob, b""); Ok(x.with_tree(tree::insert_oid( odb, tree::empty_id(), @@ -1721,10 +1728,10 @@ fn apply_impl( Op::Unapply(target, uf) => { check_experimental_features_enabled("unapply filter")?; if let LazyRef::Resolved(target) = target { - let target = objects::CommitData::read(odb, objects::git2_oid(target.as_ref()))?; + let target = objects::CommitData::read(odb, target.to_owned())?; // The message must parse as an oid, so non-UTF-8 is an error. let target_msg = target.message()?; - let target = git2::Oid::from_str(std::str::from_utf8(target_msg)?)?; + let target = gix_hash::ObjectId::from_str(std::str::from_utf8(target_msg)?)?; let target_tree = git::read_tree_id(odb, target)?; /* dbg!(&uf); */ Ok(Rewrite::from_tree(filter::unapply( @@ -1754,10 +1761,10 @@ fn apply_impl( pub fn unapply( transaction: &cache::Transaction, filter: Filter, - tree: git2::Oid, - parent_tree: git2::Oid, - commits: Option<(git2::Oid, git2::Oid)>, -) -> anyhow::Result { + tree: gix_hash::ObjectId, + parent_tree: gix_hash::ObjectId, + commits: Option<(gix_hash::ObjectId, gix_hash::ObjectId)>, +) -> anyhow::Result { // A `:rev(...)` filter has no static inverse (`invert` returns `Err`, like `:workspace` / // `:stored` / `:starlark`), so this generic path is automatically skipped for it and it falls // through to the per-commit handler / chain recursion below. @@ -1845,9 +1852,9 @@ fn reverse_strip_overlay( transaction: &cache::Transaction, filter: Filter, original_filter: Filter, - tree: git2::Oid, - parent_tree: git2::Oid, -) -> anyhow::Result { + tree: gix_hash::ObjectId, + parent_tree: gix_hash::ObjectId, +) -> anyhow::Result { let filtered = apply( transaction, original_filter, @@ -1867,7 +1874,7 @@ fn reverse_strip_overlay( fn resolve_commit_filter( transaction: &cache::Transaction, op: &Op, - commit: git2::Oid, + commit: gix_hash::ObjectId, ) -> anyhow::Result { match op { Op::Rev(revs) => get_rev_filter(transaction, commit, revs), @@ -1885,10 +1892,10 @@ fn is_commit_resolved_filter(op: &Op) -> bool { fn unapply_per_rev_filter( transaction: &cache::Transaction, op: &Op, - tree: git2::Oid, - parent_tree: git2::Oid, - commits: Option<(git2::Oid, git2::Oid)>, -) -> anyhow::Result> { + tree: gix_hash::ObjectId, + parent_tree: gix_hash::ObjectId, + commits: Option<(gix_hash::ObjectId, gix_hash::ObjectId)>, +) -> anyhow::Result> { if is_commit_resolved_filter(op) { // `:rev`/`:hook` select their sub-filter from commit identity (mirroring the forward // `Op::Rev`/`Op::Hook` paths): resolve it for the commit being reconstructed and for the @@ -1993,8 +2000,8 @@ fn unapply_per_rev_filter( fn pre_process_tree( transaction: &cache::Transaction, - tree: git2::Oid, -) -> anyhow::Result { + tree: gix_hash::ObjectId, +) -> anyhow::Result { let odb = transaction.odb(); let path = Path::new("workspace.josh"); let ws_file = tree::get_blob(transaction, odb, tree, path); @@ -2016,7 +2023,7 @@ fn pre_process_tree( odb, tree, path, - objects::git2_oid(&odb.write(gix_object::Kind::Blob, blob.as_bytes())), + odb.write(gix_object::Kind::Blob, blob.as_bytes()), git2::FileMode::Blob.into(), // Should this handle filemode? )?; @@ -2027,7 +2034,7 @@ fn pre_process_tree( pub fn compute_warnings( transaction: &cache::Transaction, filter: Filter, - tree: git2::Oid, + tree: gix_hash::ObjectId, ) -> Vec { let mut warnings = Vec::new(); let mut filter = filter; @@ -2074,7 +2081,7 @@ pub fn compute_warnings( fn compute_warnings2( transaction: &cache::Transaction, filter: Filter, - tree: git2::Oid, + tree: gix_hash::ObjectId, ) -> Vec { let mut warnings = Vec::new(); @@ -2092,8 +2099,8 @@ fn compute_warnings2( /// Creates a cache for a given `tip` so repeated queries with the same `tip` are more efficient. pub fn is_ancestor_of( transaction: &cache::Transaction, - commit: git2::Oid, - tip: git2::Oid, + commit: gix_hash::ObjectId, + tip: gix_hash::ObjectId, ) -> anyhow::Result { if let Ok(tip_sequence_number) = cache::compute_sequence_number(transaction, tip) { if cache::compute_sequence_number(transaction, commit)? > tip_sequence_number { @@ -2160,7 +2167,7 @@ fn legalize_stored( t: &cache::Transaction, odb: &josh_memodb::Odb, f: Filter, - tree: git2::Oid, + tree: gix_hash::ObjectId, reader: &tree::TreeReader, ) -> anyhow::Result { if !needs_legalization(f) { @@ -2245,9 +2252,9 @@ fn legalize_stored( fn compute_splice_parents( transaction: &cache::Transaction, commit_filter: Filter, - parent_filters: Vec<(git2::Oid, Filter)>, + parent_filters: Vec<(gix_hash::ObjectId, Filter)>, meta: &std::collections::BTreeMap, -) -> anyhow::Result>> { +) -> anyhow::Result>> { // This is only reached when history splicing is enabled, and `per_rev_filter` rejects `:pin` // in that case, so the filters here are pin-free -- no pin handling is needed. let splice_parents = parent_filters @@ -2271,7 +2278,7 @@ fn compute_splice_parents( Ok(Some( splice_parents .into_iter() - .filter(|&oid| oid != git2::Oid::ZERO_SHA1) + .filter(|&oid| oid != gix_hash::ObjectId::null(gix_hash::Kind::Sha1)) .collect(), )) } @@ -2281,8 +2288,8 @@ fn per_rev_filter( commit: &objects::CommitData, filter: Filter, commit_filter: Filter, - parent_filters: Vec<(git2::Oid, Filter)>, -) -> anyhow::Result> { + parent_filters: Vec<(gix_hash::ObjectId, Filter)>, +) -> anyhow::Result> { // Propagate any meta-options from the outer filter (e.g. :~(gpgsig="norm-lf")[:rev(...)]) // into the per-commit filter so they are applied during commit rewriting. let meta = filter.into_meta(); @@ -2317,7 +2324,7 @@ fn per_rev_filter( let normal_parents = commit .parent_ids() .map(|parent| transaction.get(filter, parent)) - .collect::>>>()?; + .collect::>>>()?; let normal_parents = some_or!(normal_parents, { return Ok(None) }); // Special case: `:pin` filter needs to be aware of filtered history @@ -2358,7 +2365,7 @@ fn per_rev_filter( let mut target = None; for id in filtered_parent_ids .iter() - .filter(|x| **x != git2::Oid::ZERO_SHA1) + .filter(|x| **x != gix_hash::ObjectId::null(gix_hash::Kind::Sha1)) { let seq = cache::compute_sequence_number(transaction, *id)?; if target.map(|(top, _)| seq > top).unwrap_or(true) { @@ -2370,10 +2377,9 @@ fn per_rev_filter( // two preserved lineages join only inside the squashed region and one // of them would silently become unreachable. Fail instead. if let Some((_, target_id)) = target { - for id in filtered_parent_ids - .iter() - .filter(|x| **x != git2::Oid::ZERO_SHA1 && **x != target_id) - { + for id in filtered_parent_ids.iter().filter(|x| { + **x != gix_hash::ObjectId::null(gix_hash::Kind::Sha1) && **x != target_id + }) { if !objects::is_descendant_of(transaction.odb(), target_id, *id)? { return Err(anyhow!( "cannot squash {}: its filtered parents {} and {} do not descend \ @@ -2422,9 +2428,9 @@ fn per_rev_filter( /// onto the minimised base via a 3-way merge. Returns the new tip OID. pub fn downstack( transaction: &cache::Transaction, - change_oid: git2::Oid, - base_oid: git2::Oid, -) -> anyhow::Result { + change_oid: gix_hash::ObjectId, + base_oid: gix_hash::ObjectId, +) -> anyhow::Result { if !objects::is_descendant_of(transaction.odb(), change_oid, base_oid)? { return Err(anyhow!( "change {} is not a descendant of base {}", @@ -2553,10 +2559,10 @@ pub fn downstack( /// unrelated operations. fn cached_merge_trees( transaction: &cache::Transaction, - a: git2::Oid, - b: git2::Oid, - c: git2::Oid, -) -> anyhow::Result { + a: gix_hash::ObjectId, + b: gix_hash::ObjectId, + c: gix_hash::ObjectId, +) -> anyhow::Result { // Conventional 3-way identities. When the ancestor matches one side, the // merge result is the other side -- common in a clean linear stack where // the rebuilt parent's tree already equals the original intermediate's @@ -2612,7 +2618,7 @@ fn downstack_commit_deps( .first_parent_id() .map(|p| git::read_tree_id(odb, p)) .transpose()? - .unwrap_or(git2::Oid::ZERO_SHA1); + .unwrap_or(gix_hash::ObjectId::null(gix_hash::Kind::Sha1)); let commit_tree_id = commit.tree_id()?; let mut deps = std::collections::HashSet::new(); @@ -2828,9 +2834,10 @@ mod tests { b.upsert(p, oid, git2::FileMode::Blob); } let empty = repo.treebuilder(None).unwrap().write().unwrap(); - let tree = b - .create_updated(&repo, &repo.find_tree(empty).unwrap()) - .unwrap(); + let tree = objects::gix_oid( + b.create_updated(&repo, &repo.find_tree(empty).unwrap()) + .unwrap(), + ); let cachestack = std::sync::Arc::new( cache::CacheStack::new().with_backend(cache::SledCacheBackend::new(td.path())), @@ -2838,7 +2845,7 @@ mod tests { let ctx = cache::TransactionContext::new(td.path(), cachestack); let t = ctx.open().unwrap(); - let apply_tree = |f: Filter| -> anyhow::Result { + let apply_tree = |f: Filter| -> anyhow::Result { Ok(apply(&t, f, Rewrite::from_tree(tree))?.tree_id()) }; @@ -2947,27 +2954,34 @@ mod tests { let td = tempfile::tempdir().unwrap(); let repo = git2::Repository::init_bare(td.path()).unwrap(); - let mk_tree = |files: &[(&str, &str)]| -> git2::Oid { + let mk_tree = |files: &[(&str, &str)]| -> gix_hash::ObjectId { let mut b = git2::build::TreeUpdateBuilder::new(); for (p, c) in files { let oid = repo.blob(c.as_bytes()).unwrap(); b.upsert(*p, oid, git2::FileMode::Blob); } let empty = repo.treebuilder(None).unwrap().write().unwrap(); - b.create_updated(&repo, &repo.find_tree(empty).unwrap()) - .unwrap() + objects::gix_oid( + b.create_updated(&repo, &repo.find_tree(empty).unwrap()) + .unwrap(), + ) }; let sig = git2::Signature::new("t", "t@e", &git2::Time::new(0, 0)).unwrap(); - let mk_commit = |tree: git2::Oid, parents: &[git2::Oid], msg: &str| -> git2::Oid { - let tree = repo.find_tree(tree).unwrap(); + let mk_commit = |tree: gix_hash::ObjectId, + parents: &[gix_hash::ObjectId], + msg: &str| + -> gix_hash::ObjectId { + let tree = repo.find_tree(objects::git2_oid(&tree)).unwrap(); let parent_commits: Vec = parents .iter() - .map(|p| repo.find_commit(*p).unwrap()) + .map(|p| repo.find_commit(objects::git2_oid(p)).unwrap()) .collect(); let parent_refs: Vec<&git2::Commit> = parent_commits.iter().collect(); - repo.commit(None, &sig, &sig, msg, &tree, &parent_refs) - .unwrap() + objects::gix_oid( + repo.commit(None, &sig, &sig, msg, &tree, &parent_refs) + .unwrap(), + ) }; // base <- side, and a merge of the two is the change being published. @@ -2986,24 +3000,24 @@ mod tests { let t = ctx.open().unwrap(); let out = repo - .find_commit(downstack(&t, merge, base).unwrap()) + .find_commit(objects::git2_oid(&downstack(&t, merge, base).unwrap())) .unwrap(); assert_eq!(out.parent_count(), 2, "merge change lost its second parent"); assert_eq!( out.parent_id(0).unwrap(), - base, + objects::git2_oid(&base), "first parent should be the minimal base" ); assert_eq!( out.parent_id(1).unwrap(), - side, + objects::git2_oid(&side), "second parent should be carried through" ); // A single-parent change is unaffected by the parent-preserving rewrite. let linear = mk_commit(mk_tree(&[("a", "1"), ("n", "n")]), &[base], "linear change"); let out_linear = repo - .find_commit(downstack(&t, linear, base).unwrap()) + .find_commit(objects::git2_oid(&downstack(&t, linear, base).unwrap())) .unwrap(); assert_eq!(out_linear.parent_count(), 1); } diff --git a/josh-core/src/filter/tree.rs b/josh-core/src/filter/tree.rs index 46fe310a5..61ce8e4ca 100644 --- a/josh-core/src/filter/tree.rs +++ b/josh-core/src/filter/tree.rs @@ -4,9 +4,9 @@ use anyhow::anyhow; pub fn pathstree( root: &str, - input: git2::Oid, + input: gix_hash::ObjectId, transaction: &cache::Transaction, -) -> anyhow::Result { +) -> anyhow::Result { let odb = transaction.odb(); pathstree_inner(root, input, transaction, odb) } @@ -14,10 +14,10 @@ pub fn pathstree( /// Oid-level body of [`pathstree`]; the odb is hoisted like in [`remove_pred_inner`]. fn pathstree_inner( root: &str, - input: git2::Oid, + input: gix_hash::ObjectId, transaction: &cache::Transaction, odb: &josh_memodb::Odb, -) -> anyhow::Result { +) -> anyhow::Result { if let Some(cached) = transaction.get_paths((input, root.to_string())) { return Ok(cached); } @@ -33,7 +33,7 @@ fn pathstree_inner( if entry.mode.is_tree() { let s = pathstree_inner( &format!("{}{}{}", root, if root.is_empty() { "" } else { "/" }, name), - objects::git2_oid(entry.oid), + entry.oid.to_owned(), transaction, odb, )?; @@ -42,7 +42,7 @@ fn pathstree_inner( rebuild.keep(gix_object::tree::Entry { mode: gix_object::tree::EntryKind::Tree.into(), filename: entry.filename.to_owned(), - oid: objects::gix_oid(s), + oid: s, }); } } else if !entry.mode.is_commit() { @@ -50,11 +50,7 @@ fn pathstree_inner( let path = normalize_path(&Path::new(root).join(name)); let path_string = path.to_str().ok_or_else(|| anyhow!("no name"))?; let file_contents = if name == "workspace.josh" { - format!( - "#{}\n{}", - path_string, - blob_text(odb, objects::git2_oid(entry.oid)) - ) + format!("#{}\n{}", path_string, blob_text(odb, entry.oid.to_owned())) } else { path_string.to_string() }; @@ -71,23 +67,23 @@ fn pathstree_inner( } pub fn regex_replace( - input: git2::Oid, + input: gix_hash::ObjectId, regex: ®ex::Regex, replacement: &str, transaction: &cache::Transaction, -) -> anyhow::Result { +) -> anyhow::Result { let odb = transaction.odb(); regex_replace_inner(input, regex, replacement, transaction, odb) } /// Oid-level body of [`regex_replace`]; the odb is hoisted like in [`remove_pred_inner`]. fn regex_replace_inner( - input: git2::Oid, + input: gix_hash::ObjectId, regex: ®ex::Regex, replacement: &str, transaction: &cache::Transaction, odb: &josh_memodb::Odb, -) -> anyhow::Result { +) -> anyhow::Result { let bytes = transaction .read_tree_bytes(odb, input)? .ok_or_else(|| anyhow!("regex_replace: {} is not a tree", input))?; @@ -98,23 +94,18 @@ fn regex_replace_inner( // Non-UTF-8 entry names stay an error even though the name is otherwise unused here. std::str::from_utf8(entry.filename).map_err(|_| anyhow!("no name"))?; if entry.mode.is_tree() { - let s = regex_replace_inner( - objects::git2_oid(entry.oid), - regex, - replacement, - transaction, - odb, - )?; + let s = + regex_replace_inner(entry.oid.to_owned(), regex, replacement, transaction, odb)?; if s != tree::empty_id() { rebuild.keep(gix_object::tree::Entry { mode: entry.mode, filename: entry.filename.to_owned(), - oid: objects::gix_oid(s), + oid: s, }); } } else if !entry.mode.is_commit() { - let file_contents = blob_text(odb, objects::git2_oid(entry.oid)); + let file_contents = blob_text(odb, entry.oid.to_owned()); let replaced = regex.replacen(&file_contents, 0, replacement); rebuild.keep(gix_object::tree::Entry { @@ -129,8 +120,8 @@ fn regex_replace_inner( /// The raw bytes of the blob `oid`, or `None` when the object is missing or not a blob -- /// `find_blob`'s tolerance, in facade currency. -pub fn blob_bytes(odb: &josh_memodb::Odb, oid: git2::Oid) -> Option { - match odb.read(objects::gix_oid(oid)) { +pub fn blob_bytes(odb: &josh_memodb::Odb, oid: gix_hash::ObjectId) -> Option { + match odb.read(oid) { Ok((gix_object::Kind::Blob, bytes)) => Some(bytes), _ => None, } @@ -140,7 +131,7 @@ pub fn blob_bytes(odb: &josh_memodb::Odb, oid: git2::Oid) -> Option String { +pub(crate) fn blob_text(odb: &josh_memodb::Odb, oid: gix_hash::ObjectId) -> String { let bytes = some_or!(blob_bytes(odb, oid), { return "".to_owned(); }); @@ -212,7 +203,11 @@ impl TreeRebuild { /// Write the rebuilt tree, or return `input` unchanged when nothing was dropped or /// rewritten: an untouched entry set reproduces `input` bit-identically, since git trees /// are content-addressed and the write preserves entry order. - fn finish(self, odb: &josh_memodb::Odb, input: git2::Oid) -> anyhow::Result { + fn finish( + self, + odb: &josh_memodb::Odb, + input: gix_hash::ObjectId, + ) -> anyhow::Result { if self.changed { objects::write_tree_now(odb, self.out) } else { @@ -288,7 +283,7 @@ impl TreeReader { pub fn read_tree( transaction: &cache::Transaction, odb: &josh_memodb::Odb, - oid: git2::Oid, + oid: gix_hash::ObjectId, ) -> anyhow::Result { let bytes = transaction .read_tree_bytes(odb, oid)? @@ -345,7 +340,7 @@ fn path_components(path: &Path) -> Option> { pub fn get_path_entry( transaction: &cache::Transaction, odb: &josh_memodb::Odb, - root: git2::Oid, + root: gix_hash::ObjectId, path: &Path, ) -> anyhow::Result> { let bytes = match transaction.read_tree_bytes(odb, root)? { @@ -374,7 +369,7 @@ pub fn get_path_entry_at( if !entry.mode.is_tree() { return Ok(None); } - let bytes = match transaction.read_tree_bytes(odb, objects::git2_oid(&entry.oid))? { + let bytes = match transaction.read_tree_bytes(odb, entry.oid)? { Some(bytes) => bytes, None => return Ok(None), }; @@ -416,10 +411,10 @@ fn insert_in_order(out: &mut Vec, entry: gix_object::tr pub fn remove_pred( transaction: &cache::Transaction, path: &mut String, - input: git2::Oid, + input: gix_hash::ObjectId, pred: &dyn Fn(&str, bool) -> bool, - key: git2::Oid, -) -> anyhow::Result { + key: gix_hash::ObjectId, +) -> anyhow::Result { let odb = transaction.odb(); remove_pred_inner(transaction, odb, path, input, pred, key) } @@ -430,14 +425,11 @@ fn remove_pred_inner( transaction: &cache::Transaction, odb: &josh_memodb::Odb, path: &mut String, - input: git2::Oid, + input: gix_hash::ObjectId, pred: &dyn Fn(&str, bool) -> bool, - key: git2::Oid, -) -> anyhow::Result { - let root_key = git2::Oid::hash_object( - git2::ObjectType::Blob, - format!("glob-fallback:{:?}:{}", key, path).as_bytes(), - )?; + key: gix_hash::ObjectId, +) -> anyhow::Result { + let root_key = objects::hash_blob(format!("glob-fallback:{:?}:{}", key, path).as_bytes()); if let Some(cached) = transaction.get_glob((input, root_key, 0)) { return Ok(cached); } @@ -458,22 +450,15 @@ fn remove_pred_inner( path.push_str(name); if entry.mode.is_tree() { - let s = remove_pred_inner( - transaction, - odb, - path, - objects::git2_oid(entry.oid), - pred, - key, - )?; - if s != objects::git2_oid(entry.oid) || s == empty { + let s = remove_pred_inner(transaction, odb, path, entry.oid.to_owned(), pred, key)?; + if s != entry.oid.to_owned() || s == empty { rebuild.mark_changed(); } if s != empty { rebuild.keep(gix_object::tree::Entry { mode: entry.mode, filename: entry.filename.to_owned(), - oid: objects::gix_oid(s), + oid: s, }); } } else if entry.mode.is_commit() { @@ -512,11 +497,11 @@ pub use josh_filter::pattern::{CompiledPattern, PATTERN_MATCH_OPTIONS, PatternCo /// a literal prefix) stay separate. pub fn remove_pattern( transaction: &cache::Transaction, - input: git2::Oid, + input: gix_hash::ObjectId, cp: &CompiledPattern, - key: git2::Oid, + key: gix_hash::ObjectId, state: u64, -) -> anyhow::Result { +) -> anyhow::Result { let odb = transaction.odb(); remove_pattern_inner(transaction, odb, input, cp, key, state) } @@ -525,11 +510,11 @@ pub fn remove_pattern( fn remove_pattern_inner( transaction: &cache::Transaction, odb: &josh_memodb::Odb, - input: git2::Oid, + input: gix_hash::ObjectId, cp: &CompiledPattern, - key: git2::Oid, + key: gix_hash::ObjectId, state: u64, -) -> anyhow::Result { +) -> anyhow::Result { let state = cp.closure(state); if let Some(cached) = transaction.get_glob((input, key, state)) { return Ok(cached); @@ -589,23 +574,16 @@ fn remove_pattern_inner( let s = if next == 0 { empty } else { - remove_pattern_inner( - transaction, - odb, - objects::git2_oid(entry.oid), - cp, - key, - next, - )? + remove_pattern_inner(transaction, odb, entry.oid.to_owned(), cp, key, next)? }; - if s != objects::git2_oid(entry.oid) || s == empty { + if s != entry.oid.to_owned() || s == empty { rebuild.mark_changed(); } if s != empty { rebuild.keep(gix_object::tree::Entry { mode: entry.mode, filename: entry.filename.to_owned(), - oid: objects::gix_oid(s), + oid: s, }); } } else if entry.mode.is_commit() { @@ -643,9 +621,9 @@ fn remove_pattern_inner( pub fn subtract( transaction: &cache::Transaction, - input1: git2::Oid, - input2: git2::Oid, -) -> anyhow::Result { + input1: gix_hash::ObjectId, + input2: gix_hash::ObjectId, +) -> anyhow::Result { let odb = transaction.odb(); subtract_inner(transaction, odb, input1, input2) } @@ -654,9 +632,9 @@ pub fn subtract( fn subtract_inner( transaction: &cache::Transaction, odb: &josh_memodb::Odb, - input1: git2::Oid, - input2: git2::Oid, -) -> anyhow::Result { + input1: gix_hash::ObjectId, + input2: gix_hash::ObjectId, +) -> anyhow::Result { if input1 == input2 { return Ok(empty_id()); } @@ -681,17 +659,13 @@ fn subtract_inner( // Modifications are collected by name first and applied in one pass: `None` removes // the entry, `Some` replaces its oid with the subtraction result (only ever produced // for a tree entry, whose raw mode is kept). - let mut mods: std::collections::HashMap<&[u8], Option> = + let mut mods: std::collections::HashMap<&[u8], Option> = std::collections::HashMap::new(); for entry in &tree2.entries { if let Some(e1) = lookup_entry(&tree1, entry.filename, sorted1) { - let sub = subtract_inner( - transaction, - odb, - objects::git2_oid(e1.oid), - objects::git2_oid(entry.oid), - )?; - if sub == empty_id() || sub == git2::Oid::ZERO_SHA1 { + let sub = + subtract_inner(transaction, odb, e1.oid.to_owned(), entry.oid.to_owned())?; + if sub == empty_id() || sub == gix_hash::ObjectId::null(gix_hash::Kind::Sha1) { mods.insert(&**entry.filename, None); } else { mods.insert(&**entry.filename, Some(sub)); @@ -703,7 +677,7 @@ fn subtract_inner( out.retain(|e| mods.get(e.filename.as_slice()) != Some(&None)); for entry in &mut out { if let Some(Some(sub)) = mods.get(entry.filename.as_slice()) { - entry.oid = objects::gix_oid(*sub); + entry.oid = *sub; } } let result = objects::write_tree_now(odb, out)?; @@ -727,9 +701,9 @@ fn subtract_inner( /// set of paths out of a large tree therefore costs O(input2) instead of O(input1). pub fn intersect( transaction: &cache::Transaction, - input1: git2::Oid, - input2: git2::Oid, -) -> anyhow::Result { + input1: gix_hash::ObjectId, + input2: gix_hash::ObjectId, +) -> anyhow::Result { let odb = transaction.odb(); intersect_inner(transaction, odb, input1, input2) } @@ -738,9 +712,9 @@ pub fn intersect( fn intersect_inner( transaction: &cache::Transaction, odb: &josh_memodb::Odb, - input1: git2::Oid, - input2: git2::Oid, -) -> anyhow::Result { + input1: gix_hash::ObjectId, + input2: gix_hash::ObjectId, +) -> anyhow::Result { // Identical (sub)trees intersect to themselves; an empty side leaves nothing to keep. if input1 == input2 { return Ok(input1); @@ -764,18 +738,14 @@ fn intersect_inner( let mut out = Vec::new(); for entry in &tree2.entries { if let Some(e1) = lookup_entry(&tree1, entry.filename, sorted1) { - let child = intersect( - transaction, - objects::git2_oid(e1.oid), - objects::git2_oid(entry.oid), - )?; - if child != empty_id() && child != git2::Oid::ZERO_SHA1 { + let child = intersect(transaction, e1.oid.to_owned(), entry.oid.to_owned())?; + if child != empty_id() && child != gix_hash::ObjectId::null(gix_hash::Kind::Sha1) { insert_in_order( &mut out, gix_object::tree::Entry { mode: e1.mode, filename: entry.filename.to_owned(), - oid: objects::gix_oid(child), + oid: child, }, ); } @@ -800,9 +770,9 @@ fn component_bytes(c: &std::ffi::OsStr) -> &[u8] { /// Read `oid` as raw tree bytes, or `None` if it is missing or not a tree. Uncached: the /// insert path can be called without a transaction to hang the tree cache off. -fn tree_bytes(src: &impl gix_object::Find, oid: git2::Oid) -> Option> { +fn tree_bytes(src: &impl gix_object::Find, oid: gix_hash::ObjectId) -> Option> { let mut buf = Vec::new(); - match src.try_find(&objects::gix_oid(oid), &mut buf) { + match src.try_find(&oid, &mut buf) { Ok(Some(data)) if data.kind == gix_object::Kind::Tree => Some(buf), _ => None, } @@ -816,10 +786,10 @@ fn tree_bytes(src: &impl gix_object::Find, oid: git2::Oid) -> Option> { fn replace_child_inner( odb: &(impl gix_object::Find + gix_object::Write), child: &[u8], - oid: git2::Oid, + oid: gix_hash::ObjectId, mode: i32, - tree_oid: git2::Oid, -) -> anyhow::Result { + tree_oid: gix_hash::ObjectId, +) -> anyhow::Result { let mut out = match tree_bytes(odb, tree_oid) { Some(bytes) => seed_entries(&gix_object::TreeRef::from_bytes( &bytes, @@ -827,7 +797,7 @@ fn replace_child_inner( )?), None => Vec::new(), }; - let remove = oid == git2::Oid::ZERO_SHA1 || oid == empty_id(); + let remove = oid == gix_hash::ObjectId::null(gix_hash::Kind::Sha1) || oid == empty_id(); let first = out.iter().position(|e| &*e.filename == child); out.retain(|e| &*e.filename != child); if !remove { @@ -835,7 +805,7 @@ fn replace_child_inner( mode: gix_object::tree::EntryMode::try_from(mode as u32) .map_err(|m| anyhow!("replace_child: invalid mode {:o}", m))?, filename: child.into(), - oid: objects::gix_oid(oid), + oid: oid, }; match first { Some(pos) => out.insert(pos, entry), @@ -850,11 +820,11 @@ fn replace_child_inner( /// overwritable. pub fn insert_oid( odb: &(impl gix_object::Find + gix_object::Write), - full_tree: git2::Oid, + full_tree: gix_hash::ObjectId, path: &Path, - oid: git2::Oid, + oid: gix_hash::ObjectId, mode: i32, -) -> anyhow::Result { +) -> anyhow::Result { let mut components = path.components(); let Some(first) = components.next() else { return Err(anyhow!("file_name")); @@ -881,7 +851,7 @@ pub fn insert_oid( let tree = gix_object::TreeRef::from_bytes(&bytes, gix_hash::Kind::Sha1)?; let sorted = entries_canonically_sorted(&tree); match lookup_entry(&tree, cb.into(), sorted) { - Some(e) => objects::git2_oid(e.oid), + Some(e) => e.oid.to_owned(), None => empty_id(), } } @@ -896,26 +866,23 @@ pub fn insert_oid( /// Kind of `oid`, or `None` when the object is missing (the zero oid included) or the /// header unreadable -- both read as an ordinary miss. -fn kind_of(src: &impl gix_object::FindHeader, oid: git2::Oid) -> Option { - src.try_header(&objects::gix_oid(oid)) - .ok() - .flatten() - .map(|h| h.kind) +fn kind_of(src: &impl gix_object::FindHeader, oid: gix_hash::ObjectId) -> Option { + src.try_header(&oid).ok().flatten().map(|h| h.kind) } /// Fill `buf` with the raw bytes of `oid` if it is a readable tree object. Parse failures /// fold to `None` at the caller, keeping the probe arms tolerant of corrupt trees. -fn read_tree_into(src: &impl gix_object::Find, oid: git2::Oid, buf: &mut Vec) -> bool { +fn read_tree_into(src: &impl gix_object::Find, oid: gix_hash::ObjectId, buf: &mut Vec) -> bool { matches!( - src.try_find(&objects::gix_oid(oid), buf), + src.try_find(&oid, buf), Ok(Some(data)) if data.kind == gix_object::Kind::Tree ) } pub fn diff_paths( src: &(impl gix_object::Find + gix_object::FindHeader), - input1: git2::Oid, - input2: git2::Oid, + input1: gix_hash::ObjectId, + input2: gix_hash::ObjectId, root: &str, ) -> anyhow::Result> { if input1 == input2 { @@ -955,15 +922,15 @@ pub fn diff_paths( if let Some(e) = tree1.entry(entry.filename) { r.append(&mut diff_paths( src, - objects::git2_oid(e.oid), - objects::git2_oid(entry.oid), + e.oid.to_owned(), + entry.oid.to_owned(), &format!("{}{}{}", root, if root.is_empty() { "" } else { "/" }, name), )?); } else { r.append(&mut diff_paths( src, - git2::Oid::ZERO_SHA1, - objects::git2_oid(entry.oid), + gix_hash::ObjectId::null(gix_hash::Kind::Sha1), + entry.oid.to_owned(), &format!("{}{}{}", root, if root.is_empty() { "" } else { "/" }, name), )?); } @@ -974,8 +941,8 @@ pub fn diff_paths( if tree2.entry(entry.filename).is_none() { r.append(&mut diff_paths( src, - objects::git2_oid(entry.oid), - git2::Oid::ZERO_SHA1, + entry.oid.to_owned(), + gix_hash::ObjectId::null(gix_hash::Kind::Sha1), &format!("{}{}{}", root, if root.is_empty() { "" } else { "/" }, name), )?); } @@ -989,8 +956,8 @@ pub fn diff_paths( let name = std::str::from_utf8(entry.filename).map_err(|_| anyhow!("no name"))?; r.append(&mut diff_paths( src, - git2::Oid::ZERO_SHA1, - objects::git2_oid(entry.oid), + gix_hash::ObjectId::null(gix_hash::Kind::Sha1), + entry.oid.to_owned(), &format!("{}{}{}", root, if root.is_empty() { "" } else { "/" }, name), )?); } @@ -1002,8 +969,8 @@ pub fn diff_paths( let name = std::str::from_utf8(entry.filename).map_err(|_| anyhow!("no name"))?; r.append(&mut diff_paths( src, - objects::git2_oid(entry.oid), - git2::Oid::ZERO_SHA1, + entry.oid.to_owned(), + gix_hash::ObjectId::null(gix_hash::Kind::Sha1), &format!("{}{}{}", root, if root.is_empty() { "" } else { "/" }, name), )?); } @@ -1015,9 +982,9 @@ pub fn diff_paths( pub fn overlay( transaction: &cache::Transaction, - input1: git2::Oid, - input2: git2::Oid, -) -> anyhow::Result { + input1: gix_hash::ObjectId, + input2: gix_hash::ObjectId, +) -> anyhow::Result { let odb = transaction.odb(); overlay_inner(transaction, odb, input1, input2) } @@ -1026,9 +993,9 @@ pub fn overlay( fn overlay_inner( transaction: &cache::Transaction, odb: &josh_memodb::Odb, - input1: git2::Oid, - input2: git2::Oid, -) -> anyhow::Result { + input1: gix_hash::ObjectId, + input2: gix_hash::ObjectId, +) -> anyhow::Result { if let Some(cached) = transaction.get_overlay((input1, input2)) { return Ok(cached); } @@ -1052,17 +1019,12 @@ fn overlay_inner( // name exists in `tree1` (with `tree1` winning on blob collisions, keeping its raw // mode), taken over as-is otherwise -- placed at its canonical position, raw mode // included. - let mut mods: std::collections::HashMap<&[u8], git2::Oid> = + let mut mods: std::collections::HashMap<&[u8], gix_hash::ObjectId> = std::collections::HashMap::new(); let mut new_entries: Vec = Vec::new(); for entry in &tree2.entries { if let Some(e1) = lookup_entry(&tree1, entry.filename, sorted1) { - let id = overlay_inner( - transaction, - odb, - objects::git2_oid(e1.oid), - objects::git2_oid(entry.oid), - )?; + let id = overlay_inner(transaction, odb, e1.oid.to_owned(), entry.oid.to_owned())?; mods.insert(&**entry.filename, id); } else { new_entries.push((*entry).into()); @@ -1072,7 +1034,7 @@ fn overlay_inner( let mut out = seed_entries(&tree1); for entry in &mut out { if let Some(id) = mods.get(entry.filename.as_slice()) { - entry.oid = objects::gix_oid(*id); + entry.oid = *id; } } for entry in new_entries { @@ -1103,8 +1065,8 @@ pub fn invert_paths( transaction: &cache::Transaction, odb: &josh_memodb::Odb, root: &str, - tree: git2::Oid, -) -> anyhow::Result { + tree: gix_hash::ObjectId, +) -> anyhow::Result { if let Some(cached) = transaction.get_invert((tree, root.to_string())) { return Ok(cached); } @@ -1127,7 +1089,7 @@ pub fn invert_paths( // entry) -- resolved on the hoisted parse. let b = parsed .entry(entry.filename) - .map(|e| blob_text(odb, objects::git2_oid(e.oid))) + .map(|e| blob_text(odb, e.oid.to_owned())) .unwrap_or_default(); let opath = pathline(&b)?; @@ -1135,7 +1097,7 @@ pub fn invert_paths( odb, result, Path::new(&opath), - objects::git2_oid(&odb.write(gix_object::Kind::Blob, mpath.as_bytes())), + odb.write(gix_object::Kind::Blob, mpath.as_bytes()), 0o0100644, ) .unwrap(); @@ -1146,7 +1108,7 @@ pub fn invert_paths( transaction, odb, &format!("{}{}{}", root, if root.is_empty() { "" } else { "/" }, name), - objects::git2_oid(entry.oid), + entry.oid.to_owned(), )?; result = overlay(transaction, result, s)?; } @@ -1160,7 +1122,7 @@ pub fn invert_paths( pub fn original_path( transaction: &cache::Transaction, filter: Filter, - tree: git2::Oid, + tree: gix_hash::ObjectId, path: &Path, ) -> anyhow::Result { let paths_tree = apply( @@ -1175,9 +1137,9 @@ pub fn original_path( pub fn repopulated_tree( transaction: &cache::Transaction, filter: Filter, - full_tree: git2::Oid, - partial_tree: git2::Oid, -) -> anyhow::Result { + full_tree: gix_hash::ObjectId, + partial_tree: gix_hash::ObjectId, +) -> anyhow::Result { let paths_tree = apply( transaction, to_filter(Op::Paths).chain(filter), @@ -1192,22 +1154,16 @@ pub fn repopulated_tree( pub fn populate( transaction: &cache::Transaction, odb: &josh_memodb::Odb, - paths: git2::Oid, - content: git2::Oid, -) -> anyhow::Result { + paths: gix_hash::ObjectId, + content: gix_hash::ObjectId, +) -> anyhow::Result { if let Some(cached) = transaction.get_populate((paths, content)) { return Ok(cached); } use gix_object::Kind; - let paths_kind = odb - .read_header(objects::gix_oid(paths)) - .map(|(kind, _)| kind) - .ok(); - let content_kind = odb - .read_header(objects::gix_oid(content)) - .map(|(kind, _)| kind) - .ok(); + let paths_kind = odb.read_header(paths).map(|(kind, _)| kind).ok(); + let content_kind = odb.read_header(content).map(|(kind, _)| kind).ok(); let mut result_tree = empty_id(); if let (Some(Kind::Blob), Some(Kind::Blob)) = (paths_kind, content_kind) { @@ -1229,12 +1185,7 @@ pub fn populate( result_tree = overlay( transaction, result_tree, - populate( - transaction, - odb, - objects::git2_oid(e.oid), - objects::git2_oid(entry.oid), - )?, + populate(transaction, odb, e.oid.to_owned(), entry.oid.to_owned())?, )?; } } @@ -1247,8 +1198,8 @@ pub fn populate( pub fn compose( transaction: &cache::Transaction, - trees: Vec<(&Filter, git2::Oid)>, -) -> anyhow::Result { + trees: Vec<(&Filter, gix_hash::ObjectId)>, +) -> anyhow::Result { let mut result = empty_id(); let mut taken = empty_id(); for (f, applied) in trees { @@ -1287,14 +1238,14 @@ pub fn compose( pub fn get_blob( transaction: &cache::Transaction, odb: &josh_memodb::Odb, - tree: git2::Oid, + tree: gix_hash::ObjectId, path: &Path, ) -> String { let entry = match get_path_entry(transaction, odb, tree, path) { Ok(Some(entry)) => entry, _ => return "".to_owned(), }; - blob_text(odb, objects::git2_oid(&entry.oid)) + blob_text(odb, entry.oid) } /// [`get_blob`] over a caller-held parse of the root tree (see [`get_path_entry_at`]). @@ -1308,26 +1259,28 @@ pub(crate) fn get_blob_at( Ok(Some(entry)) => entry, _ => return "".to_owned(), }; - blob_text(odb, objects::git2_oid(&entry.oid)) + blob_text(odb, entry.oid) } -pub fn empty_id() -> git2::Oid { - git2::Oid::from_str("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap() +pub fn empty_id() -> gix_hash::ObjectId { + gix_hash::ObjectId::empty_tree(gix_hash::Kind::Sha1) } #[cfg(test)] mod tests { use super::*; - fn make_tree(repo: &git2::Repository, paths: &[&str]) -> git2::Oid { + fn make_tree(repo: &git2::Repository, paths: &[&str]) -> gix_hash::ObjectId { let mut b = git2::build::TreeUpdateBuilder::new(); for p in paths { - let oid = repo.blob(p.as_bytes()).unwrap(); - b.upsert(*p, oid, git2::FileMode::Blob); + let oid = objects::gix_oid(repo.blob(p.as_bytes()).unwrap()); + b.upsert(*p, objects::git2_oid(&oid), git2::FileMode::Blob); } let base = repo.treebuilder(None).unwrap().write().unwrap(); - b.create_updated(repo, &repo.find_tree(base).unwrap()) - .unwrap() + objects::gix_oid( + b.create_updated(repo, &repo.find_tree(base).unwrap()) + .unwrap(), + ) } fn open_transaction(td: &tempfile::TempDir) -> cache::Transaction { @@ -1345,20 +1298,22 @@ mod tests { let td = tempfile::tempdir().unwrap(); let repo = git2::Repository::init_bare(td.path()).unwrap(); - let blob = repo.blob(b"content").unwrap(); - let link = repo.blob(b"target").unwrap(); + let blob = objects::gix_oid(repo.blob(b"content").unwrap()); + let link = objects::gix_oid(repo.blob(b"target").unwrap()); // Gitlinks reference commits in other repositories; git does not require the oid // to exist locally. - let sub = git2::Oid::from_str("0123456789012345678901234567890123456789").unwrap(); + let sub = gix_hash::ObjectId::from_str("0123456789012345678901234567890123456789").unwrap(); let mut b = repo.treebuilder(None).unwrap(); - b.insert("keep.rs", blob, 0o100644).unwrap(); - b.insert("link.rs", link, 0o120000).unwrap(); - b.insert("sub", sub, 0o160000).unwrap(); - let input = b.write().unwrap(); + b.insert("keep.rs", objects::git2_oid(&blob), 0o100644) + .unwrap(); + b.insert("link.rs", objects::git2_oid(&link), 0o120000) + .unwrap(); + b.insert("sub", objects::git2_oid(&sub), 0o160000).unwrap(); + let input = objects::gix_oid(b.write().unwrap()); let t = open_transaction(&td); - let key = git2::Oid::from_str("1111111111111111111111111111111111111111").unwrap(); + let key = gix_hash::ObjectId::from_str("1111111111111111111111111111111111111111").unwrap(); let out = remove_pred(&t, &mut String::new(), input, &|_, isblob| isblob, key).unwrap(); assert_ne!(out, input, "dropping the gitlink must produce a new tree"); @@ -1370,7 +1325,7 @@ mod tests { assert!(out_entry(&out_tree, "keep.rs").is_some()); let link_entry = out_entry(&out_tree, "link.rs").expect("symlink kept"); assert_eq!(link_entry.mode.value(), 0o120000); - assert_eq!(objects::git2_oid(&link_entry.oid), link); + assert_eq!(link_entry.oid, link); } // The predicate must see full slash-separated paths at every depth (truncate discipline of @@ -1385,7 +1340,7 @@ mod tests { let input = make_tree(&repo, &paths); let t = open_transaction(&td); - let key = git2::Oid::from_str("2222222222222222222222222222222222222222").unwrap(); + let key = gix_hash::ObjectId::from_str("2222222222222222222222222222222222222222").unwrap(); let seen = std::cell::RefCell::new(Vec::new()); let pred = |path: &str, isblob: bool| { @@ -1414,14 +1369,18 @@ mod tests { .is_none() ); - let key2 = git2::Oid::from_str("3333333333333333333333333333333333333333").unwrap(); + let key2 = + gix_hash::ObjectId::from_str("3333333333333333333333333333333333333333").unwrap(); let out2 = remove_pred(&t, &mut String::new(), input, &|_, _| true, key2).unwrap(); assert_eq!(out2, input, "keep-everything must return the input oid"); } /// Read a tree the code under test produced: its result lives in the transaction's store, /// so it is read through the facade rather than the repository handle. - fn out_entries(t: &cache::Transaction, oid: git2::Oid) -> Vec { + fn out_entries( + t: &cache::Transaction, + oid: gix_hash::ObjectId, + ) -> Vec { objects::read_tree_entries(t.odb(), oid).unwrap() } @@ -1438,7 +1397,10 @@ mod tests { // Write a raw (unvalidated) tree object straight into the odb. This can express fsck-invalid // trees -- legacy filemodes, unsorted or duplicate entries, forbidden names -- that git can // still transport with default settings and that therefore reach remove_pred in production. - fn write_raw_tree(repo: &git2::Repository, entries: &[(&str, &str, git2::Oid)]) -> git2::Oid { + fn write_raw_tree( + repo: &git2::Repository, + entries: &[(&str, &str, gix_hash::ObjectId)], + ) -> gix_hash::ObjectId { let mut data = Vec::new(); for (mode, name, oid) in entries { data.extend_from_slice(mode.as_bytes()); @@ -1447,10 +1409,12 @@ mod tests { data.push(0); data.extend_from_slice(oid.as_bytes()); } - repo.odb() - .unwrap() - .write(git2::ObjectType::Tree, &data) - .unwrap() + objects::gix_oid( + repo.odb() + .unwrap() + .write(git2::ObjectType::Tree, &data) + .unwrap(), + ) } // A tree the filter keeps entirely round-trips byte-for-byte, no matter how fsck-invalid @@ -1460,8 +1424,8 @@ mod tests { fn remove_pred_preserves_fsck_invalid_trees() { let td = tempfile::tempdir().unwrap(); let repo = git2::Repository::init_bare(td.path()).unwrap(); - let blob = repo.blob(b"content").unwrap(); - let blob2 = repo.blob(b"other").unwrap(); + let blob = objects::gix_oid(repo.blob(b"content").unwrap()); + let blob2 = objects::gix_oid(repo.blob(b"other").unwrap()); let t = open_transaction(&td); let sub = write_raw_tree(&repo, &[("100644", "inner.rs", blob)]); @@ -1475,7 +1439,7 @@ mod tests { ("100644", "a.rs", blob2), ], ); - let key = git2::Oid::from_str("4444444444444444444444444444444444444444").unwrap(); + let key = gix_hash::ObjectId::from_str("4444444444444444444444444444444444444444").unwrap(); let out = remove_pred(&t, &mut String::new(), input, &|_, _| true, key).unwrap(); assert_eq!(out, input, "kept-entirely trees pass through verbatim"); } @@ -1486,8 +1450,8 @@ mod tests { fn remove_pred_rebuild_preserves_survivors() { let td = tempfile::tempdir().unwrap(); let repo = git2::Repository::init_bare(td.path()).unwrap(); - let blob = repo.blob(b"content").unwrap(); - let blob2 = repo.blob(b"other").unwrap(); + let blob = objects::gix_oid(repo.blob(b"content").unwrap()); + let blob2 = objects::gix_oid(repo.blob(b"other").unwrap()); let t = open_transaction(&td); let input = write_raw_tree( @@ -1509,7 +1473,7 @@ mod tests { ("100644", "a.rs", blob2), ], ); - let key = git2::Oid::from_str("5555555555555555555555555555555555555555").unwrap(); + let key = gix_hash::ObjectId::from_str("5555555555555555555555555555555555555555").unwrap(); let out = remove_pred( &t, &mut String::new(), @@ -1531,14 +1495,14 @@ mod tests { fn protected_names_are_not_special() { let td = tempfile::tempdir().unwrap(); let repo = git2::Repository::init_bare(td.path()).unwrap(); - let blob = repo.blob(b"content").unwrap(); + let blob = objects::gix_oid(repo.blob(b"content").unwrap()); let t = open_transaction(&td); let input = write_raw_tree( &repo, &[("100644", ".git", blob), ("100644", "keep.rs", blob)], ); - let key = git2::Oid::from_str("6666666666666666666666666666666666666666").unwrap(); + let key = gix_hash::ObjectId::from_str("6666666666666666666666666666666666666666").unwrap(); let out = remove_pred(&t, &mut String::new(), input, &|_, isblob| isblob, key).unwrap(); assert_eq!(out, input, ".git in input passes through verbatim"); @@ -1547,7 +1511,7 @@ mod tests { assert_eq!( get_path_entry(&t, odb, inserted, Path::new("sub/.git")) .unwrap() - .map(|e| objects::git2_oid(&e.oid)), + .map(|e| e.oid), Some(blob), "inserted names are written as given" ); @@ -1560,16 +1524,17 @@ mod tests { fn subtract_overlay_tolerate_non_canonical_order() { let td = tempfile::tempdir().unwrap(); let repo = git2::Repository::init_bare(td.path()).unwrap(); - let blob = repo.blob(b"content").unwrap(); - let blob2 = repo.blob(b"other").unwrap(); + let blob = objects::gix_oid(repo.blob(b"content").unwrap()); + let blob2 = objects::gix_oid(repo.blob(b"other").unwrap()); let t = open_transaction(&td); // "z.rs" first: canonically misplaced, so a bisect for it fails. let unsorted = write_raw_tree(&repo, &[("100644", "z.rs", blob), ("100644", "a.rs", blob)]); let mut b = repo.treebuilder(None).unwrap(); - b.insert("z.rs", blob, 0o100644).unwrap(); - let selector = b.write().unwrap(); + b.insert("z.rs", objects::git2_oid(&blob), 0o100644) + .unwrap(); + let selector = objects::gix_oid(b.write().unwrap()); let out = subtract(&t, unsorted, selector).unwrap(); let out_tree = out_entries(&t, out); @@ -1583,8 +1548,9 @@ mod tests { // the new entry lands after the last entry that canonically precedes it (here: at // the end, after a.rs). let mut b = repo.treebuilder(None).unwrap(); - b.insert("m.rs", blob2, 0o100644).unwrap(); - let addition = b.write().unwrap(); + b.insert("m.rs", objects::git2_oid(&blob2), 0o100644) + .unwrap(); + let addition = objects::gix_oid(b.write().unwrap()); let out = overlay(&t, unsorted, addition).unwrap(); let expected = write_raw_tree( &repo, @@ -1603,16 +1569,18 @@ mod tests { // Build a tree whose blob contents depend only on the entry NAME (not the full path), so // directories with identical children get identical subtree oids -- the precondition for // exercising the path-aliasing scenario. - fn make_named_tree(repo: &git2::Repository, paths: &[String]) -> git2::Oid { + fn make_named_tree(repo: &git2::Repository, paths: &[String]) -> gix_hash::ObjectId { let mut b = git2::build::TreeUpdateBuilder::new(); for p in paths { let name = p.rsplit('/').next().unwrap(); - let oid = repo.blob(name.as_bytes()).unwrap(); - b.upsert(p.as_str(), oid, git2::FileMode::Blob); + let oid = objects::gix_oid(repo.blob(name.as_bytes()).unwrap()); + b.upsert(p.as_str(), objects::git2_oid(&oid), git2::FileMode::Blob); } let base = repo.treebuilder(None).unwrap().write().unwrap(); - b.create_updated(repo, &repo.find_tree(base).unwrap()) - .unwrap() + objects::gix_oid( + b.create_updated(repo, &repo.find_tree(base).unwrap()) + .unwrap(), + ) } // Ground truth for a pattern filter: enumerate every blob path of `input` and keep exactly @@ -1620,9 +1588,13 @@ mod tests { // with a TreeUpdateBuilder (which drops empty dirs). Deliberately NOT remove_pred: the old // full-path walk had an order-dependent cache-aliasing bug for identical subtrees at // different paths, which the duplicated-subtree case below exercises. - fn ground_truth_tree(repo: &git2::Repository, input: git2::Oid, pattern: &str) -> git2::Oid { + fn ground_truth_tree( + repo: &git2::Repository, + input: gix_hash::ObjectId, + pattern: &str, + ) -> gix_hash::ObjectId { let glob = glob::Pattern::new(pattern).unwrap(); - let tree = repo.find_tree(input).unwrap(); + let tree = repo.find_tree(objects::git2_oid(&input)).unwrap(); let mut kept = vec![]; tree.walk(git2::TreeWalkMode::PreOrder, |root, entry| { if entry.kind() == Some(git2::ObjectType::Blob) { @@ -1639,8 +1611,10 @@ mod tests { b.upsert(path.as_str(), *oid, git2::FileMode::Blob); } let base = repo.treebuilder(None).unwrap().write().unwrap(); - b.create_updated(repo, &repo.find_tree(base).unwrap()) - .unwrap() + objects::gix_oid( + b.create_updated(repo, &repo.find_tree(base).unwrap()) + .unwrap(), + ) } // Property-style equivalence of the component-wise NFA walk against full-path glob matching: @@ -1711,7 +1685,7 @@ mod tests { let input = make_named_tree(&repo, &paths); - let tree = repo.find_tree(input).unwrap(); + let tree = repo.find_tree(objects::git2_oid(&input)).unwrap(); assert_eq!( tree.get_path(Path::new("a/x")).unwrap().id(), tree.get_path(Path::new("c/x")).unwrap().id(), @@ -1743,7 +1717,7 @@ mod tests { // Isolate the cases from each other (and from other tests in this process). cache::clear_global_caches(); - let key = git2::Oid::hash_object(git2::ObjectType::Blob, pattern.as_bytes()).unwrap(); + let key = objects::hash_blob(pattern.as_bytes()); let cp = CompiledPattern::compile(pattern).unwrap(); assert!(!cp.fallback, "`{pattern}` must not need the fallback"); let got = @@ -1761,7 +1735,7 @@ mod tests { // must produce ground-truth results with per-root cache keys. #[test] fn compiled_pattern_fallback_cases() { - let key = git2::Oid::from_str("1234567890123456789012345678901234567890").unwrap(); + let key = gix_hash::ObjectId::from_str("1234567890123456789012345678901234567890").unwrap(); // A '/' inside a bracket class never splits: it is inside the class token, not a // `Char('/')` token, so these stay on the NFA walk. @@ -1808,7 +1782,7 @@ mod tests { let repo = git2::Repository::init_bare(td.path()).unwrap(); let paths: Vec = ["a/f.txt", "b/f.txt"].map(String::from).to_vec(); let input = make_named_tree(&repo, &paths); - let tree = repo.find_tree(input).unwrap(); + let tree = repo.find_tree(objects::git2_oid(&input)).unwrap(); assert_eq!( tree.get_path(Path::new("a")).unwrap().id(), tree.get_path(Path::new("b")).unwrap().id() @@ -1816,7 +1790,7 @@ mod tests { let t = open_transaction(&td); let pattern = glob::Pattern::new("a/*.txt").unwrap(); - let key = git2::Oid::from_str("abcdef1234567890123456789012345678901234").unwrap(); + let key = gix_hash::ObjectId::from_str("abcdef1234567890123456789012345678901234").unwrap(); let out = remove_pred( &t, &mut String::new(), @@ -1837,24 +1811,30 @@ mod tests { fn get_path_entry_contract() { let td = tempfile::tempdir().unwrap(); let repo = git2::Repository::init_bare(td.path()).unwrap(); - let blob = repo.blob(b"content").unwrap(); + let blob = objects::gix_oid(repo.blob(b"content").unwrap()); let t = open_transaction(&td); let odb = t.odb(); - let gitlink = git2::Oid::from_str("0123456789012345678901234567890123456789").unwrap(); + let gitlink = + gix_hash::ObjectId::from_str("0123456789012345678901234567890123456789").unwrap(); let mut b = git2::build::TreeUpdateBuilder::new(); - b.upsert("a/b/deep.txt", blob, git2::FileMode::Blob); - b.upsert("top.txt", blob, git2::FileMode::Blob); - b.upsert("a/sub", gitlink, git2::FileMode::Commit); + b.upsert( + "a/b/deep.txt", + objects::git2_oid(&blob), + git2::FileMode::Blob, + ); + b.upsert("top.txt", objects::git2_oid(&blob), git2::FileMode::Blob); + b.upsert("a/sub", objects::git2_oid(&gitlink), git2::FileMode::Commit); let base = repo.treebuilder(None).unwrap().write().unwrap(); - let root = b - .create_updated(&repo, &repo.find_tree(base).unwrap()) - .unwrap(); + let root = objects::gix_oid( + b.create_updated(&repo, &repo.find_tree(base).unwrap()) + .unwrap(), + ); let entry = get_path_entry(&t, odb, root, Path::new("a/b/deep.txt")) .unwrap() .expect("hit at depth 2"); - assert_eq!(objects::git2_oid(&entry.oid), blob); + assert_eq!(entry.oid, blob); assert!(!entry.mode.is_tree()); let entry = get_path_entry(&t, odb, root, Path::new("a/sub")) @@ -1931,14 +1911,15 @@ mod tests { let td = tempfile::tempdir().unwrap(); let repo = git2::Repository::init_bare(td.path()).unwrap(); - let blob = repo.blob(b"legacy").unwrap(); + let blob = objects::gix_oid(repo.blob(b"legacy").unwrap()); let sub_tree = make_tree(&repo, &["dir/a.txt", "dir/b.txt"]); - let dir = repo - .find_tree(sub_tree) - .unwrap() - .get_name("dir") - .unwrap() - .id(); + let dir = objects::gix_oid( + repo.find_tree(objects::git2_oid(&sub_tree)) + .unwrap() + .get_name("dir") + .unwrap() + .id(), + ); let input1 = write_raw_tree( &repo, &[("40000", "dir", dir), ("100664", "legacy.rs", blob)], @@ -1954,10 +1935,7 @@ mod tests { 0o100664, "untouched entries must keep their raw mode, like the seeded treebuilder" ); - let out_dir = out_entries( - &t, - objects::git2_oid(&out_entry(&out_tree, "dir").unwrap().oid), - ); + let out_dir = out_entries(&t, out_entry(&out_tree, "dir").unwrap().oid); assert!( out_entry(&out_dir, "a.txt").is_none(), "matched path removed" @@ -1975,32 +1953,27 @@ mod tests { let td = tempfile::tempdir().unwrap(); let repo = git2::Repository::init_bare(td.path()).unwrap(); - let ours = repo.blob(b"ours").unwrap(); - let theirs = repo.blob(b"theirs").unwrap(); + let ours = objects::gix_oid(repo.blob(b"ours").unwrap()); + let theirs = objects::gix_oid(repo.blob(b"theirs").unwrap()); let input1 = write_raw_tree(&repo, &[("100664", "shared.rs", ours)]); let mut b = repo.treebuilder(None).unwrap(); - b.insert("new.rs", theirs, 0o100644).unwrap(); - b.insert("shared.rs", theirs, 0o100644).unwrap(); - let input2 = b.write().unwrap(); + b.insert("new.rs", objects::git2_oid(&theirs), 0o100644) + .unwrap(); + b.insert("shared.rs", objects::git2_oid(&theirs), 0o100644) + .unwrap(); + let input2 = objects::gix_oid(b.write().unwrap()); let t = open_transaction(&td); let out = overlay(&t, input1, input2).unwrap(); let out_tree = out_entries(&t, out); let shared = out_entry(&out_tree, "shared.rs").unwrap(); - assert_eq!( - objects::git2_oid(&shared.oid), - ours, - "input1 wins blob collisions" - ); + assert_eq!(shared.oid, ours, "input1 wins blob collisions"); assert_eq!( shared.mode.value(), 0o100664, "collision entries keep input1's raw mode" ); - assert_eq!( - objects::git2_oid(&out_entry(&out_tree, "new.rs").unwrap().oid), - theirs - ); + assert_eq!(out_entry(&out_tree, "new.rs").unwrap().oid, theirs); } } diff --git a/josh-core/src/git.rs b/josh-core/src/git.rs index cd4ac9d8d..def25f308 100644 --- a/josh-core/src/git.rs +++ b/josh-core/src/git.rs @@ -1,6 +1,7 @@ use anyhow::{Context, anyhow}; use std::io::IsTerminal; use std::path::PathBuf; +use std::str::FromStr; /// Resolve the `input_ref` argument to a commit OID. /// @@ -13,7 +14,7 @@ use std::path::PathBuf; pub fn resolve_snapshot_input( transaction: &crate::cache::Transaction, input_ref: &str, -) -> anyhow::Result { +) -> anyhow::Result { let repo = transaction.git2_repo(); if input_ref == "+" || input_ref == "." { let mut index = repo.index()?; @@ -30,14 +31,18 @@ pub fn resolve_snapshot_input( let sig = crate::git::josh_commit_signature()?; let head_commit = repo.head()?.peel_to_commit()?; let commit_oid = repo.commit(None, &sig, &sig, "WIP", &tree, &[&head_commit])?; - Ok(commit_oid) - } else if let Ok(oid) = git2::Oid::from_str(input_ref) { - Ok(repo.find_object(oid, None)?.peel_to_commit()?.id()) + Ok(crate::objects::gix_oid(commit_oid)) + } else if let Ok(oid) = gix_hash::ObjectId::from_str(input_ref) { + Ok(crate::objects::gix_oid( + repo.find_object(crate::objects::git2_oid(&oid), None)? + .peel_to_commit()? + .id(), + )) } else { let obj = repo .revparse_single(input_ref) .with_context(|| format!("could not resolve input: {:?}", input_ref))?; - Ok(obj.peel_to_commit()?.id()) + Ok(crate::objects::gix_oid(obj.peel_to_commit()?.id())) } } @@ -247,8 +252,11 @@ impl GitCommand { /// lines via `gix_object::CommitRefIter`. This avoids libgit2's commit parse cache (a /// lock-guarded global that also decodes author/committer/message) whenever a caller only /// needs the parent ids; memory-store hits are zero-copy. -pub fn read_parent_ids(odb: &josh_memodb::Odb, oid: git2::Oid) -> anyhow::Result> { - let (kind, bytes) = odb.read(crate::objects::gix_oid(oid))?; +pub fn read_parent_ids( + odb: &josh_memodb::Odb, + oid: gix_hash::ObjectId, +) -> anyhow::Result> { + let (kind, bytes) = odb.read(oid)?; // A hard error, not an assert: this is reachable from inside git2 callback frames, // where unwinding across the FFI boundary would abort. if kind != gix_object::Kind::Commit { @@ -258,16 +266,20 @@ pub fn read_parent_ids(odb: &josh_memodb::Odb, oid: git2::Oid) -> anyhow::Result kind )); } - gix_object::CommitRefIter::from_bytes(&bytes, gix_hash::Kind::Sha1) - .parent_ids() - .map(|p| Ok(git2::Oid::from_bytes(p.as_bytes())?)) - .collect() + Ok( + gix_object::CommitRefIter::from_bytes(&bytes, gix_hash::Kind::Sha1) + .parent_ids() + .collect(), + ) } /// Sibling of [`read_parent_ids`]: read a commit's tree OID without touching libgit2's /// commit parse cache. -pub fn read_tree_id(odb: &josh_memodb::Odb, oid: git2::Oid) -> anyhow::Result { - let (kind, bytes) = odb.read(crate::objects::gix_oid(oid))?; +pub fn read_tree_id( + odb: &josh_memodb::Odb, + oid: gix_hash::ObjectId, +) -> anyhow::Result { + let (kind, bytes) = odb.read(oid)?; // Same hard-error rationale as read_parent_ids. if kind != gix_object::Kind::Commit { return Err(anyhow::anyhow!( @@ -276,8 +288,7 @@ pub fn read_tree_id(odb: &josh_memodb::Odb, oid: git2::Oid) -> anyhow::Result, flag: &str) -> bool { pub fn walk2( filter: filter::Filter, - input: git2::Oid, + input: gix_hash::ObjectId, transaction: &cache::Transaction, ) -> anyhow::Result<()> { if transaction.known(filter, input)? { @@ -69,16 +69,16 @@ fn find_unapply_base( // Used as a cache to avoid re-applying the filter to the same commit - // this function is called during revwalk so there be a lot of repeated // calls - filtered_to_original: &mut HashMap, + filtered_to_original: &mut HashMap, filter: filter::Filter, // When building the filtered_to_original mapping use this as a starting point // for the search for originals. If there are multiple originals that map to the // same filtered commit (which is common) use one that is reachable from contained_in. // Or, in other words, one that is contained in the history of contained_in. - contained_in: git2::Oid, + contained_in: gix_hash::ObjectId, // Filtered OID to compare against - filtered: git2::Oid, -) -> anyhow::Result { + filtered: gix_hash::ObjectId, +) -> anyhow::Result { // Consult the running map first: during an unapply walk we insert every // freshly created commit here, so a later commit can find its parent even // when there is no `contained_in` hint (e.g. a no-base push of an orphan @@ -89,13 +89,13 @@ fn find_unapply_base( return Ok(*original); } - if contained_in == git2::Oid::ZERO_SHA1 { + if contained_in == gix_hash::ObjectId::null(gix_hash::Kind::Sha1) { tracing::info!("contained in zero",); - return Ok(git2::Oid::ZERO_SHA1); + return Ok(gix_hash::ObjectId::null(gix_hash::Kind::Sha1)); } let oid = filter::apply_to_commit(filter, contained_in, transaction)?; - if oid != git2::Oid::ZERO_SHA1 { + if oid != gix_hash::ObjectId::null(gix_hash::Kind::Sha1) { filtered_to_original.insert(oid, contained_in); } @@ -110,7 +110,7 @@ fn find_unapply_base( let mut walk = objects::RevWalk::new(odb); walk.push(contained_in)?; - let mut result: Option<(git2::Oid, git2::Oid)> = None; + let mut result: Option<(gix_hash::ObjectId, gix_hash::ObjectId)> = None; let mut unlocked = HashSet::new(); let mut pending = HashSet::new(); unlocked.insert(contained_in); @@ -152,7 +152,7 @@ fn find_unapply_base( } None => { tracing::info!("Didn't find original",); - Ok(git2::Oid::ZERO_SHA1) + Ok(gix_hash::ObjectId::null(gix_hash::Kind::Sha1)) } } } @@ -160,12 +160,12 @@ fn find_unapply_base( pub fn find_original( transaction: &cache::Transaction, filter: filter::Filter, - contained_in: git2::Oid, - filtered: git2::Oid, + contained_in: gix_hash::ObjectId, + filtered: gix_hash::ObjectId, linear: bool, -) -> anyhow::Result { - if contained_in == git2::Oid::ZERO_SHA1 { - return Ok(git2::Oid::ZERO_SHA1); +) -> anyhow::Result { + if contained_in == gix_hash::ObjectId::null(gix_hash::Kind::Sha1) { + return Ok(gix_hash::ObjectId::null(gix_hash::Kind::Sha1)); } if filter.is_nop() { return Ok(filtered); @@ -191,7 +191,7 @@ pub fn find_original( } } - Ok(git2::Oid::ZERO_SHA1) + Ok(gix_hash::ObjectId::null(gix_hash::Kind::Sha1)) } // takes everything from base except its tree and replaces it with the tree @@ -199,10 +199,10 @@ pub fn find_original( pub fn rewrite_commit( odb: &josh_memodb::Odb, base: &objects::CommitData, - parents: &[git2::Oid], + parents: &[gix_hash::ObjectId], rewrite_data: filter::Rewrite, gpgsig: GpgsigMode, -) -> anyhow::Result { +) -> anyhow::Result { use gix_object::bstr::BString; // gix_object::CommitRef uses byte strings for Oids, but in hex representation, not raw bytes. @@ -292,7 +292,7 @@ pub fn rewrite_commit( let mut b = vec![]; gix_object::WriteTo::write_to(&commit, &mut b)?; - Ok(objects::git2_oid(&odb.write(gix_object::Kind::Commit, &b))) + Ok(odb.write(gix_object::Kind::Commit, &b)) } // Given an OID of an unfiltered commit and a filter, @@ -301,8 +301,8 @@ pub fn rewrite_commit( fn find_oldest_similar_commit( transaction: &cache::Transaction, filter: filter::Filter, - unfiltered: git2::Oid, -) -> anyhow::Result { + unfiltered: gix_hash::ObjectId, +) -> anyhow::Result { let odb = transaction.odb(); let mut walk = objects::RevWalk::new(odb); walk.push(unfiltered)?; @@ -323,12 +323,12 @@ fn find_oldest_similar_commit( fn find_new_branch_base( transaction: &cache::Transaction, - filtered_to_original: &mut HashMap, + filtered_to_original: &mut HashMap, filter: filter::Filter, // See "contained_in" in find_unapply_base - contained_in: git2::Oid, - filtered: git2::Oid, -) -> anyhow::Result { + contained_in: gix_hash::ObjectId, + filtered: gix_hash::ObjectId, +) -> anyhow::Result { let odb = transaction.odb(); let mut walk = objects::RevWalk::new(odb); walk.push(filtered)?; @@ -338,7 +338,7 @@ fn find_new_branch_base( for rev in walk.into_topo_vec(|_| false)? { if let Ok(base) = find_unapply_base(transaction, filtered_to_original, filter, contained_in, rev) - && base != git2::Oid::ZERO_SHA1 + && base != gix_hash::ObjectId::null(gix_hash::Kind::Sha1) { tracing::info!("new branch base: {:?} mapping to {:?}", base, rev); let base = if let Ok(new_base) = find_oldest_similar_commit(transaction, filter, base) { @@ -354,7 +354,7 @@ fn find_new_branch_base( } } tracing::info!("new branch base not found"); - Ok(git2::Oid::ZERO_SHA1) + Ok(gix_hash::ObjectId::null(gix_hash::Kind::Sha1)) } #[derive(Clone, Debug)] @@ -368,16 +368,16 @@ pub enum OrphansMode { pub fn unapply_filter( transaction: &cache::Transaction, filter: filter::Filter, - original_target: git2::Oid, - old_filtered_oid: git2::Oid, - new_filtered_oid: git2::Oid, + original_target: gix_hash::ObjectId, + old_filtered_oid: gix_hash::ObjectId, + new_filtered_oid: gix_hash::ObjectId, orphans_mode: OrphansMode, - reparent_orphans: Option, -) -> anyhow::Result { + reparent_orphans: Option, +) -> anyhow::Result { let mut filtered_to_original = HashMap::new(); let mut ret = original_target; - let old_filtered_oid = if old_filtered_oid == git2::Oid::ZERO_SHA1 { + let old_filtered_oid = if old_filtered_oid == gix_hash::ObjectId::null(gix_hash::Kind::Sha1) { match find_new_branch_base( transaction, &mut filtered_to_original, @@ -426,7 +426,7 @@ pub fn unapply_filter( // The old filtered oid can be missing from the repo (e.g. a new branch); // there is no range to exclude then, so take everything reachable. let old_filtered_exists = matches!( - odb.read_header(objects::gix_oid(old_filtered_oid)), + odb.read_header(old_filtered_oid), Ok((gix_object::Kind::Commit, _)) ); let revs = if old_filtered_exists { @@ -470,7 +470,7 @@ pub fn unapply_filter( OrphansMode::Fail => { return Err(anyhow!(indoc::formatdoc!( r###" - Rejecting new orphan branch at {:?} ({:?}) + Rejecting new orphan branch at {:?} ({}) Specify one of these options: '-o allow_orphans' to keep the history as is '-o merge' to import new history by creating merge commit @@ -497,7 +497,7 @@ pub fn unapply_filter( }) .filter(|unapply_base| { if let Ok(oid) = unapply_base { - *oid != git2::Oid::ZERO_SHA1 + *oid != gix_hash::ObjectId::null(gix_hash::Kind::Sha1) } else { true } @@ -555,7 +555,7 @@ pub fn unapply_filter( Ok(new_trees) => new_trees, Err(e) => { return Err(anyhow!( - "\nCan't apply {:?} ({:?})\n{}", + "\nCan't apply {:?} ({})\n{}", commit_message, module_commit.id(), e @@ -594,7 +594,7 @@ pub fn unapply_filter( // This will typically be parent_count == 2 and mean we are dealing with a merge // where the parents have differences outside of the filter. parent_count => { - let mut tid = git2::Oid::ZERO_SHA1; + let mut tid = gix_hash::ObjectId::null(gix_hash::Kind::Sha1); for i in 0..parent_count { // If one of the parents is a descendant of the target branch and the other is // not, pick the tree of the one that is a descendant. @@ -610,7 +610,7 @@ pub fn unapply_filter( } } - if tid == git2::Oid::ZERO_SHA1 && parent_count == 2 { + if tid == gix_hash::ObjectId::null(gix_hash::Kind::Sha1) && parent_count == 2 { // If we could not select one of the parents, try to merge them. // We expect conflicts to occur only in the paths that are present in // the filtered commit. @@ -657,7 +657,7 @@ pub fn unapply_filter( } } - if tid == git2::Oid::ZERO_SHA1 { + if tid == gix_hash::ObjectId::null(gix_hash::Kind::Sha1) { // We give up. If we see this message again we need to investigate once // more and maybe consider allowing a manual override as last resort. tracing::warn!("rejecting merge"); @@ -679,7 +679,7 @@ pub fn unapply_filter( let apply = filter::Rewrite::from_tree(new_tree); - let original_parent_oids: Vec = + let original_parent_oids: Vec = original_parents.iter().map(|c| c.id()).collect(); ret = rewrite_commit( odb, @@ -711,9 +711,9 @@ pub fn unapply_filter( fn select_parent_commits( odb: &josh_memodb::Odb, original_commit: &objects::CommitData, - filtered_tree_id: git2::Oid, - filtered_parents: &[(git2::Oid, git2::Oid)], -) -> anyhow::Result> { + filtered_tree_id: gix_hash::ObjectId, + filtered_parents: &[(gix_hash::ObjectId, gix_hash::ObjectId)], +) -> anyhow::Result> { let affects_filtered = filtered_parents .iter() .any(|(_, tree_id)| filtered_tree_id != *tree_id); @@ -737,15 +737,15 @@ fn select_parent_commits( // parents={none, linear, keep-trivial, default} pub fn drop_commit( - original_commit: git2::Oid, - filtered_parent_ids: Vec, + original_commit: gix_hash::ObjectId, + filtered_parent_ids: Vec, transaction: &cache::Transaction, filter: filter::Filter, -) -> anyhow::Result { +) -> anyhow::Result { let r = if let Some(id) = filtered_parent_ids.first() { *id } else { - git2::Oid::ZERO_SHA1 + gix_hash::ObjectId::null(gix_hash::Kind::Sha1) }; transaction.insert(filter, original_commit, r, false)?; @@ -755,12 +755,12 @@ pub fn drop_commit( pub fn create_filtered_commit_with_meta( original_commit: &objects::CommitData, - filtered_parent_ids: Vec, + filtered_parent_ids: Vec, rewrite_data: filter::Rewrite, transaction: &cache::Transaction, filter: filter::Filter, meta: std::collections::BTreeMap, -) -> anyhow::Result { +) -> anyhow::Result { let (r, is_new) = create_filtered_commit2( transaction, original_commit, @@ -778,11 +778,11 @@ pub fn create_filtered_commit_with_meta( pub fn create_filtered_commit( original_commit: &objects::CommitData, - filtered_parent_ids: Vec, + filtered_parent_ids: Vec, rewrite_data: filter::Rewrite, transaction: &cache::Transaction, filter: filter::Filter, -) -> anyhow::Result { +) -> anyhow::Result { create_filtered_commit_with_meta( original_commit, filtered_parent_ids, @@ -796,14 +796,14 @@ pub fn create_filtered_commit( fn create_filtered_commit2( transaction: &cache::Transaction, original_commit: &objects::CommitData, - filtered_parent_ids: Vec, + filtered_parent_ids: Vec, rewrite_data: filter::Rewrite, options: BTreeMap, -) -> anyhow::Result<(git2::Oid, bool)> { +) -> anyhow::Result<(gix_hash::ObjectId, bool)> { let odb = transaction.odb(); - let mut filtered_parents: Vec<(git2::Oid, git2::Oid)> = filtered_parent_ids + let mut filtered_parents: Vec<(gix_hash::ObjectId, gix_hash::ObjectId)> = filtered_parent_ids .iter() - .filter(|x| **x != git2::Oid::ZERO_SHA1) + .filter(|x| **x != gix_hash::ObjectId::null(gix_hash::Kind::Sha1)) .map(|x| Ok((*x, filtered_parent_tree_id(transaction, *x)?))) .collect::>()?; @@ -819,7 +819,7 @@ fn create_filtered_commit2( let nonzero_parent_ids: Vec<_> = filtered_parent_ids .iter() .copied() - .filter(|x| *x != git2::Oid::ZERO_SHA1) + .filter(|x| *x != gix_hash::ObjectId::null(gix_hash::Kind::Sha1)) .collect(); let is_initial_merge = nonzero_parent_ids.len() > 1 && !cache::parents_share_root(transaction, &nonzero_parent_ids)?; @@ -859,7 +859,7 @@ fn create_filtered_commit2( } } - let selected_filtered_parent_ids: Vec = select_parent_commits( + let selected_filtered_parent_ids: Vec = select_parent_commits( odb, original_commit, rewrite_data.tree_id(), @@ -875,7 +875,7 @@ fn create_filtered_commit2( return Ok((filtered_parents[0].0, false)); } if rewrite_data.tree_id() == filter::tree::empty_id() { - return Ok((git2::Oid::ZERO_SHA1, false)); + return Ok((gix_hash::ObjectId::null(gix_hash::Kind::Sha1), false)); } } @@ -905,8 +905,8 @@ fn create_filtered_commit2( /// parsing the commit from the odb. pub(crate) fn filtered_parent_tree_id( transaction: &cache::Transaction, - oid: git2::Oid, -) -> anyhow::Result { + oid: gix_hash::ObjectId, +) -> anyhow::Result { if let Some((last, tree_id)) = transaction.last_written_commit() { if last == oid { return Ok(tree_id); @@ -920,7 +920,7 @@ pub(crate) fn filtered_parent_tree_id( fn is_empty_root( transaction: &cache::Transaction, odb: &josh_memodb::Odb, - oid: git2::Oid, + oid: gix_hash::ObjectId, ) -> anyhow::Result { if oid == filter::tree::empty_id() { return Ok(true); @@ -933,13 +933,13 @@ fn is_empty_root( Ok(tree .entries .iter() - .all(|e| e.mode.is_tree() && is_empty_subtree(transaction, odb, objects::git2_oid(e.oid)))) + .all(|e| e.mode.is_tree() && is_empty_subtree(transaction, odb, e.oid.to_owned()))) } fn is_empty_subtree( transaction: &cache::Transaction, odb: &josh_memodb::Odb, - oid: git2::Oid, + oid: gix_hash::ObjectId, ) -> bool { if oid == filter::tree::empty_id() { return true; @@ -952,12 +952,13 @@ fn is_empty_subtree( }; tree.entries .iter() - .all(|e| e.mode.is_tree() && is_empty_subtree(transaction, odb, objects::git2_oid(e.oid))) + .all(|e| e.mode.is_tree() && is_empty_subtree(transaction, odb, e.oid.to_owned())) } #[cfg(test)] mod tests { use super::*; + use std::str::FromStr; // A root is "empty" iff it contains nothing but (recursively) empty trees: the empty tree // itself and nested empty chains qualify; any blob or gitlink anywhere disqualifies. @@ -981,20 +982,22 @@ mod tests { data.extend_from_slice(b"40000 sub"); data.push(0); data.extend_from_slice(empty.as_bytes()); - let chain = repo - .odb() - .unwrap() - .write(git2::ObjectType::Tree, &data) - .unwrap(); + let chain = objects::gix_oid( + repo.odb() + .unwrap() + .write(git2::ObjectType::Tree, &data) + .unwrap(), + ); let mut data = Vec::new(); data.extend_from_slice(b"40000 nested"); data.push(0); data.extend_from_slice(chain.as_bytes()); - let chain2 = repo - .odb() - .unwrap() - .write(git2::ObjectType::Tree, &data) - .unwrap(); + let chain2 = objects::gix_oid( + repo.odb() + .unwrap() + .write(git2::ObjectType::Tree, &data) + .unwrap(), + ); assert!(is_empty_root(&t, odb, chain2).unwrap()); // A blob anywhere makes the root non-empty; so does a gitlink. @@ -1002,15 +1005,18 @@ mod tests { let mut b = git2::build::TreeUpdateBuilder::new(); b.upsert("a/b/file.txt", blob, git2::FileMode::Blob); let base = repo.treebuilder(None).unwrap().write().unwrap(); - let with_blob = b - .create_updated(&repo, &repo.find_tree(base).unwrap()) - .unwrap(); + let with_blob = objects::gix_oid( + b.create_updated(&repo, &repo.find_tree(base).unwrap()) + .unwrap(), + ); assert!(!is_empty_root(&t, odb, with_blob).unwrap()); - let gitlink = git2::Oid::from_str("0123456789012345678901234567890123456789").unwrap(); + let gitlink = + gix_hash::ObjectId::from_str("0123456789012345678901234567890123456789").unwrap(); let mut b = repo.treebuilder(None).unwrap(); - b.insert("sub", gitlink, 0o160000).unwrap(); - let with_gitlink = b.write().unwrap(); + b.insert("sub", objects::git2_oid(&gitlink), 0o160000) + .unwrap(); + let with_gitlink = objects::gix_oid(b.write().unwrap()); assert!(!is_empty_root(&t, odb, with_gitlink).unwrap()); } } diff --git a/josh-core/src/housekeeping.rs b/josh-core/src/housekeeping.rs index d5d22f3b2..027fbc515 100644 --- a/josh-core/src/housekeeping.rs +++ b/josh-core/src/housekeeping.rs @@ -5,7 +5,7 @@ use std::collections::{BTreeSet, HashMap}; use std::sync::LazyLock; use tracing::{Level, info, span}; -pub type KnownViews = HashMap)>; +pub type KnownViews = HashMap)>; static KNOWN_FILTERS: LazyLock> = LazyLock::new(|| std::sync::Mutex::new(std::collections::HashMap::new())); @@ -13,7 +13,7 @@ static KNOWN_FILTERS: LazyLock> = pub fn list_refs( transaction: &cache::Transaction, upstream_repo: &str, -) -> anyhow::Result> { +) -> anyhow::Result> { let mut refs = vec![]; let prefix = ["refs", "josh", "upstream", &to_ns(upstream_repo)] @@ -48,7 +48,12 @@ pub fn remember_filter(upstream_repo: &str, filter_spec: &str) { { let known_f = &mut known_filters .entry(upstream_repo.trim_start_matches('/').to_string()) - .or_insert_with(|| (git2::Oid::ZERO_SHA1, BTreeSet::new())); + .or_insert_with(|| { + ( + gix_hash::ObjectId::null(gix_hash::Kind::Sha1), + BTreeSet::new(), + ) + }); known_f.1.insert(filter_spec.to_string()); } @@ -83,7 +88,12 @@ pub fn default_from_to( { let known_f = &mut known_filters .entry(upstream_repo.trim_start_matches('/').to_string()) - .or_insert_with(|| (git2::Oid::ZERO_SHA1, BTreeSet::new())); + .or_insert_with(|| { + ( + gix_hash::ObjectId::null(gix_hash::Kind::Sha1), + BTreeSet::new(), + ) + }); known_f.1.insert(filter_spec.to_string()); } @@ -95,7 +105,7 @@ pub fn memorize_from_to( transaction: &cache::Transaction, namespace: &str, upstream_repo: &str, -) -> anyhow::Result<((String, git2::Oid), String)> { +) -> anyhow::Result<((String, gix_hash::ObjectId), String)> { let from = format!("refs/josh/upstream/{}/HEAD", &to_ns(upstream_repo)); let to_ref = format!("refs/{}/HEAD", &namespace); @@ -135,17 +145,19 @@ pub fn discover_filter_candidates(transaction: &cache::Transaction) -> anyhow::R let name = from_ns(&name); - let known_f = &mut known_filters - .entry(name.clone()) - .or_insert_with(|| (git2::Oid::ZERO_SHA1, BTreeSet::new())); + let known_f = &mut known_filters.entry(name.clone()).or_insert_with(|| { + ( + gix_hash::ObjectId::null(gix_hash::Kind::Sha1), + BTreeSet::new(), + ) + }); if known_f.0 != target { - // PORT: mirror-resident fetched target, and `peel` tolerates annotated tags -- - // stays on git2 until flag day. + // Fetched targets may be annotated tags. let tree = repo - .find_object(target, None)? + .find_object(objects::git2_oid(&target), None)? .peel(git2::ObjectType::Tree)?; - let hs = find_all_workspaces_and_subdirectories(odb, tree.id())?; + let hs = find_all_workspaces_and_subdirectories(odb, objects::gix_oid(tree.id()))?; known_f.0 = target; for i in hs { known_f.1.insert(i); @@ -161,7 +173,12 @@ pub fn discover_filter_candidates(transaction: &cache::Transaction) -> anyhow::R known_filters .entry(from_ns(&filtered.upstream_repo)) - .or_insert_with(|| (git2::Oid::ZERO_SHA1, BTreeSet::new())) + .or_insert_with(|| { + ( + gix_hash::ObjectId::null(gix_hash::Kind::Sha1), + BTreeSet::new(), + ) + }) .1 .insert(from_ns(&filtered.filter_spec)); Ok(()) @@ -172,7 +189,7 @@ pub fn discover_filter_candidates(transaction: &cache::Transaction) -> anyhow::R pub fn find_all_workspaces_and_subdirectories( src: &impl gix_object::Find, - tree: git2::Oid, + tree: gix_hash::ObjectId, ) -> anyhow::Result> { let _trace_s = span!(Level::TRACE, "find_all_workspaces_and_subdirectories"); let mut hs = std::collections::HashSet::new(); @@ -198,7 +215,7 @@ pub fn find_all_workspaces_and_subdirectories( pub fn refresh_known_filters( transaction_mirror: &cache::Transaction, transaction_overlay: &cache::Transaction, -) -> anyhow::Result> { +) -> anyhow::Result> { let known_filters = KNOWN_FILTERS.lock().unwrap(); let mut updated_refs = vec![]; for (upstream_repo, e) in known_filters.iter() { diff --git a/josh-core/src/lib.rs b/josh-core/src/lib.rs index 546726918..d98a08d7a 100644 --- a/josh-core/src/lib.rs +++ b/josh-core/src/lib.rs @@ -132,8 +132,8 @@ pub fn reset_caches() -> anyhow::Result<()> { pub fn filter_commit( transaction: &cache::Transaction, filterobj: filter::Filter, - oid: git2::Oid, -) -> anyhow::Result { + oid: gix_hash::ObjectId, +) -> anyhow::Result { // A chained filter feeds the previous step's freshly built commit in here, so the peel // has to see the transaction's buffered objects. let original_commit = objects::peel_to_commit(transaction.odb(), oid)?; @@ -154,8 +154,11 @@ pub fn filter_commit( pub fn filter_refs( transaction: &cache::Transaction, filterobj: filter::Filter, - refs: &[(String, git2::Oid)], -) -> (Vec<(String, git2::Oid)>, Vec<(String, anyhow::Error)>) { + refs: &[(String, gix_hash::ObjectId)], +) -> ( + Vec<(String, gix_hash::ObjectId)>, + Vec<(String, anyhow::Error)>, +) { let s = tracing::Span::current(); let _e = s.enter(); let mut updated = vec![]; @@ -174,7 +177,7 @@ pub fn filter_refs( warn = true, from = k.0.as_str(), ); - git2::Oid::ZERO_SHA1 + gix_hash::ObjectId::null(gix_hash::Kind::Sha1) } }; updated.push((k.0.to_string(), oid)); @@ -183,9 +186,9 @@ pub fn filter_refs( (updated, errors) } -pub fn update_refs(transaction: &cache::Transaction, updated: Vec<(String, git2::Oid)>) { +pub fn update_refs(transaction: &cache::Transaction, updated: Vec<(String, gix_hash::ObjectId)>) { for (refn, filtered_commit) in updated.into_iter() { - if filtered_commit.is_zero() { + if filtered_commit.is_null() { continue; } diff --git a/josh-core/src/link.rs b/josh-core/src/link.rs index 3c15a71de..3e8ee6c81 100644 --- a/josh-core/src/link.rs +++ b/josh-core/src/link.rs @@ -1,6 +1,6 @@ pub fn find_link_files( src: &impl gix_object::Find, - tree: git2::Oid, + tree: gix_hash::ObjectId, ) -> anyhow::Result> { use crate::filter; use crate::objects; diff --git a/josh-core/tests/cache_lock.rs b/josh-core/tests/cache_lock.rs index 74d3938eb..1cd56273b 100644 --- a/josh-core/tests/cache_lock.rs +++ b/josh-core/tests/cache_lock.rs @@ -37,7 +37,8 @@ fn sled_lock_follows_transaction_lifetime() { let tree = git .find_tree(git.treebuilder(None).unwrap().write().unwrap()) .unwrap(); - let commit = git.commit(None, &sig, &sig, "c", &tree, &[]).unwrap(); + let commit = + josh_core::objects::gix_oid(git.commit(None, &sig, &sig, "c", &tree, &[]).unwrap()); transaction .insert(filter::parse(":/").unwrap(), commit, commit, true) .unwrap(); diff --git a/josh-gix-ext/src/graph.rs b/josh-gix-ext/src/graph.rs index f02d818c8..4d0438ea7 100644 --- a/josh-gix-ext/src/graph.rs +++ b/josh-gix-ext/src/graph.rs @@ -4,15 +4,13 @@ //! Both queries are merge-base computations: `gix_revision` walks the commits it reads from //! `objects`, keeping the answers consistent with what the caller can see. -use crate::{git2_oid, gix_oid}; - /// A commit the walk cannot read is indistinguishable from unrelated history, so the inputs /// are checked up front: asking about an object that is not there is a caller bug, not an /// answer of "no". -fn ensure_commit(objects: &impl gix_object::Find, oid: git2::Oid) -> anyhow::Result<()> { +fn ensure_commit(objects: &impl gix_object::Find, oid: gix_hash::ObjectId) -> anyhow::Result<()> { let mut buf = Vec::new(); let data = objects - .try_find(&gix_oid(oid), &mut buf) + .try_find(&oid, &mut buf) .map_err(|e| anyhow::anyhow!("{oid}: {e}"))? .ok_or_else(|| anyhow::anyhow!("object {oid} not found"))?; if data.kind != gix_object::Kind::Commit { @@ -29,18 +27,18 @@ fn ensure_commit(objects: &impl gix_object::Find, oid: git2::Oid) -> anyhow::Res /// descend from itself. pub fn is_descendant_of( objects: &impl gix_object::Find, - commit: git2::Oid, - ancestor: git2::Oid, + commit: gix_hash::ObjectId, + ancestor: gix_hash::ObjectId, ) -> anyhow::Result { if commit == ancestor { return Ok(false); } ensure_commit(objects, commit)?; ensure_commit(objects, ancestor)?; - let ancestor = gix_oid(ancestor); + let ancestor = ancestor; let mut graph = gix_revision::Graph::new(objects, None); // Reachable is exactly "is one of the best common ancestors of the two". - let bases = gix_revision::merge_base(gix_oid(commit), &[ancestor], &mut graph) + let bases = gix_revision::merge_base(commit, &[ancestor], &mut graph) .map_err(|e| anyhow::anyhow!("is_descendant_of: {e}"))?; Ok(bases.is_some_and(|bases| bases.iter().any(|base| *base == ancestor))) } @@ -49,15 +47,15 @@ pub fn is_descendant_of( /// several equally good candidates the choice among them is arbitrary. pub fn merge_base( objects: &impl gix_object::Find, - a: git2::Oid, - b: git2::Oid, -) -> anyhow::Result { + a: gix_hash::ObjectId, + b: gix_hash::ObjectId, +) -> anyhow::Result { ensure_commit(objects, a)?; ensure_commit(objects, b)?; let mut graph = gix_revision::Graph::new(objects, None); - gix_revision::merge_base(gix_oid(a), &[gix_oid(b)], &mut graph) + gix_revision::merge_base(a, &[b], &mut graph) .map_err(|e| anyhow::anyhow!("merge_base: {e}"))? - .map(|bases| git2_oid(bases.first())) + .map(|bases| bases.first().to_owned()) .ok_or_else(|| anyhow::anyhow!("{a} and {b} share no history")) } @@ -65,24 +63,25 @@ pub fn merge_base( /// share history. pub fn merge_base_octopus( objects: &impl gix_object::Find, - commits: &[git2::Oid], -) -> anyhow::Result> { + commits: &[gix_hash::ObjectId], +) -> anyhow::Result> { let (first, rest) = commits .split_first() .ok_or_else(|| anyhow::anyhow!("merge_base_octopus: no commits"))?; for commit in commits { ensure_commit(objects, *commit)?; } - let rest: Vec<_> = rest.iter().map(|c| gix_oid(*c)).collect(); + let rest = rest.to_vec(); let mut graph = gix_revision::Graph::new(objects, None); - let base = gix_revision::merge_base::octopus(gix_oid(*first), &rest, &mut graph) + let base = gix_revision::merge_base::octopus(*first, &rest, &mut graph) .map_err(|e| anyhow::anyhow!("merge_base_octopus: {e}"))?; - Ok(base.map(|id| git2_oid(&id))) + Ok(base) } #[cfg(test)] mod tests { use super::*; + use std::str::FromStr; struct TestRepo { _dir: tempfile::TempDir, @@ -98,7 +97,7 @@ mod tests { /// Commits are dated by their index so the merge-base walk sees a plausible history; /// equal timestamps would still be correct, just slower to settle. - fn commit(&self, seconds: i64, parents: &[git2::Oid]) -> git2::Oid { + fn commit(&self, seconds: i64, parents: &[gix_hash::ObjectId]) -> gix_hash::ObjectId { let sig = git2::Signature::new("Test", "test@example.com", &git2::Time::new(seconds, 0)) .unwrap(); @@ -106,12 +105,14 @@ mod tests { let tree = self.repo.find_tree(tree_id).unwrap(); let parents: Vec<_> = parents .iter() - .map(|&p| self.repo.find_commit(p).unwrap()) + .map(|p| self.repo.find_commit(crate::git2_oid(p)).unwrap()) .collect(); let parent_refs: Vec<&git2::Commit> = parents.iter().collect(); - self.repo - .commit(None, &sig, &sig, "c", &tree, &parent_refs) - .unwrap() + crate::gix_oid( + self.repo + .commit(None, &sig, &sig, "c", &tree, &parent_refs) + .unwrap(), + ) } } @@ -130,64 +131,6 @@ mod tests { assert!(!is_descendant_of(&objects, tip, tip).unwrap()); } - #[test] - fn merge_descends_from_both_sides_but_the_sides_do_not() { - let t = TestRepo::new(); - let root = t.commit(1000, &[]); - let left = t.commit(1001, &[root]); - let right = t.commit(1002, &[root]); - let merge = t.commit(1003, &[left, right]); - let odb = t.repo.odb().unwrap(); - let objects = crate::Git2Odb(&odb); - - assert!(is_descendant_of(&objects, merge, left).unwrap()); - assert!(is_descendant_of(&objects, merge, right).unwrap()); - assert!(!is_descendant_of(&objects, left, right).unwrap()); - assert!(!is_descendant_of(&objects, right, left).unwrap()); - } - - #[test] - fn unrelated_histories_never_descend() { - let t = TestRepo::new(); - let a = t.commit(1000, &[]); - let b = t.commit(1001, &[]); - let odb = t.repo.odb().unwrap(); - let objects = crate::Git2Odb(&odb); - - assert!(!is_descendant_of(&objects, a, b).unwrap()); - assert!(!is_descendant_of(&objects, b, a).unwrap()); - assert_eq!(merge_base_octopus(&objects, &[a, b]).unwrap(), None); - } - - #[test] - fn octopus_base_is_shared_by_every_input() { - let t = TestRepo::new(); - let root = t.commit(1000, &[]); - let a = t.commit(1001, &[root]); - let b = t.commit(1002, &[root]); - let c = t.commit(1003, &[a]); - let odb = t.repo.odb().unwrap(); - let objects = crate::Git2Odb(&odb); - - assert_eq!( - merge_base_octopus(&objects, &[c, b]).unwrap(), - Some(root), - "a fork joins at the root" - ); - assert_eq!( - merge_base_octopus(&objects, &[c, a]).unwrap(), - Some(a), - "an ancestor is its own best base" - ); - assert_eq!(merge_base_octopus(&objects, &[c]).unwrap(), Some(c)); - - let unrelated = t.commit(1004, &[]); - assert_eq!( - merge_base_octopus(&objects, &[c, b, unrelated]).unwrap(), - None - ); - } - #[test] fn merge_base_is_the_join_of_two_lineages() { let t = TestRepo::new(); @@ -211,9 +154,12 @@ mod tests { let root = t.commit(1000, &[]); let odb = t.repo.odb().unwrap(); let objects = crate::Git2Odb(&odb); - let missing = git2::Oid::from_str("1234567890123456789012345678901234567890").unwrap(); + let missing = + gix_hash::ObjectId::from_str("1234567890123456789012345678901234567890").unwrap(); assert!(is_descendant_of(&objects, missing, root).is_err()); assert!(merge_base_octopus(&objects, &[root, missing]).is_err()); + + assert!(merge_base_octopus(&objects, &[]).is_err()); } } diff --git a/josh-gix-ext/src/lib.rs b/josh-gix-ext/src/lib.rs index dbdda209d..2d87e218e 100644 --- a/josh-gix-ext/src/lib.rs +++ b/josh-gix-ext/src/lib.rs @@ -1,23 +1,4 @@ -//! In-memory object staging over a single object database. -//! -//! This is the transition vehicle for the incremental git2 -> gix port: all gix-object compute -//! (tree construction, commit parsing and serialization, hashing) works against this adapter, -//! which stages written objects in memory and reads through to the one repository object -//! database. At no point does a second repository handle perform I/O -- the lesson from the -//! reverted side-by-side gitoxide integration (cd6dc206) is that gix is used for pure in-memory -//! compute while a single ODB owns all I/O. -//! -//! Objects are staged as raw `(kind, bytes)` pairs keyed by their content hash, computed with -//! [`gix_object::compute_hash`] -- no repository access, no zlib, no filesystem. [`flush`] batch -//! writes the staged objects to the repository ODB at an explicit boundary, skipping objects that -//! already exist (on some platforms `exists()` is cheaper in terms of I/O than `write()`, because -//! `write()` updates the file access time in the loose object backend). -//! -//! The adapter implements [`gix_object::Find`] (and friends), so gix readers -- `TreeRef` -//! parsing, `CommitRefIter`, the tree editor, the topo walk -- see staged-but-unflushed objects -//! and disk objects through one interface. -//! -//! [`flush`]: StagingOdb::flush +//! Git object helpers over trait-based object stores. use std::collections::HashMap; @@ -31,8 +12,7 @@ pub use graph::{is_descendant_of, merge_base, merge_base_octopus}; pub use merge::{merge_commits, merge_trees}; pub use revwalk::{RangeWalk, RevWalk}; -/// Map the kind of a raw object between the two libraries. Infallible: both enums cover exactly -/// the four git object kinds. +/// Convert a gitoxide object kind to libgit2. pub fn git2_kind(kind: gix_object::Kind) -> git2::ObjectType { match kind { gix_object::Kind::Tree => git2::ObjectType::Tree, @@ -42,7 +22,7 @@ pub fn git2_kind(kind: gix_object::Kind) -> git2::ObjectType { } } -/// See [`git2_kind`]. Fails only for `Any`/`Ref`, which are not object kinds. +/// Convert a libgit2 object kind when it represents an object. pub fn gix_kind(kind: git2::ObjectType) -> Option { match kind { git2::ObjectType::Tree => Some(gix_object::Kind::Tree), @@ -53,31 +33,29 @@ pub fn gix_kind(kind: git2::ObjectType) -> Option { } } -/// Zero-cost oid conversion: both libraries use the same 20-byte binary representation. +/// Convert a SHA-1 object ID to gitoxide. pub fn gix_oid(oid: git2::Oid) -> gix_hash::ObjectId { gix_hash::ObjectId::from_bytes_or_panic(oid.as_bytes()) } -/// See [`gix_oid`]. +/// Convert a SHA-1 object ID to libgit2. pub fn git2_oid(oid: &gix_hash::oid) -> git2::Oid { git2::Oid::from_bytes(oid.as_bytes()).expect("oid sizes match") } -/// Hash `data` as a blob without writing it anywhere. -pub fn hash_blob(data: &[u8]) -> git2::Oid { - git2_oid( - &gix_object::compute_hash(gix_hash::Kind::Sha1, gix_object::Kind::Blob, data) - .expect("failed to compute hash"), - ) +/// Hash a blob without writing it. +pub fn hash_blob(data: &[u8]) -> gix_hash::ObjectId { + gix_object::compute_hash(gix_hash::Kind::Sha1, gix_object::Kind::Blob, data) + .expect("failed to compute hash") } /// Follow `oid` to the commit it names, unwrapping annotated tags on the way. Errors when the /// object is missing or resolves to something that is not a commit. pub fn peel_to_commit( src: &(impl gix_object::Find + ?Sized), - oid: git2::Oid, -) -> anyhow::Result { - let mut current = gix_oid(oid); + oid: gix_hash::ObjectId, +) -> anyhow::Result { + let mut current = oid; let mut buffer = Vec::new(); loop { let data = src @@ -85,7 +63,7 @@ pub fn peel_to_commit( .map_err(|e| anyhow::anyhow!("peel {}: {}", current, e))? .ok_or_else(|| anyhow::anyhow!("object {} not found", current))?; match data.kind { - gix_object::Kind::Commit => return Ok(git2_oid(¤t)), + gix_object::Kind::Commit => return Ok(current), gix_object::Kind::Tag => { current = gix_object::TagRefIter::from_bytes(&buffer, gix_hash::Kind::Sha1) .target_id()?; @@ -105,11 +83,11 @@ pub fn peel_to_commit( /// Errors when the object is missing or is not a tree. pub fn read_tree_entries( src: &(impl gix_object::Find + ?Sized), - oid: git2::Oid, + oid: gix_hash::ObjectId, ) -> anyhow::Result> { let mut buffer = Vec::new(); let data = src - .try_find(&gix_oid(oid), &mut buffer) + .try_find(&oid, &mut buffer) .map_err(|e| anyhow::anyhow!("read tree {}: {}", oid, e))? .ok_or_else(|| anyhow::anyhow!("object {} not found", oid))?; if data.kind != gix_object::Kind::Tree { @@ -126,7 +104,7 @@ pub fn read_tree_entries( /// entry on the way; the final entry may be of any kind. pub fn path_entry( src: &(impl gix_object::Find + ?Sized), - oid: git2::Oid, + oid: gix_hash::ObjectId, path: &std::path::Path, ) -> anyhow::Result> { let mut current = oid; @@ -134,7 +112,7 @@ pub fn path_entry( while let Some(component) = components.next() { let mut buffer = Vec::new(); let Some(data) = src - .try_find(&gix_oid(current), &mut buffer) + .try_find(¤t, &mut buffer) .map_err(|e| anyhow::anyhow!("read tree {}: {}", current, e))? else { return Ok(None); @@ -150,7 +128,7 @@ pub fn path_entry( if components.peek().is_none() { return Ok(Some((*entry).into())); } - current = git2_oid(entry.oid); + current = entry.oid.to_owned(); } Ok(None) } @@ -158,9 +136,9 @@ pub fn path_entry( /// The text of the blob `oid`, or `""` when it is missing, is not a blob, holds a NUL byte or /// is not valid UTF-8 -- the tolerance the display and script paths want, where a file that /// cannot be shown is the same as a file that is not there. -pub fn blob_text(src: &(impl gix_object::Find + ?Sized), oid: git2::Oid) -> String { +pub fn blob_text(src: &(impl gix_object::Find + ?Sized), oid: gix_hash::ObjectId) -> String { let mut buffer = Vec::new(); - let Ok(Some(data)) = src.try_find(&gix_oid(oid), &mut buffer) else { + let Ok(Some(data)) = src.try_find(&oid, &mut buffer) else { return String::new(); }; if data.kind != gix_object::Kind::Blob || buffer.contains(&0) { @@ -179,7 +157,7 @@ pub fn blob_text(src: &(impl gix_object::Find + ?Sized), oid: git2::Oid) -> Stri /// the merge commits it creates. pub fn walk_tree_preorder( src: &impl gix_object::Find, - root: git2::Oid, + root: gix_hash::ObjectId, cb: &mut dyn FnMut(&str, &gix_object::tree::EntryRef<'_>) -> anyhow::Result<()>, ) -> anyhow::Result<()> { let mut path = String::new(); @@ -188,13 +166,13 @@ pub fn walk_tree_preorder( fn walk_tree_preorder_inner( src: &impl gix_object::Find, - tree: git2::Oid, + tree: gix_hash::ObjectId, path: &mut String, cb: &mut dyn FnMut(&str, &gix_object::tree::EntryRef<'_>) -> anyhow::Result<()>, ) -> anyhow::Result<()> { let mut buf = Vec::new(); let data = src - .try_find(&gix_oid(tree), &mut buf) + .try_find(&tree, &mut buf) .map_err(|e| anyhow::anyhow!("walk_tree_preorder: {e}"))? .ok_or_else(|| anyhow::anyhow!("object {} not found", tree))?; if data.kind != gix_object::Kind::Tree { @@ -215,7 +193,7 @@ fn walk_tree_preorder_inner( path.push('/'); } path.push_str(name); - walk_tree_preorder_inner(src, git2_oid(entry.oid), path, cb)?; + walk_tree_preorder_inner(src, entry.oid.to_owned(), path, cb)?; path.truncate(base); } } @@ -233,7 +211,7 @@ fn walk_tree_preorder_inner( pub fn write_tree_now( out: &impl gix_object::Write, entries: Vec, -) -> anyhow::Result { +) -> anyhow::Result { // Exact-fit upper bound: mode (<= 6 octal digits) + space + name + NUL + 20 oid bytes. let mut buffer = Vec::with_capacity( entries @@ -258,15 +236,15 @@ pub fn write_tree_now( let id = out .write_buf(gix_object::Kind::Tree, &buffer) .map_err(|e| anyhow::anyhow!("write_tree_now: {e}"))?; - Ok(git2_oid(&id)) + Ok(id) } /// Write `data` as a blob to `out`. -pub fn write_blob(out: &impl gix_object::Write, data: &[u8]) -> anyhow::Result { +pub fn write_blob(out: &impl gix_object::Write, data: &[u8]) -> anyhow::Result { let id = out .write_buf(gix_object::Kind::Blob, data) .map_err(|e| anyhow::anyhow!("write_blob: {e}"))?; - Ok(git2_oid(&id)) + Ok(id) } /// Serialize a new commit and write it to `out`. Signatures carry the seconds and UTC @@ -274,15 +252,15 @@ pub fn write_blob(out: &impl gix_object::Write, data: &[u8]) -> anyhow::Result, committer: &git2::Signature<'_>, message: &str, -) -> anyhow::Result { +) -> anyhow::Result { let commit = gix_object::Commit { - tree: gix_oid(tree), - parents: parents.iter().map(|p| gix_oid(*p)).collect(), + tree, + parents: parents.to_vec().into(), author: gix_signature(author)?, committer: gix_signature(committer)?, encoding: None, @@ -294,7 +272,7 @@ pub fn write_commit( let id = out .write_buf(gix_object::Kind::Commit, &buffer) .map_err(|e| anyhow::anyhow!("write_commit: {e}"))?; - Ok(git2_oid(&id)) + Ok(id) } /// Serialize a new commit whose author and committer are taken from `base`, and write it to @@ -302,14 +280,14 @@ pub fn write_commit( pub fn write_commit_with_signatures_of( out: &impl gix_object::Write, base: &CommitData, - tree: git2::Oid, - parents: &[git2::Oid], + tree: gix_hash::ObjectId, + parents: &[gix_hash::ObjectId], message: &str, -) -> anyhow::Result { +) -> anyhow::Result { let parsed = base.parsed()?; let commit = gix_object::Commit { - tree: gix_oid(tree), - parents: parents.iter().map(|p| gix_oid(*p)).collect(), + tree, + parents: parents.to_vec().into(), author: parsed.author()?.into(), committer: parsed.committer()?.into(), encoding: None, @@ -321,7 +299,7 @@ pub fn write_commit_with_signatures_of( let id = out .write_buf(gix_object::Kind::Commit, &buffer) .map_err(|e| anyhow::anyhow!("write_commit: {e}"))?; - Ok(git2_oid(&id)) + Ok(id) } /// The gix spelling of a git2 signature: name and email verbatim, and the timestamp as @@ -343,17 +321,20 @@ pub fn gix_signature(sig: &git2::Signature<'_>) -> anyhow::Result` later without any signature change. #[derive(Clone, Debug)] pub struct CommitData { - id: git2::Oid, + id: gix_hash::ObjectId, bytes: Vec, } impl CommitData { /// Errors if the object is missing or not a commit. `src` is the transaction's facade in /// practice, so unflushed in-memory commits resolve. - pub fn read(src: &impl gix_object::Find, oid: git2::Oid) -> anyhow::Result { + pub fn read( + src: &impl gix_object::Find, + oid: gix_hash::ObjectId, + ) -> anyhow::Result { let mut bytes = Vec::new(); let data = src - .try_find(&gix_oid(oid), &mut bytes) + .try_find(&oid, &mut bytes) .map_err(|e| anyhow::anyhow!("CommitData::read: {e}"))? .ok_or_else(|| anyhow::anyhow!("object {} not found", oid))?; if data.kind != gix_object::Kind::Commit { @@ -366,7 +347,7 @@ impl CommitData { Ok(CommitData { id: oid, bytes }) } - pub fn id(&self) -> git2::Oid { + pub fn id(&self) -> gix_hash::ObjectId { self.id } @@ -384,10 +365,10 @@ impl CommitData { )?) } - pub fn tree_id(&self) -> anyhow::Result { + pub fn tree_id(&self) -> anyhow::Result { let id = gix_object::CommitRefIter::from_bytes(&self.bytes, gix_hash::Kind::Sha1).tree_id()?; - Ok(git2_oid(&id)) + Ok(id) } /// The stored message verbatim, by way of a full commit parse -- an unparseable commit @@ -413,13 +394,11 @@ impl CommitData { /// Binary parent ids read from the parsed commit header id array via /// `CommitRefIter::parent_ids`; never does odb lookups. - pub fn parent_ids(&self) -> impl Iterator + '_ { - gix_object::CommitRefIter::from_bytes(&self.bytes, gix_hash::Kind::Sha1) - .parent_ids() - .map(|p| git2_oid(&p)) + pub fn parent_ids(&self) -> impl Iterator + '_ { + gix_object::CommitRefIter::from_bytes(&self.bytes, gix_hash::Kind::Sha1).parent_ids() } - pub fn first_parent_id(&self) -> Option { + pub fn first_parent_id(&self) -> Option { self.parent_ids().next() } @@ -646,33 +625,20 @@ impl gix_object::Write for Git2Odb<'_> { mod tests { use super::*; - #[test] - fn hash_blob_matches_git2() { - for data in [&b""[..], b"x", b"1:{\"a\":1}\n"] { - assert_eq!( - super::hash_blob(data), - git2::Oid::hash_object(git2::ObjectType::Blob, data).unwrap() - ); - } - assert_eq!( - super::hash_blob(b"").to_string(), - "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391" - ); - } - - /// Write a commit with `message` verbatim through the raw odb, so non-UTF-8 messages can - /// be expressed too. - fn commit_with_message(repo: &git2::Repository, message: &[u8]) -> git2::Oid { + /// Write raw commit bytes, including non-UTF-8 messages. + fn commit_with_message(repo: &git2::Repository, message: &[u8]) -> gix_hash::ObjectId { let tree = repo.treebuilder(None).unwrap().write().unwrap(); let mut data = Vec::new(); data.extend_from_slice(format!("tree {}\n", tree).as_bytes()); data.extend_from_slice(b"author t 0 +0000\n"); data.extend_from_slice(b"committer t 0 +0000\n\n"); data.extend_from_slice(message); - repo.odb() - .unwrap() - .write(git2::ObjectType::Commit, &data) - .unwrap() + gix_oid( + repo.odb() + .unwrap() + .write(git2::ObjectType::Commit, &data) + .unwrap(), + ) } /// Commits written here must be byte-identical to the ones libgit2 writes for the same @@ -684,17 +650,17 @@ mod tests { let repo = git2::Repository::init_bare(dir.path()).unwrap(); let odb = repo.odb().unwrap(); - let tree = repo.treebuilder(None).unwrap().write().unwrap(); + let tree = gix_oid(repo.treebuilder(None).unwrap().write().unwrap()); let blob = repo.blob(b"x").unwrap(); let mut b = repo.treebuilder(None).unwrap(); b.insert("f", blob, 0o100644).unwrap(); - let tree2 = b.write().unwrap(); + let tree2 = gix_oid(b.write().unwrap()); let sig = |name: &str, email: &str, secs: i64, offset: i32| { git2::Signature::new(name, email, &git2::Time::new(secs, offset)).unwrap() }; - let cases: Vec<(git2::Signature, git2::Signature, &str, git2::Oid)> = vec![ + let cases: Vec<(git2::Signature, git2::Signature, &str, gix_hash::ObjectId)> = vec![ ( sig("A", "a@e", 0, 0), sig("A", "a@e", 0, 0), @@ -719,30 +685,33 @@ mod tests { for (author, committer, message, tree) in &cases { for parents in [vec![], vec![0usize], vec![0, 1]] { // Build the parent commits with git2 so both writers see identical inputs. - let parent_ids: Vec = parents + let parent_ids: Vec = parents .iter() .map(|i| { - let t = repo.find_tree(*tree).unwrap(); - repo.commit(None, author, committer, &format!("parent {i}"), &t, &[]) - .unwrap() + let t = repo.find_tree(git2_oid(tree)).unwrap(); + gix_oid( + repo.commit(None, author, committer, &format!("parent {i}"), &t, &[]) + .unwrap(), + ) }) .collect(); let parent_commits: Vec = parent_ids .iter() - .map(|id| repo.find_commit(*id).unwrap()) + .map(|id| repo.find_commit(git2_oid(id)).unwrap()) .collect(); let parent_refs: Vec<&git2::Commit> = parent_commits.iter().collect(); - let want = repo - .commit( + let want = gix_oid( + repo.commit( None, author, committer, message, - &repo.find_tree(*tree).unwrap(), + &repo.find_tree(git2_oid(tree)).unwrap(), &parent_refs, ) - .unwrap(); + .unwrap(), + ); let got = write_commit( &Git2Odb(&odb), *tree, @@ -804,9 +773,9 @@ mod tests { let dir = tempfile::tempdir().unwrap(); let repo = git2::Repository::init_bare(dir.path()).unwrap(); let odb = repo.odb().unwrap(); - let blob = repo.blob(b"x").unwrap(); + let blob = gix_oid(repo.blob(b"x").unwrap()); - let write_tree = |entries: &[(&str, &str, git2::Oid)]| -> git2::Oid { + let write_tree = |entries: &[(&str, &str, gix_hash::ObjectId)]| -> gix_hash::ObjectId { let mut data = Vec::new(); for (mode, name, oid) in entries { data.extend_from_slice(mode.as_bytes()); @@ -815,10 +784,12 @@ mod tests { data.push(0); data.extend_from_slice(oid.as_bytes()); } - repo.odb() - .unwrap() - .write(git2::ObjectType::Tree, &data) - .unwrap() + gix_oid( + repo.odb() + .unwrap() + .write(git2::ObjectType::Tree, &data) + .unwrap(), + ) }; let deep = write_tree(&[("100644", "leaf.txt", blob)]); @@ -862,21 +833,22 @@ mod tests { let dir = tempfile::tempdir().unwrap(); let repo = git2::Repository::init_bare(dir.path()).unwrap(); let odb = repo.odb().unwrap(); - let blob = repo.blob(b"x").unwrap(); + let blob = gix_oid(repo.blob(b"x").unwrap()); let mut inner = repo.treebuilder(None).unwrap(); - inner.insert("f.txt", blob, 0o100644).unwrap(); + inner.insert("f.txt", git2_oid(&blob), 0o100644).unwrap(); let inner = inner.write().unwrap(); let mut data = Vec::new(); data.extend_from_slice(b"40000 bad\xff"); data.push(0); data.extend_from_slice(inner.as_bytes()); - let root = repo - .odb() - .unwrap() - .write(git2::ObjectType::Tree, &data) - .unwrap(); + let root = gix_oid( + repo.odb() + .unwrap() + .write(git2::ObjectType::Tree, &data) + .unwrap(), + ); assert!(walk_tree_preorder(&Git2Odb(&odb), root, &mut |_, _| Ok(())).is_err()); } diff --git a/josh-gix-ext/src/merge.rs b/josh-gix-ext/src/merge.rs index 27d4c2eca..874535ea1 100644 --- a/josh-gix-ext/src/merge.rs +++ b/josh-gix-ext/src/merge.rs @@ -4,8 +4,6 @@ //! josh merges bare trees: there is no worktree to read from, no attributes to consult and no //! external merge drivers to run, so the platforms below are the empty configuration of each. -use crate::{git2_oid, gix_oid}; - /// Which side wins a conflicting hunk. #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum Favor { @@ -107,8 +105,8 @@ fn options(favor: Option) -> gix_merge::tree::Options { fn write_result( objects: &impl gix_object::Write, mut outcome: gix_merge::tree::Outcome<'_>, - labels: (git2::Oid, git2::Oid), -) -> anyhow::Result { + labels: (gix_hash::ObjectId, gix_hash::ObjectId), +) -> anyhow::Result { let unresolved = gix_merge::tree::TreatAsUnresolved::default(); if outcome.has_unresolved_conflicts(unresolved) { let paths: Vec<_> = outcome @@ -128,21 +126,21 @@ fn write_result( .tree .write(|tree| gix_object::Write::write(objects, tree)) .map_err(|e| anyhow::anyhow!("writing merge result: {e}"))?; - Ok(git2_oid(&id)) + Ok(id) } /// Merge `ours` and `theirs` against their common ancestor `base`, all trees. pub fn merge_trees( objects: &(impl gix_object::FindObjectOrHeader + gix_object::Write), - base: git2::Oid, - ours: git2::Oid, - theirs: git2::Oid, -) -> anyhow::Result { + base: gix_hash::ObjectId, + ours: gix_hash::ObjectId, + theirs: gix_hash::ObjectId, +) -> anyhow::Result { let mut platforms = platforms(); let outcome = gix_merge::tree( - &gix_oid(base), - &gix_oid(ours), - &gix_oid(theirs), + &base, + &ours, + &theirs, gix_merge::blob::builtin_driver::text::Labels::default(), objects, |buf| gix_object::Write::write_buf(objects, gix_object::Kind::Blob, buf), @@ -159,15 +157,15 @@ pub fn merge_trees( /// conflicts no side preference can resolve. pub fn merge_commits( objects: &(impl gix_object::FindObjectOrHeader + gix_object::Write), - ours: git2::Oid, - theirs: git2::Oid, + ours: gix_hash::ObjectId, + theirs: gix_hash::ObjectId, favor: Option, -) -> anyhow::Result { +) -> anyhow::Result { let mut platforms = platforms(); let mut graph = gix_revwalk::Graph::new(objects, None); let outcome = gix_merge::commit( - gix_oid(ours), - gix_oid(theirs), + ours, + theirs, gix_merge::blob::builtin_driver::text::Labels::default(), &mut graph, &mut platforms.diff, @@ -199,33 +197,39 @@ mod tests { TestRepo { _dir: dir, repo } } - fn tree(&self, files: &[(&str, &str)]) -> git2::Oid { + fn tree(&self, files: &[(&str, &str)]) -> gix_hash::ObjectId { let mut builder = self.repo.treebuilder(None).unwrap(); for (name, content) in files { let blob = self.repo.blob(content.as_bytes()).unwrap(); builder.insert(name, blob, 0o100644).unwrap(); } - builder.write().unwrap() + crate::gix_oid(builder.write().unwrap()) } - fn commit(&self, tree: git2::Oid, parents: &[git2::Oid]) -> git2::Oid { + fn commit( + &self, + tree: gix_hash::ObjectId, + parents: &[gix_hash::ObjectId], + ) -> gix_hash::ObjectId { let sig = git2::Signature::new("Test", "test@example.com", &git2::Time::new(1000, 0)) .unwrap(); - let tree = self.repo.find_tree(tree).unwrap(); + let tree = self.repo.find_tree(crate::git2_oid(&tree)).unwrap(); let parents: Vec<_> = parents .iter() - .map(|&p| self.repo.find_commit(p).unwrap()) + .map(|p| self.repo.find_commit(crate::git2_oid(p)).unwrap()) .collect(); let parent_refs: Vec<&git2::Commit> = parents.iter().collect(); - self.repo - .commit(None, &sig, &sig, "c", &tree, &parent_refs) - .unwrap() + crate::gix_oid( + self.repo + .commit(None, &sig, &sig, "c", &tree, &parent_refs) + .unwrap(), + ) } - fn file(&self, tree: git2::Oid, name: &str) -> String { + fn file(&self, tree: gix_hash::ObjectId, name: &str) -> String { let entry = self .repo - .find_tree(tree) + .find_tree(crate::git2_oid(&tree)) .unwrap() .get_name(name) .unwrap_or_else(|| panic!("{name} not in tree")) @@ -234,24 +238,6 @@ mod tests { } } - /// Edits on different lines of one file merge into a single file holding both, and each - /// side's other changes carry over. - #[test] - fn non_overlapping_edits_merge_cleanly() { - let t = TestRepo::new(); - let base = t.tree(&[("a", "1\n2\n3\n"), ("b", "keep\n")]); - let ours = t.tree(&[("a", "one\n2\n3\n"), ("b", "keep\n"), ("new", "ours\n")]); - let theirs = t.tree(&[("a", "1\n2\nthree\n"), ("b", "keep\n")]); - let odb = t.repo.odb().unwrap(); - let objects = crate::Git2Odb(&odb); - - let merged = merge_trees(&objects, base, ours, theirs).unwrap(); - - assert_eq!(t.file(merged, "a"), "one\n2\nthree\n"); - assert_eq!(t.file(merged, "b"), "keep\n"); - assert_eq!(t.file(merged, "new"), "ours\n"); - } - #[test] fn overlapping_edits_conflict() { let t = TestRepo::new(); @@ -283,8 +269,6 @@ mod tests { assert_eq!(t.file(merged, "a"), "theirs\n"); } - /// The conflicts josh reports are the ones no side preference can decide, which is what - /// makes a favored merge fail at all. #[test] fn a_favor_cannot_decide_delete_against_modify() { let t = TestRepo::new(); @@ -299,19 +283,4 @@ mod tests { .to_string(); assert!(err.contains("conflicts in a"), "{err}"); } - - #[test] - fn commits_merge_across_their_common_ancestor() { - let t = TestRepo::new(); - let base = t.commit(t.tree(&[("a", "1\n"), ("b", "1\n")]), &[]); - let ours = t.commit(t.tree(&[("a", "2\n"), ("b", "1\n")]), &[base]); - let ours = t.commit(t.tree(&[("a", "3\n"), ("b", "1\n")]), &[ours]); - let theirs = t.commit(t.tree(&[("a", "1\n"), ("b", "2\n")]), &[base]); - let odb = t.repo.odb().unwrap(); - let objects = crate::Git2Odb(&odb); - - let merged = merge_commits(&objects, ours, theirs, Some(Favor::Ours)).unwrap(); - assert_eq!(t.file(merged, "a"), "3\n"); - assert_eq!(t.file(merged, "b"), "2\n"); - } } diff --git a/josh-gix-ext/src/revwalk.rs b/josh-gix-ext/src/revwalk.rs index 066888de2..9566b45e5 100644 --- a/josh-gix-ext/src/revwalk.rs +++ b/josh-gix-ext/src/revwalk.rs @@ -26,9 +26,7 @@ use std::collections::HashMap; use std::ops::ControlFlow; -use crate::git2_oid; - -type Visit<'v> = &'v mut dyn FnMut(git2::Oid) -> anyhow::Result>; +type Visit<'v> = &'v mut dyn FnMut(gix_hash::ObjectId) -> anyhow::Result>; /// Read a commit's parent ids: one raw object read, one `CommitRefIter` pass /// stopped at the first non-tree/parent header. A missing or non-commit @@ -36,11 +34,11 @@ type Visit<'v> = &'v mut dyn FnMut(git2::Oid) -> anyhow::Result> /// message) are never read or validated. fn read_parent_oids( src: &impl gix_object::Find, - oid: git2::Oid, + oid: gix_hash::ObjectId, buf: &mut Vec, -) -> anyhow::Result> { +) -> anyhow::Result> { let data = src - .try_find(&crate::gix_oid(oid), buf) + .try_find(&oid, buf) .map_err(|e| anyhow::anyhow!("read_parent_oids: {e}"))? .ok_or_else(|| anyhow::anyhow!("object {} not found", oid))?; if data.kind != gix_object::Kind::Commit { @@ -51,7 +49,7 @@ fn read_parent_oids( use gix_object::commit::ref_iter::Token; match token? { Token::Tree { .. } => {} - Token::Parent { id } => parents.push(git2_oid(&id)), + Token::Parent { id } => parents.push(id), _ => break, } } @@ -59,9 +57,9 @@ fn read_parent_oids( } /// Verify `oid` names a commit without decompressing it. -fn ensure_commit(src: &impl gix_object::FindHeader, oid: git2::Oid) -> anyhow::Result<()> { +fn ensure_commit(src: &impl gix_object::FindHeader, oid: gix_hash::ObjectId) -> anyhow::Result<()> { let header = src - .try_header(&crate::gix_oid(oid)) + .try_header(&oid) .map_err(|e| anyhow::anyhow!("ensure_commit: {e}"))? .ok_or_else(|| anyhow::anyhow!("object {} not found", oid))?; if header.kind != gix_object::Kind::Commit { @@ -72,7 +70,7 @@ fn ensure_commit(src: &impl gix_object::FindHeader, oid: git2::Oid) -> anyhow::R /// Per-commit state of a [`RevWalk`]. struct Node { - oid: git2::Oid, + oid: gix_hash::ObjectId, /// Parents as node indices, filled in on first visit. parents: Vec, parsed: bool, @@ -107,7 +105,7 @@ struct Node { pub struct RevWalk<'a, S> { odb: &'a S, nodes: Vec, - by_oid: HashMap, + by_oid: HashMap, /// Pushed tips in push order; the DFS explores them in this order. tips: Vec, first_parent: bool, @@ -129,7 +127,7 @@ impl<'a, S: gix_object::Find + gix_object::FindHeader> RevWalk<'a, S> { /// Mark a commit to start traversal from. A missing oid or a non-commit /// object errors immediately. - pub fn push(&mut self, tip: git2::Oid) -> anyhow::Result<()> { + pub fn push(&mut self, tip: gix_hash::ObjectId) -> anyhow::Result<()> { ensure_commit(self.odb, tip)?; let tip = self.intern(tip); self.tips.push(tip); @@ -145,8 +143,8 @@ impl<'a, S: gix_object::Find + gix_object::FindHeader> RevWalk<'a, S> { /// parents); iterate the result back to front for parents-first order. pub fn into_topo_vec( mut self, - mut prune: impl FnMut(git2::Oid) -> bool, - ) -> anyhow::Result> { + mut prune: impl FnMut(gix_hash::ObjectId) -> bool, + ) -> anyhow::Result> { let post = self.dfs(&mut prune, &mut None)?; Ok(post.into_iter().rev().map(|i| self.nodes[i].oid).collect()) } @@ -156,16 +154,16 @@ impl<'a, S: gix_object::Find + gix_object::FindHeader> RevWalk<'a, S> { /// [`ControlFlow::Break`] to abort the walk. pub fn discover( mut self, - mut visit: impl FnMut(git2::Oid) -> anyhow::Result>, + mut visit: impl FnMut(gix_hash::ObjectId) -> anyhow::Result>, ) -> anyhow::Result<()> { - let mut keep_all = |_: git2::Oid| false; + let mut keep_all = |_: gix_hash::ObjectId| false; let visit: Visit<'_> = &mut visit; self.dfs(&mut keep_all, &mut Some(visit))?; Ok(()) } /// The node index for `oid`, creating blank unparsed state on first sight. - fn intern(&mut self, oid: git2::Oid) -> usize { + fn intern(&mut self, oid: gix_hash::ObjectId) -> usize { if let Some(&i) = self.by_oid.get(&oid) { return i; } @@ -200,7 +198,7 @@ impl<'a, S: gix_object::Find + gix_object::FindHeader> RevWalk<'a, S> { /// `Break` aborts the walk with the partial result. fn dfs( &mut self, - prune: &mut dyn FnMut(git2::Oid) -> bool, + prune: &mut dyn FnMut(gix_hash::ObjectId) -> bool, visit: &mut Option>, ) -> anyhow::Result> { enum Frame { @@ -248,7 +246,7 @@ const FAST_FORWARD_PROBE_LIMIT: usize = 1000; /// Per-commit state of a [`RangeWalk`]. struct RangeNode { - oid: git2::Oid, + oid: gix_hash::ObjectId, /// Parents as node indices, filled in on first visit. parents: Vec, parsed: bool, @@ -292,9 +290,9 @@ struct RangeNode { /// because the callers consume the whole yield set parents-first. pub struct RangeWalk<'a, S> { odb: &'a S, - sequence_numbers: Box anyhow::Result + 'a>, + sequence_numbers: Box anyhow::Result + 'a>, nodes: Vec, - by_oid: HashMap, + by_oid: HashMap, /// Reused per-commit read buffer, so a walk does one allocation, not one per commit. scratch: Vec, } @@ -302,7 +300,7 @@ pub struct RangeWalk<'a, S> { impl<'a, S: gix_object::Find + gix_object::FindHeader> RangeWalk<'a, S> { pub fn new( odb: &'a S, - sequence_numbers: impl Fn(git2::Oid) -> anyhow::Result + 'a, + sequence_numbers: impl Fn(gix_hash::ObjectId) -> anyhow::Result + 'a, ) -> Self { RangeWalk { odb, @@ -320,9 +318,9 @@ impl<'a, S: gix_object::Find + gix_object::FindHeader> RangeWalk<'a, S> { /// actually reaches them. pub fn into_topo_vec( mut self, - tip: git2::Oid, - base: git2::Oid, - ) -> anyhow::Result> { + tip: gix_hash::ObjectId, + base: gix_hash::ObjectId, + ) -> anyhow::Result> { ensure_commit(self.odb, tip)?; ensure_commit(self.odb, base)?; if tip == base { @@ -338,7 +336,7 @@ impl<'a, S: gix_object::Find + gix_object::FindHeader> RangeWalk<'a, S> { } /// The node index for `oid`, creating blank unparsed state on first sight. - fn intern(&mut self, oid: git2::Oid) -> usize { + fn intern(&mut self, oid: gix_hash::ObjectId) -> usize { if let Some(&i) = self.by_oid.get(&oid) { return i; } @@ -377,7 +375,7 @@ impl<'a, S: gix_object::Find + gix_object::FindHeader> RangeWalk<'a, S> { &mut self, tip: usize, base: usize, - ) -> anyhow::Result>> { + ) -> anyhow::Result>> { let mut out = Vec::new(); let mut c = tip; for _ in 0..FAST_FORWARD_PROBE_LIMIT { @@ -399,7 +397,7 @@ impl<'a, S: gix_object::Find + gix_object::FindHeader> RangeWalk<'a, S> { /// The ranked strategy (see the type docs): a max-heap frontier keyed by /// sequence number, ties resolved by insertion order (equal numbers are /// never ancestor-related, so any deterministic order is correct). - fn ranked_walk(&mut self, tip: usize, base: usize) -> anyhow::Result> { + fn ranked_walk(&mut self, tip: usize, base: usize) -> anyhow::Result> { let seq_of = std::mem::replace(&mut self.sequence_numbers, Box::new(|_| unreachable!())); let mut heap: std::collections::BinaryHeap<(u64, std::cmp::Reverse, usize)> = std::collections::BinaryHeap::new(); @@ -467,6 +465,7 @@ impl<'a, S: gix_object::Find + gix_object::FindHeader> RangeWalk<'a, S> { mod tests { use super::*; use std::collections::HashSet; + use std::str::FromStr; struct TestRepo { _dir: tempfile::TempDir, @@ -480,24 +479,26 @@ mod tests { TestRepo { _dir: dir, repo } } - fn commit(&self, msg: &str, parents: &[git2::Oid]) -> git2::Oid { + fn commit(&self, msg: &str, parents: &[gix_hash::ObjectId]) -> gix_hash::ObjectId { let sig = git2::Signature::new("Test", "test@example.com", &git2::Time::new(1000, 0)) .unwrap(); let tree_id = self.repo.treebuilder(None).unwrap().write().unwrap(); let tree = self.repo.find_tree(tree_id).unwrap(); let parents: Vec<_> = parents .iter() - .map(|&p| self.repo.find_commit(p).unwrap()) + .map(|p| self.repo.find_commit(crate::git2_oid(p)).unwrap()) .collect(); let parent_refs: Vec<&git2::Commit> = parents.iter().collect(); - self.repo - .commit(None, &sig, &sig, msg, &tree, &parent_refs) - .unwrap() + crate::gix_oid( + self.repo + .commit(None, &sig, &sig, msg, &tree, &parent_refs) + .unwrap(), + ) } /// Write raw commit bytes so parents don't have to exist in the odb /// (and may repeat -- git2's commit builder can produce neither). - fn raw_commit(&self, msg: &str, parents: &[git2::Oid]) -> git2::Oid { + fn raw_commit(&self, msg: &str, parents: &[gix_hash::ObjectId]) -> gix_hash::ObjectId { let tree_id = self.repo.treebuilder(None).unwrap().write().unwrap(); let mut buf = format!("tree {}\n", tree_id); for p in parents { @@ -507,11 +508,13 @@ mod tests { "author Test 1000 +0000\n\ committer Test 1000 +0000\n\n{msg}\n" )); - self.repo - .odb() - .unwrap() - .write(git2::ObjectType::Commit, buf.as_bytes()) - .unwrap() + crate::gix_oid( + self.repo + .odb() + .unwrap() + .write(git2::ObjectType::Commit, buf.as_bytes()) + .unwrap(), + ) } } @@ -520,13 +523,17 @@ mod tests { /// Errors on missing ancestors, like the real lookup. fn exact_seq( repo: &git2::Repository, - memo: &std::cell::RefCell>, - oid: git2::Oid, + memo: &std::cell::RefCell>, + oid: gix_hash::ObjectId, ) -> anyhow::Result { if let Some(&s) = memo.borrow().get(&oid) { return Ok(s); } - let parents: Vec = repo.find_commit(oid)?.parent_ids().collect(); + let parents: Vec = repo + .find_commit(crate::git2_oid(&oid))? + .parent_ids() + .map(crate::gix_oid) + .collect(); let mut max = 0; for p in parents { max = max.max(exact_seq(repo, memo, p)?); @@ -542,24 +549,27 @@ mod tests { /// no code with the walkers. fn reference_set( repo: &git2::Repository, - tips: &[git2::Oid], - base: Option, - pruned: &HashSet, + tips: &[gix_hash::ObjectId], + base: Option, + pruned: &HashSet, first_parent: bool, - ) -> HashSet { - let parents_of = |oid: git2::Oid| -> Vec { - let c = repo.find_commit(oid).unwrap(); - c.parent_ids().collect() + ) -> HashSet { + let parents_of = |oid: gix_hash::ObjectId| -> Vec { + repo.find_commit(crate::git2_oid(&oid)) + .unwrap() + .parent_ids() + .map(crate::gix_oid) + .collect() }; - let mut hidden: HashSet = HashSet::new(); - let mut stack: Vec = base.into_iter().collect(); + let mut hidden: HashSet = HashSet::new(); + let mut stack: Vec = base.into_iter().collect(); while let Some(h) = stack.pop() { if hidden.insert(h) { stack.extend(parents_of(h)); } } let mut out = HashSet::new(); - let mut stack: Vec = tips.to_vec(); + let mut stack: Vec = tips.to_vec(); while let Some(c) = stack.pop() { if out.contains(&c) || hidden.contains(&c) || pruned.contains(&c) { continue; @@ -576,10 +586,10 @@ mod tests { fn rev_walk( repo: &git2::Repository, - tips: &[git2::Oid], - pruned: &HashSet, + tips: &[gix_hash::ObjectId], + pruned: &HashSet, first_parent: bool, - ) -> anyhow::Result> { + ) -> anyhow::Result> { let odb = repo.odb().unwrap(); let odb = crate::Git2Odb(&odb); let mut walk = RevWalk::new(&odb); @@ -594,9 +604,9 @@ mod tests { fn range_walk( repo: &git2::Repository, - tip: git2::Oid, - base: git2::Oid, - ) -> anyhow::Result> { + tip: gix_hash::ObjectId, + base: gix_hash::ObjectId, + ) -> anyhow::Result> { let memo = std::cell::RefCell::new(HashMap::new()); let odb = repo.odb().unwrap(); let odb = crate::Git2Odb(&odb); @@ -606,12 +616,19 @@ mod tests { /// Check a yielded order for topological validity over the followed /// edges: every yielded commit precedes its yielded parents. - fn assert_topo(repo: &git2::Repository, got: &[git2::Oid], first_parent: bool, mode: &str) { - let pos: HashMap = got.iter().enumerate().map(|(i, &o)| (o, i)).collect(); + fn assert_topo( + repo: &git2::Repository, + got: &[gix_hash::ObjectId], + first_parent: bool, + mode: &str, + ) { + let pos: HashMap = + got.iter().enumerate().map(|(i, &o)| (o, i)).collect(); assert_eq!(pos.len(), got.len(), "duplicate yields: {mode}"); for &c in got { - let commit = repo.find_commit(c).unwrap(); - let mut parents: Vec = commit.parent_ids().collect(); + let commit = repo.find_commit(crate::git2_oid(&c)).unwrap(); + let mut parents: Vec = + commit.parent_ids().map(crate::gix_oid).collect(); if first_parent { parents.truncate(1); } @@ -628,15 +645,15 @@ mod tests { /// equality with `git2::Revwalk`. fn assert_rev_walk( repo: &git2::Repository, - tips: &[git2::Oid], - pruned: &HashSet, + tips: &[gix_hash::ObjectId], + pruned: &HashSet, label: &str, ) { for first_parent in [false, true] { let mode = format!("{label} first_parent={first_parent}"); let got = rev_walk(repo, tips, pruned, first_parent).unwrap(); let want = reference_set(repo, tips, None, pruned, first_parent); - let got_set: HashSet = got.iter().copied().collect(); + let got_set: HashSet = got.iter().copied().collect(); assert_eq!(got_set, want, "yield set mismatch: {mode}"); assert_topo(repo, &got, first_parent, &mode); let again = rev_walk(repo, tips, pruned, first_parent).unwrap(); @@ -648,10 +665,11 @@ mod tests { if first_parent { g2.simplify_first_parent().unwrap(); } - for &t in tips { - g2.push(t).unwrap(); + for t in tips { + g2.push(crate::git2_oid(t)).unwrap(); } - let theirs: HashSet = g2.map(|r| r.unwrap()).collect(); + let theirs: HashSet = + g2.map(|r| crate::gix_oid(r.unwrap())).collect(); assert_eq!(got_set, theirs, "git2 set mismatch: {mode}"); } } @@ -659,17 +677,22 @@ mod tests { /// Assert [`RangeWalk`] against the reference model: exact yield set, /// valid topological order, run-to-run determinism. - fn assert_range_walk(repo: &git2::Repository, tip: git2::Oid, base: git2::Oid, label: &str) { + fn assert_range_walk( + repo: &git2::Repository, + tip: gix_hash::ObjectId, + base: gix_hash::ObjectId, + label: &str, + ) { let got = range_walk(repo, tip, base).unwrap(); let want = reference_set(repo, &[tip], Some(base), &HashSet::new(), false); - let got_set: HashSet = got.iter().copied().collect(); + let got_set: HashSet = got.iter().copied().collect(); assert_eq!(got_set, want, "yield set mismatch: {label}"); assert_topo(repo, &got, false, label); let again = range_walk(repo, tip, base).unwrap(); assert_eq!(got, again, "non-deterministic order: {label}"); } - fn no_prune() -> HashSet { + fn no_prune() -> HashSet { HashSet::new() } @@ -689,10 +712,13 @@ mod tests { } // Forward order is tip-first down the chain. let got = range_walk(&t.repo, tip, all[5]).unwrap(); - let expect: Vec = all[6..].iter().rev().copied().collect(); + let expect: Vec = all[6..].iter().rev().copied().collect(); assert_eq!(got, expect); // An empty range yields nothing. - assert_eq!(range_walk(&t.repo, tip, tip).unwrap(), vec![]); + assert_eq!( + range_walk(&t.repo, tip, tip).unwrap(), + Vec::::new() + ); } #[test] @@ -756,7 +782,7 @@ mod tests { } let tip = t.commit("tip", &[base, b]); assert_range_walk(&t.repo, tip, base, "merged-side-branch"); - let got: HashSet = range_walk(&t.repo, tip, base) + let got: HashSet = range_walk(&t.repo, tip, base) .unwrap() .into_iter() .collect(); @@ -899,7 +925,7 @@ mod tests { false }) .unwrap(); - let unique: HashSet = calls.iter().copied().collect(); + let unique: HashSet = calls.iter().copied().collect(); assert_eq!(calls.len(), unique.len(), "prune consulted once per commit"); } @@ -938,18 +964,20 @@ mod tests { fn input_error_cases() { let t = TestRepo::new(); let a = t.commit("a", &[]); - let missing = git2::Oid::from_str("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef").unwrap(); - let blob = t - .repo - .odb() - .unwrap() - .write(git2::ObjectType::Blob, b"x") - .unwrap(); - let tree = t.repo.treebuilder(None).unwrap().write().unwrap(); + let missing = + gix_hash::ObjectId::from_str("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef").unwrap(); + let blob = crate::gix_oid( + t.repo + .odb() + .unwrap() + .write(git2::ObjectType::Blob, b"x") + .unwrap(), + ); + let tree = crate::gix_oid(t.repo.treebuilder(None).unwrap().write().unwrap()); let sig = git2::Signature::new("Test", "test@example.com", &git2::Time::new(1000, 0)).unwrap(); - let obj = t.repo.find_object(a, None).unwrap(); - let tag = t.repo.tag("v1", &obj, &sig, "annotated", false).unwrap(); + let obj = t.repo.find_object(crate::git2_oid(&a), None).unwrap(); + let tag = crate::gix_oid(t.repo.tag("v1", &obj, &sig, "annotated", false).unwrap()); let odb = t.repo.odb().unwrap(); let odb = crate::Git2Odb(&odb); @@ -967,7 +995,8 @@ mod tests { #[test] fn missing_ancestors_error_only_when_visited() { let t = TestRepo::new(); - let missing = git2::Oid::from_str("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef").unwrap(); + let missing = + gix_hash::ObjectId::from_str("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef").unwrap(); let dangling = t.raw_commit("dangling", &[missing]); // A pushed commit with a missing parent errors during the walk. @@ -1000,12 +1029,13 @@ mod tests { let t = TestRepo::new(); let tree_id = t.repo.treebuilder(None).unwrap().write().unwrap(); let odb = t.repo.odb().unwrap(); - let trunc = odb - .write( + let trunc = crate::gix_oid( + odb.write( git2::ObjectType::Commit, format!("tree {}\n", tree_id).as_bytes(), ) - .unwrap(); + .unwrap(), + ); let odb = crate::Git2Odb(&odb); let tip = t.raw_commit("tip", &[trunc]); let mut w = RevWalk::new(&odb); @@ -1095,9 +1125,9 @@ mod tests { let mut rng = Rng(seed); let t = TestRepo::new(); let n = 20 + rng.below(30); - let mut commits: Vec = Vec::new(); + let mut commits: Vec = Vec::new(); for i in 0..n { - let parents: Vec = if commits.is_empty() || rng.below(12) == 0 { + let parents: Vec = if commits.is_empty() || rng.below(12) == 0 { vec![] } else { let k = 1 + rng.below(3.min(commits.len())); @@ -1116,7 +1146,7 @@ mod tests { for _ in 0..1 + rng.below(3) { tips.push(commits[rng.below(commits.len())]); } - let pruned: HashSet = commits + let pruned: HashSet = commits .iter() .copied() .filter(|_| rng.below(6) == 0) diff --git a/josh-graphql/Cargo.toml b/josh-graphql/Cargo.toml index 26ec9607a..6552476c5 100644 --- a/josh-graphql/Cargo.toml +++ b/josh-graphql/Cargo.toml @@ -15,6 +15,7 @@ strfmt = "0.2.5" anyhow.workspace = true juniper.workspace = true git2.workspace = true +gix-hash.workspace = true regex.workspace = true serde_json.workspace = true serde_yaml.workspace = true diff --git a/josh-graphql/src/graphql.rs b/josh-graphql/src/graphql.rs index aa9b16fed..68a6d1316 100644 --- a/josh-graphql/src/graphql.rs +++ b/josh-graphql/src/graphql.rs @@ -7,16 +7,17 @@ use josh_core::objects; use josh_core::objects::CommitData; use josh_core::{cache, filter, history}; use juniper::{EmptyMutation, EmptySubscription, FieldResult, graphql_object}; +use std::str::FromStr; pub struct Revision { filter: filter::Filter, - commit_id: git2::Oid, + commit_id: gix_hash::ObjectId, } fn find_paths( transaction: &cache::Transaction, odb: &josh_core::memodb::Odb, - tree: git2::Oid, + tree: gix_hash::ObjectId, at: Option, depth: Option, kind: git2::ObjectType, @@ -28,7 +29,7 @@ fn find_paths( if !entry.mode.is_tree() { return Err(anyhow!("not a directory: {}", at)); } - objects::git2_oid(&entry.oid) + entry.oid.to_owned() } _ => tree, }; @@ -45,7 +46,7 @@ fn find_paths( fn collect_paths( transaction: &cache::Transaction, odb: &josh_core::memodb::Odb, - tree: git2::Oid, + tree: gix_hash::ObjectId, prefix: &std::path::Path, level: i32, depth: Option, @@ -72,7 +73,7 @@ fn collect_paths( collect_paths( transaction, odb, - objects::git2_oid(&entry.oid), + entry.oid.to_owned(), &path, level + 1, depth, @@ -88,7 +89,7 @@ fn collect_paths( fn filtered_commit( transaction: &cache::Transaction, filter: filter::Filter, - commit_id: git2::Oid, + commit_id: gix_hash::ObjectId, ) -> anyhow::Result { let filtered = filter::apply_to_commit(filter, commit_id, transaction)?; CommitData::read(transaction.odb(), filtered) @@ -229,7 +230,7 @@ impl Revision { id, false, ) - .unwrap_or_else(|_| git2::Oid::ZERO_SHA1), + .unwrap_or_else(|_| gix_hash::ObjectId::null(gix_hash::Kind::Sha1)), }) .collect(); @@ -266,7 +267,7 @@ impl Revision { let orig = history::find_original(&transaction, self.filter, contained_in, ids[i], true)?; - if orig != git2::Oid::ZERO_SHA1 { + if orig != gix_hash::ObjectId::null(gix_hash::Kind::Sha1) { ids[i] = orig; contained_in = josh_core::git::read_parent_ids(transaction.odb(), ids[i])? .into_iter() @@ -318,7 +319,10 @@ impl Revision { let odb = transaction.odb(); let (parent_id, parent_tree_id) = match filter_commit.first_parent_id() { Some(parent) => (parent, josh_core::git::read_tree_id(odb, parent)?), - None => (git2::Oid::ZERO_SHA1, git2::Oid::ZERO_SHA1), + None => ( + gix_hash::ObjectId::null(gix_hash::Kind::Sha1), + gix_hash::ObjectId::null(gix_hash::Kind::Sha1), + ), }; let filter_tree_id = filter_commit.tree_id()?; @@ -500,9 +504,9 @@ impl Warning { #[derive(Clone)] pub struct Path { path: std::path::PathBuf, - commit_id: git2::Oid, + commit_id: gix_hash::ObjectId, filter: filter::Filter, - tree: git2::Oid, + tree: gix_hash::ObjectId, } #[derive(Clone)] @@ -539,7 +543,7 @@ impl SearchResult { pub fn linecount( transaction: &cache::Transaction, odb: &josh_core::memodb::Odb, - id: git2::Oid, + id: gix_hash::ObjectId, ) -> usize { if let Some(blob) = tree::blob_bytes(odb, id) { return blob.iter().filter(|x| **x == b'\n').count() + if blob.is_empty() { 0 } else { 1 }; @@ -548,7 +552,7 @@ pub fn linecount( if let Ok(reader) = tree::read_tree(transaction, odb, id) { return reader .entries() - .map(|e| linecount(transaction, odb, objects::git2_oid(&e.oid))) + .map(|e| linecount(transaction, odb, e.oid.to_owned())) .sum(); } 0 @@ -556,7 +560,7 @@ pub fn linecount( struct Markers { path: std::path::PathBuf, - commit_id: git2::Oid, + commit_id: gix_hash::ObjectId, filter: filter::Filter, topic: String, } @@ -590,7 +594,7 @@ impl Markers { let prev = match tree::get_path_entry(&transaction, odb, tree, &path)? { Some(entry) => { - let blob = tree::blob_bytes(odb, objects::git2_oid(&entry.oid)) + let blob = tree::blob_bytes(odb, entry.oid.to_owned()) .ok_or_else(|| anyhow!("not a blob: {}", entry.oid))?; std::str::from_utf8(&blob)?.to_owned() } @@ -605,8 +609,8 @@ impl Markers { Document { id: s .next() - .and_then(|x| git2::Oid::from_str(x).ok()) - .unwrap_or(git2::Oid::ZERO_SHA1), + .and_then(|x| gix_hash::ObjectId::from_str(x).ok()) + .unwrap_or(gix_hash::ObjectId::null(gix_hash::Kind::Sha1)), value: s .next() .and_then(|x| serde_json::from_str::(x).ok()) @@ -637,7 +641,7 @@ impl Markers { .ok() .flatten() .filter(|entry| entry.mode.is_tree()) - .map(|entry| objects::git2_oid(&entry.oid)) + .map(|entry| entry.oid.to_owned()) .unwrap_or_else(filter::tree::empty_id); let mtree = if self.filter.is_nop() { @@ -651,7 +655,7 @@ impl Markers { )? }; if let Ok(Some(p)) = tree::get_path_entry(&transaction, odb, mtree, &self.path) { - return Ok(linecount(&transaction, odb, objects::git2_oid(&p.oid)) as i32); + return Ok(linecount(&transaction, odb, p.oid.to_owned()) as i32); } else if self.path == std::path::Path::new("") { return Ok(linecount(&transaction, odb, mtree) as i32); } @@ -663,7 +667,7 @@ impl Path { fn internal_serialize( &self, context: &Context, - to_result: impl FnOnce(&cache::Transaction, git2::Oid) -> FieldResult, + to_result: impl FnOnce(&cache::Transaction, gix_hash::ObjectId) -> FieldResult, ) -> FieldResult { let transaction = context.transaction.lock().unwrap(); @@ -673,7 +677,7 @@ impl Path { let odb = transaction.odb(); let entry = tree::get_path_entry(&transaction, odb, self.tree, &self.path)? .ok_or_else(|| anyhow!("no such path: {}", self.path.display()))?; - objects::git2_oid(&entry.oid) + entry.oid.to_owned() }; to_result(&transaction, id) } @@ -760,7 +764,7 @@ impl Path { } pub struct Document { - id: git2::Oid, + id: gix_hash::ObjectId, value: serde_json::Value, } @@ -808,7 +812,7 @@ impl Document { if let serde_json::Value::Array(a) = &self.pointer(at) { for x in a.iter() { v.push(Document { - id: git2::Oid::ZERO_SHA1, + id: gix_hash::ObjectId::null(gix_hash::Kind::Sha1), value: x.clone(), }); } @@ -820,7 +824,7 @@ impl Document { fn value(&self, at: String) -> Option { self.value.pointer(&at).map(|x| Document { - id: git2::Oid::ZERO_SHA1, + id: gix_hash::ObjectId::null(gix_hash::Kind::Sha1), value: x.to_owned(), }) } @@ -858,7 +862,7 @@ impl Reference { } type ToPushSet = std::sync::Arc< - std::sync::Mutex)>>, + std::sync::Mutex)>>, >; #[derive(PartialEq, Eq, Clone, Copy)] @@ -941,8 +945,11 @@ impl RevMut { fn push(&self, target: String, repo: Option, context: &Context) -> FieldResult { let transaction = context.transaction.lock().unwrap(); - let filter_commit = - filtered_commit(&transaction, self.filter, git2::Oid::from_str(&self.at)?)?; + let filter_commit = filtered_commit( + &transaction, + self.filter, + gix_hash::ObjectId::from_str(&self.at)?, + )?; if let Ok(mut to_push) = context.to_push.lock() { to_push.insert((filter_commit.id(), target, repo)); @@ -990,7 +997,7 @@ impl RepositoryMut { let transaction_mirror = context.transaction_mirror.lock().unwrap(); // Just check that the commit exists - CommitData::read(transaction_mirror.odb(), git2::Oid::from_str(&at)?)?; + CommitData::read(transaction_mirror.odb(), gix_hash::ObjectId::from_str(&at)?)?; let filter = if let Some(spec) = filter { filter::parse(&spec)? @@ -1045,8 +1052,8 @@ impl Repository { let transaction_mirror = context.transaction_mirror.lock().unwrap(); let commit_id = { - let oid = if let Ok(id) = git2::Oid::from_str(&at) { - Some((id, transaction_mirror.odb().contains(objects::gix_oid(id)))) + let oid = if let Ok(id) = gix_hash::ObjectId::from_str(&at) { + Some((id, transaction_mirror.odb().contains(id))) } else { None }; @@ -1097,7 +1104,7 @@ pub fn context(transaction: cache::Transaction, transaction_mirror: cache::Trans pub type CommitSchema = juniper::RootNode, EmptySubscription>; -pub fn commit_schema(commit_id: git2::Oid) -> CommitSchema { +pub fn commit_schema(commit_id: gix_hash::ObjectId) -> CommitSchema { CommitSchema::new( Revision { commit_id, diff --git a/josh-gui/Cargo.lock b/josh-gui/Cargo.lock index cd743e6ac..61aee952b 100644 --- a/josh-gui/Cargo.lock +++ b/josh-gui/Cargo.lock @@ -4205,6 +4205,7 @@ dependencies = [ "anyhow", "chrono", "git2", + "gix-hash", "josh-core", "josh-git-serde", "serde", @@ -4319,7 +4320,7 @@ name = "josh-github-changes" version = "26.7.28" dependencies = [ "anyhow", - "git2", + "gix-hash", "josh-changes", "josh-core", "josh-git-serde", @@ -4351,7 +4352,7 @@ version = "26.7.28" dependencies = [ "anyhow", "async-trait", - "git2", + "gix-hash", "graphql_client", "http", "josh-github-auth", @@ -4409,6 +4410,7 @@ dependencies = [ "clap", "dioxus", "git2", + "gix-hash", "josh-changes", "josh-core", "josh-github-changes", @@ -4438,7 +4440,6 @@ name = "josh-search" version = "26.7.28" dependencies = [ "anyhow", - "git2", "gix-hash", "gix-object", ] @@ -4449,7 +4450,7 @@ version = "26.7.28" dependencies = [ "allocative", "anyhow", - "git2", + "gix-hash", "gix-object", "josh-filter", "josh-gix-ext", diff --git a/josh-gui/Cargo.toml b/josh-gui/Cargo.toml index 635f1c2da..7262f453c 100644 --- a/josh-gui/Cargo.toml +++ b/josh-gui/Cargo.toml @@ -20,6 +20,7 @@ josh-changes = { path = "../josh-changes" } josh-core = { path = "../josh-core" } josh-github-changes = { path = "../forges/josh-github-changes" } git2 = { version = "0.21.0", default-features = false, features = ["vendored-libgit2"] } +gix-hash = { version = "^0.26", features = ["sha1"] } tokio = { version = "1", features = ["time"] } # Same as the root workspace: use the vendored glob fork, which keeps the diff --git a/josh-gui/src/detail.rs b/josh-gui/src/detail.rs index be9801b43..71015c4bc 100644 --- a/josh-gui/src/detail.rs +++ b/josh-gui/src/detail.rs @@ -1,4 +1,5 @@ use dioxus::prelude::*; +use std::str::FromStr; use crate::Page; use crate::common::{ @@ -46,7 +47,7 @@ pub struct PrInfo { #[component] pub fn DetailView(sha: String, scope: josh_changes::ChangesRef, mut page: Signal) -> Element { - let changes_ref_oid = use_context::>>(); + let changes_ref_oid = use_context::>>(); // Establish a reactive dependency on ref changes. let _ = changes_ref_oid.read(); let data = load_detail(&sha, &scope); @@ -289,9 +290,9 @@ pub fn DetailView(sha: String, scope: josh_changes::ChangesRef, mut page: Signal pub fn load_detail(sha: &str, scope: &josh_changes::ChangesRef) -> anyhow::Result { let transaction = crate::common::open_transaction()?; - let oid = git2::Oid::from_str(sha)?; + let oid = gix_hash::ObjectId::from_str(sha)?; let repo = transaction.git2_repo(); - let commit = repo.find_commit(oid)?; + let commit = repo.find_commit(josh_core::objects::git2_oid(&oid))?; let msg = commit.message().unwrap_or(""); let subject = msg.lines().next().unwrap_or("").to_string(); @@ -345,13 +346,13 @@ pub fn load_detail(sha: &str, scope: &josh_changes::ChangesRef) -> anyhow::Resul if let Ok(all) = josh_changes::list_changes(&transaction, scope) { if let Some(c) = all.iter().find(|c| c.id() == Some(cid.as_str())) { let base = c.base(); - if base != git2::Oid::ZERO_SHA1 { + if base != gix_hash::ObjectId::null(gix_hash::Kind::Sha1) { change.set_base(base); } } } for oid in change.contributing(&transaction).unwrap_or_default() { - if let Ok(c) = repo.find_commit(oid) { + if let Ok(c) = repo.find_commit(josh_core::objects::git2_oid(&oid)) { let msg = c.message().unwrap_or(""); let c_subject = msg.lines().next().unwrap_or("").to_string(); let c_author = c.author().email().unwrap_or("").to_string(); @@ -401,7 +402,7 @@ pub fn save_comment( scope: &josh_changes::ChangesRef, ) -> anyhow::Result { let transaction = crate::common::open_transaction()?; - let oid = git2::Oid::from_str(sha)?; + let oid = gix_hash::ObjectId::from_str(sha)?; let change = josh_changes::Change::new(&transaction, oid)?; let meta = josh_changes::CommentMeta { @@ -433,7 +434,7 @@ pub fn save_comment( /// Refresh the shared OID after a local ref mutation. pub fn bump_changes_ref_oid( - mut changes_ref_oid: Signal>, + mut changes_ref_oid: Signal>, scope: &josh_changes::ChangesRef, ) { let new_oid = git2::Repository::discover(".") @@ -451,7 +452,7 @@ pub fn save_vote( scope: &josh_changes::ChangesRef, ) -> anyhow::Result { let transaction = crate::common::open_transaction()?; - let oid = git2::Oid::from_str(sha)?; + let oid = gix_hash::ObjectId::from_str(sha)?; let change = josh_changes::Change::new(&transaction, oid)?; let body_meta = || josh_changes::CommentMeta { diff --git a/josh-gui/src/diff.rs b/josh-gui/src/diff.rs index a7fe272ae..400a2bf92 100644 --- a/josh-gui/src/diff.rs +++ b/josh-gui/src/diff.rs @@ -1,4 +1,5 @@ use dioxus::prelude::*; +use std::str::FromStr; use crate::Page; use crate::common::{FlatComment, parse_hunk_header, render_comment_card}; @@ -49,8 +50,8 @@ fn selected_file_line(items: &[DiffItem], sel: Option) -> Option { fn load_file_diff(sha: &str, path: &str, context_lines: u32) -> anyhow::Result> { let repo = git2::Repository::discover(".")?; - let oid = git2::Oid::from_str(sha)?; - let commit = repo.find_commit(oid)?; + let oid = gix_hash::ObjectId::from_str(sha)?; + let commit = repo.find_commit(josh_core::objects::git2_oid(&oid))?; let parent_tree = commit.parent(0).ok().and_then(|p| p.tree().ok()); let mut opts = git2::DiffOptions::new(); @@ -202,7 +203,7 @@ pub fn FileDiffView( scope: josh_changes::ChangesRef, mut page: Signal, ) -> Element { - let changes_ref_oid = use_context::>>(); + let changes_ref_oid = use_context::>>(); let mut detail = use_signal(|| detail::load_detail(&sha, &scope)); let mut prev_sha = use_signal(|| sha.clone()); let mut prev_oid = use_signal(|| *changes_ref_oid.peek()); diff --git a/josh-gui/src/list.rs b/josh-gui/src/list.rs index 79c82ed28..f45a7cc24 100644 --- a/josh-gui/src/list.rs +++ b/josh-gui/src/list.rs @@ -45,8 +45,8 @@ pub fn ListView( mut page: Signal, mut selected_change: Signal>, ) -> Element { - let changes_ref_oid = use_context::>>(); - let mut prev_metadata_oid = use_signal(|| None::>); + let changes_ref_oid = use_context::>>(); + let mut prev_metadata_oid = use_signal(|| None::>); { let scope_for_meta = scope.clone(); @@ -325,7 +325,9 @@ pub fn load_rows(scope: &josh_changes::ChangesRef) -> anyhow::Result { let mut dependencies: HashMap> = HashMap::new(); for change in &changes { - let commit = transaction.git2_repo().find_commit(change.commit())?; + let commit = transaction + .git2_repo() + .find_commit(josh_core::objects::git2_oid(&change.commit()))?; let subject = commit .message() .unwrap_or("") diff --git a/josh-gui/src/main.rs b/josh-gui/src/main.rs index 8f2807610..261bc63ef 100644 --- a/josh-gui/src/main.rs +++ b/josh-gui/src/main.rs @@ -76,7 +76,7 @@ fn app() -> Element { let current_scope = use_signal(|| initial_scope.clone()); // Views subscribe to this signal to invalidate ref-derived data. - let mut changes_ref_oid: Signal> = use_signal(|| { + let mut changes_ref_oid: Signal> = use_signal(|| { git2::Repository::discover(".") .ok() .and_then(|r| josh_changes::read_ref_oid(&r, ¤t_scope.read())) diff --git a/josh-link/Cargo.toml b/josh-link/Cargo.toml index bab2398fb..bdce7e489 100644 --- a/josh-link/Cargo.toml +++ b/josh-link/Cargo.toml @@ -10,6 +10,7 @@ keywords = ["git", "monorepo", "workflow", "scm"] [dependencies] git2.workspace = true +gix-hash.workspace = true anyhow.workspace = true josh-core.workspace = true diff --git a/josh-link/src/lib.rs b/josh-link/src/lib.rs index 728a0c24e..803c9007c 100644 --- a/josh-link/src/lib.rs +++ b/josh-link/src/lib.rs @@ -7,7 +7,7 @@ use std::path::PathBuf; /// Prepared link addition, ready to be finalized pub struct PreparedLinkAdd { - tree_oid: git2::Oid, + tree_oid: gix_hash::ObjectId, path: PathBuf, } @@ -15,9 +15,9 @@ impl PreparedLinkAdd { pub fn into_commit( self, transaction: &josh_core::cache::Transaction, - head_commit: git2::Oid, + head_commit: gix_hash::ObjectId, signature: &git2::Signature, - ) -> anyhow::Result { + ) -> anyhow::Result { josh_core::objects::write_commit( transaction.odb(), self.tree_oid, @@ -32,7 +32,7 @@ impl PreparedLinkAdd { /// Get tree OID for custom commit creation /// /// This is used by josh-cq to add additional files before creating a commit - pub fn into_tree_oid(self) -> git2::Oid { + pub fn into_tree_oid(self) -> gix_hash::ObjectId { self.tree_oid } } @@ -40,9 +40,9 @@ impl PreparedLinkAdd { /// Result from updating links pub struct UpdateLinksResult { /// Commit with updated .link.josh files - pub commit_with_updates: git2::Oid, + pub commit_with_updates: gix_hash::ObjectId, /// Commit after applying :link filter - pub filtered_commit: git2::Oid, + pub filtered_commit: gix_hash::ObjectId, } /// A remote URL and commit SHA found in a `.link.josh` file. @@ -56,7 +56,7 @@ pub struct LinkRef { /// all (remote, commit) pairs found in any `.link.josh` file across all commits and trees. pub fn collect_all_link_refs( transaction: &josh_core::cache::Transaction, - commit: git2::Oid, + commit: gix_hash::ObjectId, ) -> anyhow::Result> { // Apply a filter that keeps only .link.josh files. This prunes the history // to only commits that actually changed those files, so the revwalk below @@ -67,7 +67,7 @@ pub fn collect_all_link_refs( let filtered_commit = josh_core::filter_commit(transaction, link_file_filter, commit) .context("Failed to apply .link.josh filter")?; - if filtered_commit == git2::Oid::ZERO_SHA1 { + if filtered_commit == gix_hash::ObjectId::null(gix_hash::Kind::Sha1) { return Ok(HashSet::new()); } @@ -122,8 +122,8 @@ pub fn prepare_link_add( url: &str, filter: Option<&str>, target: &str, - fetched_commit: git2::Oid, - head_tree: git2::Oid, + fetched_commit: gix_hash::ObjectId, + head_tree: gix_hash::ObjectId, mode: josh_core::filter::LinkMode, ) -> anyhow::Result { let odb = transaction.odb(); @@ -166,8 +166,8 @@ pub fn prepare_link_add( pub fn update_links( transaction: &josh_core::cache::Transaction, - head_commit: git2::Oid, - links_to_update: Vec<(PathBuf, git2::Oid)>, + head_commit: gix_hash::ObjectId, + links_to_update: Vec<(PathBuf, gix_hash::ObjectId)>, signature: &git2::Signature, ) -> anyhow::Result> { let odb = transaction.odb(); diff --git a/josh-proxy/Cargo.toml b/josh-proxy/Cargo.toml index 354dcead8..eb1b48ff6 100644 --- a/josh-proxy/Cargo.toml +++ b/josh-proxy/Cargo.toml @@ -51,6 +51,7 @@ tempfile.workspace = true gix.workspace = true juniper.workspace = true git2.workspace = true +gix-hash.workspace = true url.workspace = true josh-changes.workspace = true diff --git a/josh-proxy/src/lib.rs b/josh-proxy/src/lib.rs index d73d6636a..600e2280b 100644 --- a/josh-proxy/src/lib.rs +++ b/josh-proxy/src/lib.rs @@ -287,7 +287,7 @@ pub fn merge_meta( transaction: &josh_core::cache::Transaction, transaction_mirror: &josh_core::cache::Transaction, meta_add: &std::collections::HashMap>, -) -> anyhow::Result> { +) -> anyhow::Result> { if meta_add.is_empty() { return Ok(None); } @@ -304,11 +304,8 @@ pub fn merge_meta( for (path, add_lines) in meta_add.iter() { let prev = match josh_core::filter::tree::get_path_entry(transaction, odb, tree, path)? { Some(entry) => { - let blob = josh_core::filter::tree::blob_bytes( - odb, - josh_core::objects::git2_oid(&entry.oid), - ) - .ok_or_else(|| anyhow!("not a blob: {}", entry.oid))?; + let blob = josh_core::filter::tree::blob_bytes(odb, entry.oid.to_owned()) + .ok_or_else(|| anyhow!("not a blob: {}", entry.oid))?; std::str::from_utf8(&blob)?.to_owned() } None => "".to_owned(), diff --git a/josh-proxy/src/service.rs b/josh-proxy/src/service.rs index fa78047a6..722a27881 100644 --- a/josh-proxy/src/service.rs +++ b/josh-proxy/src/service.rs @@ -498,7 +498,7 @@ fn resolve_upstream_ref( transaction: &josh_core::cache::Transaction, repo: &str, ref_value: &str, -) -> anyhow::Result { +) -> anyhow::Result { let josh_name = format!( "refs/josh/upstream/{}/{}", josh_core::to_ns(repo), @@ -513,7 +513,7 @@ fn resolve_upstream_ref( pub struct NamespacedRefs { transaction: josh_core::cache::Transaction, ns: Arc, - refs: Vec<(String, git2::Oid)>, + refs: Vec<(String, gix_hash::ObjectId)>, head_symref: (String, String), } @@ -540,7 +540,7 @@ impl NamespacedRefs { Ok(()) } - pub fn into_inner(self) -> (Vec<(String, git2::Oid)>, (String, String)) { + pub fn into_inner(self) -> (Vec<(String, gix_hash::ObjectId)>, (String, String)) { (self.refs, self.head_symref) } } @@ -642,7 +642,7 @@ async fn filter_to_namespace( let (filtered_refs, _) = josh_core::filter_refs(&t2, filter, &refs_to_filter); let populate_refs = filtered_refs .iter() - .any(|(refn, oid)| refn == &head_symref_target && !oid.is_zero()); + .any(|(refn, oid)| refn == &head_symref_target && !oid.is_null()); let head_symref = ("HEAD".to_string(), head_symref_target); @@ -655,7 +655,7 @@ async fn filter_to_namespace( let namespaced_refs = if populate_refs { filtered_refs .into_iter() - .filter(|(_, oid)| !oid.is_zero()) + .filter(|(_, oid)| !oid.is_null()) .collect() } else { Default::default() @@ -883,7 +883,7 @@ async fn serve_namespace( pub enum HeadRef { ExplicitHead, ExplicitRef(String), - ExplicitSha(String, git2::Oid), + ExplicitSha(String, gix_hash::ObjectId), Implicit, } @@ -909,7 +909,7 @@ impl FromStr for HeadRef { "HEAD" => HeadRef::ExplicitHead, r if r.starts_with("refs/") => HeadRef::ExplicitRef(r.into()), r => { - if let Ok(oid) = git2::Oid::from_str(&r) { + if let Ok(oid) = gix_hash::ObjectId::from_str(&r) { HeadRef::ExplicitSha(r.to_string(), oid) } else { return Err(anyhow!("failed to parse ref")); @@ -1340,7 +1340,7 @@ async fn serve_render_template( .unwrap(), )?; - let commit_id = if let Ok(oid) = git2::Oid::from_str(&head_ref) { + let commit_id = if let Ok(oid) = gix_hash::ObjectId::from_str(&head_ref) { oid } else { transaction_mirror diff --git a/josh-proxy/src/upstream.rs b/josh-proxy/src/upstream.rs index 86b9e2d36..f5d4f7a51 100644 --- a/josh-proxy/src/upstream.rs +++ b/josh-proxy/src/upstream.rs @@ -3,6 +3,7 @@ use crate::service::{JoshProxyService, UpstreamProtocol}; use crate::{FetchError, auth, run_git_with_auth}; use anyhow::{Context, anyhow}; use backon::BackoffBuilder; +use std::str::FromStr; use josh_changes::{PushMode, baseref_and_options, build_to_push}; use josh_core::cache::{CacheStack, TransactionContext}; @@ -339,11 +340,11 @@ pub fn process_repo_update(repo_update: RepoUpdate) -> anyhow::Result { transaction .add_disk_alternate(transaction_mirror.path().join("objects").to_str().unwrap())?; - let old = git2::Oid::from_str(old)?; + let old = gix_hash::ObjectId::from_str(old)?; let author = push_options.author.as_deref().unwrap_or(""); let (baseref, push_to, options, push_mode) = baseref_and_options(refname, author)?; - let old = if old == git2::Oid::ZERO_SHA1 { + let old = if old == gix_hash::ObjectId::null(gix_hash::Kind::Sha1) { let rev = format!("refs/namespaces/{}/{}", repo_update.git_ns, &baseref); let oid = transaction.resolve_ref(&rev)?.unwrap_or(old); @@ -412,7 +413,7 @@ pub fn process_repo_update(repo_update: RepoUpdate) -> anyhow::Result { }; let filter = josh_core::filter::parse(&repo_update.filter_spec)?; - let new_oid = git2::Oid::from_str(new)?; + let new_oid = gix_hash::ObjectId::from_str(new)?; let backward_new_oid = { let unapply_result = josh_core::history::unapply_filter( &transaction, @@ -546,7 +547,7 @@ pub fn process_repo_update(repo_update: RepoUpdate) -> anyhow::Result { pub fn push_head_url( transaction: &josh_core::cache::Transaction, alternate: &str, - oid: git2::Oid, + oid: gix_hash::ObjectId, refname: &str, url: &str, remote_auth: &RemoteAuth, diff --git a/josh-search/Cargo.toml b/josh-search/Cargo.toml index c6a968ee6..e9806aaef 100644 --- a/josh-search/Cargo.toml +++ b/josh-search/Cargo.toml @@ -15,11 +15,11 @@ harness = false [dependencies] anyhow.workspace = true -git2.workspace = true gix-hash.workspace = true gix-object.workspace = true [dev-dependencies] +git2.workspace = true josh-gix-ext.workspace = true gix.workspace = true criterion2 = { version = "3.0.4" } diff --git a/josh-search/benches/trigram.rs b/josh-search/benches/trigram.rs index 02cadd7cf..d7eaa03b3 100644 --- a/josh-search/benches/trigram.rs +++ b/josh-search/benches/trigram.rs @@ -3,6 +3,7 @@ use josh_core::git::josh_commit_signature; use josh_test_support::bench::{EntryKind, git2_oid, gix_oid}; use rand::prelude::*; use std::path::PathBuf; +use std::str::FromStr; // Benchmarks for the trigram indexer (`josh_search`), covering its three // externally meaningful operations through the public API only, so the numbers stay comparable @@ -84,14 +85,14 @@ const JOSH_BENCH_COMMIT_TIME: &str = "1700000000"; /// bookkeeping. struct Case { n_files: usize, - chain: Vec, - index_tree_oid: git2::Oid, - chain_indexes: Vec<(git2::Oid, git2::Oid)>, + chain: Vec, + index_tree_oid: gix_hash::ObjectId, + chain_indexes: Vec<(gix_hash::ObjectId, gix_hash::ObjectId)>, total_bytes: u64, } impl Case { - fn tip(&self) -> git2::Oid { + fn tip(&self) -> gix_hash::ObjectId { *self.chain.last().expect("chain is never empty") } } @@ -169,7 +170,7 @@ fn build_case( repo: &git2::Repository, vocab: &[String], n_files: usize, -) -> anyhow::Result { +) -> anyhow::Result { let gix_repo = gix::open(repo.path())?; let baseline = repo.treebuilder(None)?.write()?; let mut builder = gix_repo.edit_tree(gix_oid(baseline))?; @@ -226,18 +227,21 @@ fn build_case( "bench case tip", )?; - Ok(head) + Ok(gix_oid(head)) } /// Aggregate every case tip under one index commit. Its oid changes whenever any case head /// changes, making it a faithful content-addressed cache stamp for the entire repo, and it keeps /// all cases reachable so provision_repo's `git prune` retains the full history. -fn build_index(repo: &git2::Repository, heads: &[git2::Oid]) -> anyhow::Result { +fn build_index( + repo: &git2::Repository, + heads: &[gix_hash::ObjectId], +) -> anyhow::Result { let sig = josh_commit_signature()?; let empty_tree = repo.find_tree(repo.treebuilder(None)?.write()?)?; let parents = heads .iter() - .map(|oid| repo.find_commit(*oid)) + .map(|oid| repo.find_commit(git2_oid(*oid))) .collect::, _>>()?; let parent_refs = parents.iter().collect::>(); let index = repo.commit( @@ -248,7 +252,7 @@ fn build_index(repo: &git2::Repository, heads: &[git2::Oid]) -> anyhow::Result anyhow::Res } /// Recover the first-parent chain (root first, tip last) of a case from its tip ref. -fn recover_chain(repo: &git2::Repository, n_files: usize) -> anyhow::Result> { +fn recover_chain( + repo: &git2::Repository, + n_files: usize, +) -> anyhow::Result> { let mut chain = vec![]; let mut oid = repo.refname_to_id(&format!("refs/heads/case_{n_files}"))?; loop { - chain.push(oid); + chain.push(gix_oid(oid)); match repo.find_commit(oid)?.parent_id(0) { Ok(parent) => oid = parent, Err(_) => break, @@ -289,8 +296,8 @@ fn recover_chain(repo: &git2::Repository, n_files: usize) -> anyhow::Result anyhow::Result<(Vec, Vec<(String, Vec<(usize, String)>)>)> { let candidates = josh_search::search_candidates(src, index_tree, source_tree, needle)?; @@ -312,7 +319,8 @@ impl TrigramBench { let provisioned = josh_test_support::provision_repo::provision_repo( TESTCASE, - &git2::Oid::from_str(EXPECTED_HEAD).expect("EXPECTED_HEAD must be a valid oid"), + &gix_hash::ObjectId::from_str(EXPECTED_HEAD) + .expect("EXPECTED_HEAD must be a valid oid"), |repo| { let vocab = vocabulary(); let mut heads = vec![]; @@ -344,7 +352,7 @@ impl TrigramBench { josh_core::reset_caches()?; let transaction = context.open()?; let repo = transaction.git2_repo(); - let tip_tree = repo.find_commit(tip)?.tree()?; + let tip_tree = repo.find_commit(git2_oid(tip))?.tree()?; let total_bytes = tree_content_bytes(repo, &tip_tree)?; let odb = transaction.odb(); @@ -354,7 +362,7 @@ impl TrigramBench { odb, &transaction.trigram_index_cache(tip), &mut josh_search::Indexer::default(), - tip_tree.id(), + gix_oid(tip_tree.id()), )?; transaction.flush_mem_odb()?; @@ -363,7 +371,8 @@ impl TrigramBench { // degenerated and the search numbers would be meaningless. (The bound is kept at // the pre-rework value of 5; the exact index should always produce exactly 1.) let rare_path = path_for(n_files / 2).to_string_lossy().into_owned(); - let (candidates, matches) = search(odb, index_tree_oid, tip_tree.id(), NEEDLE_RARE)?; + let (candidates, matches) = + search(odb, index_tree_oid, gix_oid(tip_tree.id()), NEEDLE_RARE)?; anyhow::ensure!( matches.len() == 1 && matches[0].0 == rare_path, "rare needle not found in exactly its planted file {rare_path}: {matches:?}" @@ -376,13 +385,13 @@ impl TrigramBench { // Gate: the common needle is found in every planted file, the absent one nowhere. let common_count = n_files.div_ceil(COMMON_EVERY); - let (_, matches) = search(odb, index_tree_oid, tip_tree.id(), NEEDLE_COMMON)?; + let (_, matches) = search(odb, index_tree_oid, gix_oid(tip_tree.id()), NEEDLE_COMMON)?; anyhow::ensure!( matches.len() == common_count, "common needle found in {} files, expected {common_count}", matches.len() ); - let (_, matches) = search(odb, index_tree_oid, tip_tree.id(), NEEDLE_ABSENT)?; + let (_, matches) = search(odb, index_tree_oid, gix_oid(tip_tree.id()), NEEDLE_ABSENT)?; anyhow::ensure!( matches.is_empty(), "absent needle found in {} files", @@ -395,23 +404,24 @@ impl TrigramBench { josh_core::reset_caches()?; let transaction = context.open()?; let repo = transaction.git2_repo(); - let root_tree = repo.find_commit(chain[0])?.tree()?; + let root_tree = repo.find_commit(git2_oid(chain[0]))?.tree()?; let odb = transaction.odb(); // One indexer state for the whole chain, matching how josh keeps one per // transaction. Collect the per-commit (source tree, index tree) pairs on the way: // the history search group iterates them. let mut indexer = josh_search::Indexer::default(); + let root_tree_oid = gix_oid(root_tree.id()); let mut chain_indexes = vec![( - root_tree.id(), + root_tree_oid, josh_search::trigram_index( odb, &transaction.trigram_index_cache(chain[0]), &mut indexer, - root_tree.id(), + root_tree_oid, )?, )]; for &oid in &chain[1..] { - let tree_oid = repo.find_commit(oid)?.tree_id(); + let tree_oid = gix_oid(repo.find_commit(git2_oid(oid))?.tree_id()); let index_oid = josh_search::trigram_index( odb, &transaction.trigram_index_cache(oid), @@ -469,7 +479,11 @@ fn trigram_benches(c: &mut Criterion) { josh_core::reset_caches().expect("reset caches"); let transaction = bench.context.open().expect("open transaction"); let repo = transaction.git2_repo(); - let tip_tree = repo.find_commit(case.tip()).expect("find tip").tree_id(); + let tip_tree = gix_oid( + repo.find_commit(git2_oid(case.tip())) + .expect("find tip") + .tree_id(), + ); let odb = transaction.odb(); let mut indexer = josh_search::Indexer::default(); @@ -502,10 +516,11 @@ fn trigram_benches(c: &mut Criterion) { josh_core::reset_caches().expect("reset caches"); let transaction = bench.context.open().expect("open transaction"); let repo = transaction.git2_repo(); - let root_tree = repo - .find_commit(case.chain[0]) - .expect("find root") - .tree_id(); + let root_tree = gix_oid( + repo.find_commit(git2_oid(case.chain[0])) + .expect("find root") + .tree_id(), + ); let odb = transaction.odb(); // One indexer state across the warm root and the whole chain, matching how // josh keeps one per transaction. @@ -520,7 +535,11 @@ fn trigram_benches(c: &mut Criterion) { runner.run(|| { for &oid in &case.chain[1..] { - let tree = repo.find_commit(oid).expect("find churn commit").tree_id(); + let tree = gix_oid( + repo.find_commit(git2_oid(oid)) + .expect("find churn commit") + .tree_id(), + ); josh_search::trigram_index( odb, &transaction.trigram_index_cache(oid), @@ -553,7 +572,11 @@ fn trigram_benches(c: &mut Criterion) { josh_core::reset_caches().expect("reset caches"); let transaction = bench.context.open().expect("open transaction"); let repo = transaction.git2_repo(); - let source_tree = repo.find_commit(case.tip()).expect("find tip").tree_id(); + let source_tree = gix_oid( + repo.find_commit(git2_oid(case.tip())) + .expect("find tip") + .tree_id(), + ); let odb = transaction.odb(); runner.run(|| { diff --git a/josh-search/src/lib.rs b/josh-search/src/lib.rs index b65e2e163..b06e06020 100644 --- a/josh-search/src/lib.rs +++ b/josh-search/src/lib.rs @@ -42,8 +42,8 @@ use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; /// [`trigram_index`] consults this per (sub)tree, which is what makes indexing incremental: when /// a new commit is indexed, unchanged subtrees hit the cache and reuse their index. pub trait IndexCache { - fn get_index(&self, tree: git2::Oid) -> Option; - fn set_index(&self, tree: git2::Oid, index: git2::Oid); + fn get_index(&self, tree: gix_hash::ObjectId) -> Option; + fn set_index(&self, tree: gix_hash::ObjectId, index: gix_hash::ObjectId); } fn empty_tree() -> gix_hash::ObjectId { @@ -54,14 +54,6 @@ fn empty_blob() -> gix_hash::ObjectId { gix_hash::ObjectId::empty_blob(gix_hash::Kind::Sha1) } -fn to_gix(oid: git2::Oid) -> gix_hash::ObjectId { - gix_hash::ObjectId::from_bytes_or_panic(oid.as_bytes()) -} - -fn to_git2(oid: gix_hash::ObjectId) -> git2::Oid { - git2::Oid::from_bytes(oid.as_bytes()).expect("oid size mismatch") -} - /// Fold a byte for trigram extraction: ASCII letters lowercase, and every ASCII byte that is /// not alphanumeric or `_` (whitespace, punctuation, brackets, operators) becomes one class /// glyph. Folding collapses the combinatorial variety of near-content-free trigrams — on @@ -137,19 +129,19 @@ pub struct Indexer { /// Parsed trees from `pending` and the ODB, so merges don't re-parse the same spine nodes. trees: HashMap>, /// Source tree -> index, for trees indexed or cache-resolved with this state. - tree_memo: HashMap, + tree_memo: HashMap, /// Source blob and entry name -> the file's wrapped trigram tree. The name is part of the /// key because the mirror entries inside carry it. - blob_memo: HashMap<(git2::Oid, String), gix_hash::ObjectId>, + blob_memo: HashMap<(gix_hash::ObjectId, String), gix_hash::ObjectId>, /// Source blob -> its nameless trigram spine (empty-blob leaves), the building block of /// coarse indexes. Name-independent, so identical blobs share it everywhere. - blob_spine_memo: HashMap, + blob_spine_memo: HashMap, /// Source tree -> its coarse index (the subtree treated as one pseudo-file). Kept apart /// from the fine-grained [`IndexCache`]; coarse subtrees are small by definition, so /// keeping this per process is cheap enough. - coarse_memo: HashMap, + coarse_memo: HashMap, /// Source tree -> transitive `(file count, content bytes)`, for the granularity decision. - stats_memo: HashMap, + stats_memo: HashMap, wrap_memo: HashMap<(gix_hash::ObjectId, String), gix_hash::ObjectId>, overlay_memo: HashMap, gix_hash::ObjectId>, } @@ -178,7 +170,7 @@ struct Run<'a> { ix: &'a mut Indexer, /// `(source tree, index)` pairs of this call, memoized by [`flush`](Run::flush) only after /// their objects reach the ODB: an [`IndexCache`] entry must never point at missing objects. - roots: Vec<(git2::Oid, gix_hash::ObjectId)>, + roots: Vec<(gix_hash::ObjectId, gix_hash::ObjectId)>, } impl Run<'_> { @@ -273,7 +265,12 @@ impl Run<'_> { /// `{name: empty blob}` at every leaf. Building the wrapped form directly (rather than /// wrapping afterwards via [`wrap`](Run::wrap)) avoids an intermediate tree that would /// be rewritten anyway. - fn index_blob(&mut self, oid: git2::Oid, name: &str, content: &str) -> gix_hash::ObjectId { + fn index_blob( + &mut self, + oid: gix_hash::ObjectId, + name: &str, + content: &str, + ) -> gix_hash::ObjectId { let key = (oid, name.to_owned()); if let Some(id) = self.ix.blob_memo.get(&key) { return *id; @@ -292,7 +289,7 @@ impl Run<'_> { /// The nameless trigram spine of one blob: empty-blob leaves, no mirror. The building /// block of coarse (directory granularity) indexes. - fn blob_spine(&mut self, oid: git2::Oid, content: &str) -> gix_hash::ObjectId { + fn blob_spine(&mut self, oid: gix_hash::ObjectId, content: &str) -> gix_hash::ObjectId { if let Some(id) = self.ix.blob_spine_memo.get(&oid) { return *id; } @@ -306,15 +303,15 @@ impl Run<'_> { } /// Transitive `(file count, content bytes)` of a tree, for the granularity decision. - fn subtree_stats(&mut self, tree_oid: git2::Oid) -> anyhow::Result<(u64, u64)> { + fn subtree_stats(&mut self, tree_oid: gix_hash::ObjectId) -> anyhow::Result<(u64, u64)> { if let Some(stats) = self.ix.stats_memo.get(&tree_oid) { return Ok(*stats); } - let tree = self.read_tree(to_gix(tree_oid))?; + let tree = self.read_tree(tree_oid)?; let (mut files, mut bytes) = (0u64, 0u64); for entry in tree.entries.clone() { if entry.mode.is_tree() { - let (f, b) = self.subtree_stats(to_git2(entry.oid))?; + let (f, b) = self.subtree_stats(entry.oid.to_owned())?; files += f; bytes += b; } else if !entry.mode.is_commit() { @@ -332,7 +329,7 @@ impl Run<'_> { } /// Whether a child directory is recorded at directory granularity by its parent. - fn is_small(&mut self, tree_oid: git2::Oid) -> anyhow::Result { + fn is_small(&mut self, tree_oid: gix_hash::ObjectId) -> anyhow::Result { let (files, bytes) = self.subtree_stats(tree_oid)?; Ok(files <= COARSE_MAX_FILES && bytes <= COARSE_MAX_BYTES) } @@ -342,19 +339,19 @@ impl Run<'_> { /// structure. Wrapping it under the directory's name yields `{name: empty blob}` mirrors: /// a blob leaf where the source has a directory, meaning "some file under here contains /// the trigram". - fn coarse_index(&mut self, tree_oid: git2::Oid) -> anyhow::Result { + fn coarse_index(&mut self, tree_oid: gix_hash::ObjectId) -> anyhow::Result { if let Some(id) = self.ix.coarse_memo.get(&tree_oid) { return Ok(*id); } - let tree = self.read_tree(to_gix(tree_oid))?; + let tree = self.read_tree(tree_oid)?; let mut spines = Vec::with_capacity(tree.entries.len()); for entry in tree.entries.clone() { if entry.mode.is_tree() { - spines.push(self.coarse_index(to_git2(entry.oid))?); + spines.push(self.coarse_index(entry.oid.to_owned())?); } else if !entry.mode.is_commit() { let content = read_blob_text(self.src, entry.oid); - spines.push(self.blob_spine(to_git2(entry.oid), &content)); + spines.push(self.blob_spine(entry.oid.to_owned(), &content)); } } let index = self.overlay_many(spines)?; @@ -473,13 +470,16 @@ impl Run<'_> { /// The index of a directory: the overlay of its children's wrapped indexes, memoized per /// source tree oid. The root is not special — incrementality is just this memo hitting on /// unchanged subtrees. - fn index_tree_oid(&mut self, tree_oid: git2::Oid) -> anyhow::Result { - let tree = self.read_tree(to_gix(tree_oid))?; + fn index_tree_oid( + &mut self, + tree_oid: gix_hash::ObjectId, + ) -> anyhow::Result { + let tree = self.read_tree(tree_oid)?; if let Some(id) = self.ix.tree_memo.get(&tree_oid) { return Ok(*id); } if let Some(cached) = self.cache.get_index(tree_oid) { - let id = to_gix(cached); + let id = cached; self.ix.tree_memo.insert(tree_oid, id); return Ok(id); } @@ -487,7 +487,7 @@ impl Run<'_> { let mut wrapped = Vec::with_capacity(tree.entries.len()); for entry in tree.entries.clone() { let name = std::str::from_utf8(&entry.filename)?.to_owned(); - let child_oid = to_git2(entry.oid); + let child_oid = entry.oid.to_owned(); if entry.mode.is_tree() { // Small directories are recorded at directory granularity: one coarse // leaf for the whole directory instead of per-file mirrors. @@ -559,7 +559,7 @@ impl Run<'_> { } for (tree, index) in &self.roots { - self.cache.set_index(*tree, to_git2(*index)); + self.cache.set_index(*tree, *index); } Ok(()) } @@ -569,8 +569,8 @@ pub fn trigram_index( src: &dyn Objects, cache: &dyn IndexCache, indexer: &mut Indexer, - tree: git2::Oid, -) -> anyhow::Result { + tree: gix_hash::ObjectId, +) -> anyhow::Result { let mut run = Run { src, cache, @@ -579,7 +579,7 @@ pub fn trigram_index( }; let index = run.index_tree_oid(tree)?; run.flush()?; - Ok(to_git2(index)) + Ok(index) } /// The candidate files for `searchstring`: those containing every trigram of the query. @@ -588,8 +588,8 @@ pub fn trigram_index( /// candidate then, and [`search_matches`] does the filtering. pub fn search_candidates( src: &dyn Objects, - index_tree: git2::Oid, - source_tree: git2::Oid, + index_tree: gix_hash::ObjectId, + source_tree: gix_hash::ObjectId, searchstring: &str, ) -> anyhow::Result> { let trigrams = distinct_trigrams(searchstring); @@ -638,8 +638,8 @@ pub fn search_candidates( /// expands to every file under that source directory. fn intersect_walk( src: &dyn Objects, - roots: &[git2::Oid], - source: git2::Oid, + roots: &[gix_hash::ObjectId], + source: gix_hash::ObjectId, prefix: &str, out: &mut Vec, ) -> anyhow::Result<()> { @@ -666,7 +666,7 @@ fn intersect_walk( for tree in &trees { match tree.iter().find(|e| e.filename == entry.filename) { Some(other) if other.mode.is_tree() == entry.mode.is_tree() => { - child_roots.push(to_git2(other.oid)) + child_roots.push(other.oid.to_owned()) } _ => continue 'entry, } @@ -678,7 +678,7 @@ fn intersect_walk( else { continue; }; - intersect_walk(src, &child_roots, to_git2(source_entry.oid), &path, out)?; + intersect_walk(src, &child_roots, source_entry.oid.to_owned(), &path, out)?; } else if !entry.mode.is_commit() { emit_leaf(src, &source_entries, name, &path, out)?; } @@ -699,7 +699,7 @@ fn emit_leaf( .iter() .find(|e| e.filename == name.as_bytes()) { - Some(e) if e.mode.is_tree() => collect_paths(src, to_git2(e.oid), path, out)?, + Some(e) if e.mode.is_tree() => collect_paths(src, e.oid.to_owned(), path, out)?, Some(e) if !e.mode.is_commit() => out.push(path.to_owned()), _ => {} } @@ -709,8 +709,8 @@ fn emit_leaf( /// Emit every candidate of the single mirror `oid`, following `source` for coarse expansion. fn collect_mirror_paths( src: &dyn Objects, - oid: git2::Oid, - source: git2::Oid, + oid: gix_hash::ObjectId, + source: gix_hash::ObjectId, prefix: &str, out: &mut Vec, ) -> anyhow::Result<()> { @@ -725,8 +725,8 @@ fn collect_mirror_paths( }; collect_mirror_paths( src, - to_git2(entry.oid), - to_git2(source_entry.oid), + entry.oid.to_owned(), + source_entry.oid.to_owned(), &path, out, )?; @@ -740,11 +740,11 @@ fn collect_mirror_paths( /// The entries of the tree `oid`, owned so several trees can be walked side by side. fn read_tree_entries( src: &dyn Objects, - oid: git2::Oid, + oid: gix_hash::ObjectId, ) -> anyhow::Result> { let mut buffer = Vec::new(); let data = src - .try_find(&to_gix(oid), &mut buffer) + .try_find(&oid, &mut buffer) .map_err(|e| anyhow::anyhow!("read tree {}: {}", oid, e))? .ok_or_else(|| anyhow::anyhow!("object {} not found", oid))?; if data.kind != gix_object::Kind::Tree { @@ -760,13 +760,13 @@ fn read_tree_entries( /// Emit every blob path under `oid` (a tree), prefixed with `prefix`. fn collect_paths( src: &dyn Objects, - oid: git2::Oid, + oid: gix_hash::ObjectId, prefix: &str, out: &mut Vec, ) -> anyhow::Result<()> { let mut buffer = Vec::new(); let Some(data) = src - .try_find(&to_gix(oid), &mut buffer) + .try_find(&oid, &mut buffer) .map_err(|e| anyhow::anyhow!("read tree {}: {}", oid, e))? else { return Ok(()); @@ -779,7 +779,7 @@ fn collect_paths( let name = std::str::from_utf8(&entry.filename)?; let path = join_path(prefix, name); if entry.mode.is_tree() { - collect_paths(src, to_git2(entry.oid), &path, out)?; + collect_paths(src, entry.oid.to_owned(), &path, out)?; } else if !entry.mode.is_commit() { out.push(path); } @@ -799,7 +799,7 @@ type SearchMatchesResult = Vec<(String, Vec<(usize, String)>)>; pub fn search_matches( src: &dyn Objects, - tree: git2::Oid, + tree: gix_hash::ObjectId, searchstring: &str, candidates: &Vec, ) -> anyhow::Result { @@ -825,9 +825,9 @@ pub fn search_matches( } /// Like [`read_blob_text`], but for a path inside `tree`. -fn get_blob_path(src: &dyn Objects, tree: git2::Oid, path: &std::path::Path) -> String { +fn get_blob_path(src: &dyn Objects, tree: gix_hash::ObjectId, path: &std::path::Path) -> String { match path_entry(src, tree, path) { - Ok(Some(oid)) => read_blob_text(src, to_gix(oid)), + Ok(Some(oid)) => read_blob_text(src, oid), _ => "".to_owned(), } } @@ -835,15 +835,15 @@ fn get_blob_path(src: &dyn Objects, tree: git2::Oid, path: &std::path::Path) -> /// The oid at `path` inside `tree`, or `None` when any component is missing or not a tree. fn path_entry( src: &dyn Objects, - tree: git2::Oid, + tree: gix_hash::ObjectId, path: &std::path::Path, -) -> anyhow::Result> { +) -> anyhow::Result> { let mut current = tree; let mut components = path.components().peekable(); while let Some(component) = components.next() { let mut buffer = Vec::new(); let Some(data) = src - .try_find(&to_gix(current), &mut buffer) + .try_find(¤t, &mut buffer) .map_err(|e| anyhow::anyhow!("read tree {}: {}", current, e))? else { return Ok(None); @@ -856,7 +856,7 @@ fn path_entry( let Some(entry) = parsed.entries.iter().find(|e| e.filename == name) else { return Ok(None); }; - current = to_git2(entry.oid.to_owned()); + current = entry.oid.to_owned(); if components.peek().is_some() && !entry.mode.is_tree() { return Ok(None); } @@ -867,6 +867,13 @@ fn path_entry( #[cfg(test)] mod tests { use super::*; + fn gix(oid: git2::Oid) -> gix_hash::ObjectId { + gix_hash::ObjectId::from_bytes_or_panic(oid.as_bytes()) + } + + fn git2(oid: gix_hash::ObjectId) -> git2::Oid { + git2::Oid::from_bytes(oid.as_bytes()).expect("SHA-1 object id") + } #[test] fn distinct_trigrams_basics() { @@ -896,14 +903,14 @@ mod tests { #[derive(Default)] struct MapCache { - map: std::cell::RefCell>, + map: std::cell::RefCell>, } impl IndexCache for MapCache { - fn get_index(&self, tree: git2::Oid) -> Option { + fn get_index(&self, tree: gix_hash::ObjectId) -> Option { self.map.borrow().get(&tree).copied() } - fn set_index(&self, tree: git2::Oid, index: git2::Oid) { + fn set_index(&self, tree: gix_hash::ObjectId, index: gix_hash::ObjectId) { self.map.borrow_mut().insert(tree, index); } } @@ -946,8 +953,13 @@ mod tests { ], ); - let index = - trigram_index(&objects(&repo), &cache, &mut Indexer::default(), tree.id()).unwrap(); + let index = trigram_index( + &objects(&repo), + &cache, + &mut Indexer::default(), + gix(tree.id()), + ) + .unwrap(); // The index format is pinned: cached indexes of older josh versions stay valid only as // long as this oid does not change. @@ -966,35 +978,42 @@ mod tests { .unwrap() .unwrap(); assert_eq!( - repo.find_object(leaf, None).unwrap().kind(), + repo.find_object(git2(leaf), None).unwrap().kind(), Some(git2::ObjectType::Blob) ); // Coarse hits expand to every file under the directory; verification is exact. - let candidates = search_candidates(&objects(&repo), index, tree.id(), "document").unwrap(); + let candidates = + search_candidates(&objects(&repo), index, gix(tree.id()), "document").unwrap(); assert_eq!(candidates, vec!["sub1/file1", "sub1/file2"]); - let matches = search_matches(&objects(&repo), tree.id(), "document", &candidates).unwrap(); + let matches = + search_matches(&objects(&repo), gix(tree.id()), "document", &candidates).unwrap(); assert_eq!(matches.len(), 2); // Trigrams are case-folded, so candidates are a case-insensitive superset ("Test" in // file1 makes sub1 a candidate for "test") while match verification stays byte-exact. - let candidates = search_candidates(&objects(&repo), index, tree.id(), "test").unwrap(); + let candidates = search_candidates(&objects(&repo), index, gix(tree.id()), "test").unwrap(); assert_eq!(candidates, vec!["sub1/file1", "sub1/file2"]); - let matches = search_matches(&objects(&repo), tree.id(), "test", &candidates).unwrap(); + let matches = search_matches(&objects(&repo), gix(tree.id()), "test", &candidates).unwrap(); assert!(matches.is_empty()); let candidates = - search_candidates(&objects(&repo), index, tree.id(), "missingword").unwrap(); + search_candidates(&objects(&repo), index, gix(tree.id()), "missingword").unwrap(); assert!(candidates.is_empty()); // Short query: every file is a candidate. - let candidates = search_candidates(&objects(&repo), index, tree.id(), "e").unwrap(); + let candidates = search_candidates(&objects(&repo), index, gix(tree.id()), "e").unwrap(); assert_eq!(candidates.len(), 3); // Indexing is deterministic and memoization-independent. let cold = MapCache::default(); - let index2 = - trigram_index(&objects(&repo), &cold, &mut Indexer::default(), tree.id()).unwrap(); + let index2 = trigram_index( + &objects(&repo), + &cold, + &mut Indexer::default(), + gix(tree.id()), + ) + .unwrap(); assert_eq!(index, index2); } @@ -1018,8 +1037,13 @@ mod tests { let files: Vec<(&str, &str)> = files.iter().map(|(p, c)| (&p[..], &c[..])).collect(); let tree = commit_tree(&repo, &files); - let index = - trigram_index(&objects(&repo), &cache, &mut Indexer::default(), tree.id()).unwrap(); + let index = trigram_index( + &objects(&repo), + &cache, + &mut Indexer::default(), + gix(tree.id()), + ) + .unwrap(); // Fine: "d07" (from uniqueword07) mirrors big's structure down to the file. let leaf = path_entry( @@ -1030,7 +1054,7 @@ mod tests { .unwrap() .unwrap(); assert_eq!( - repo.find_object(leaf, None).unwrap().kind(), + repo.find_object(git2(leaf), None).unwrap().kind(), Some(git2::ObjectType::Blob) ); @@ -1043,29 +1067,39 @@ mod tests { .unwrap() .unwrap(); assert_eq!( - repo.find_object(leaf, None).unwrap().kind(), + repo.find_object(git2(leaf), None).unwrap().kind(), Some(git2::ObjectType::Blob) ); // Fine candidates stay per-file and exact. let candidates = - search_candidates(&objects(&repo), index, tree.id(), "uniqueword07").unwrap(); + search_candidates(&objects(&repo), index, gix(tree.id()), "uniqueword07").unwrap(); assert_eq!(candidates, vec!["big/file_07"]); // A coarse hit makes every file under the directory a candidate; verification is // exact. let candidates = - search_candidates(&objects(&repo), index, tree.id(), "needleinsmall").unwrap(); + search_candidates(&objects(&repo), index, gix(tree.id()), "needleinsmall").unwrap(); assert_eq!(candidates, vec!["small/a", "small/b"]); - let matches = - search_matches(&objects(&repo), tree.id(), "needleinsmall", &candidates).unwrap(); + let matches = search_matches( + &objects(&repo), + gix(tree.id()), + "needleinsmall", + &candidates, + ) + .unwrap(); assert_eq!(matches.len(), 1); assert_eq!(matches[0].0, "small/a"); // Determinism across memoization states, coarse dirs included. let cold = MapCache::default(); - let index2 = - trigram_index(&objects(&repo), &cold, &mut Indexer::default(), tree.id()).unwrap(); + let index2 = trigram_index( + &objects(&repo), + &cold, + &mut Indexer::default(), + gix(tree.id()), + ) + .unwrap(); assert_eq!(index, index2); } @@ -1086,7 +1120,7 @@ mod tests { ("sub2/mod", "alpha beta gamma"), ], ); - trigram_index(&objects(&repo), &cache, &mut indexer, tree_a.id()).unwrap(); + trigram_index(&objects(&repo), &cache, &mut indexer, gix(tree_a.id())).unwrap(); // One file modified, one removed (its unique trigrams must vanish from the spine), one // added in a fresh directory. @@ -1098,25 +1132,32 @@ mod tests { ("sub3/new", "fresh addition here"), ], ); - let index_b = trigram_index(&objects(&repo), &cache, &mut indexer, tree_b.id()).unwrap(); + let index_b = + trigram_index(&objects(&repo), &cache, &mut indexer, gix(tree_b.id())).unwrap(); // The roots are memoized in the persistent cache. (The sub dirs are below the coarse // threshold and live in the Indexer's coarse memo instead — only fine-grained indexes // go through the IndexCache.) - assert!(cache.get_index(tree_b.id()).is_some()); + assert!(cache.get_index(gix(tree_b.id())).is_some()); // Warm (incremental) and cold-built indexes agree bit for bit. let cold = MapCache::default(); - let index_b_cold = - trigram_index(&objects(&repo), &cold, &mut Indexer::default(), tree_b.id()).unwrap(); + let index_b_cold = trigram_index( + &objects(&repo), + &cold, + &mut Indexer::default(), + gix(tree_b.id()), + ) + .unwrap(); assert_eq!(index_b, index_b_cold); // The incremental index searches correctly. - let hits = search_candidates(&objects(&repo), index_b, tree_b.id(), "delta").unwrap(); + let hits = search_candidates(&objects(&repo), index_b, gix(tree_b.id()), "delta").unwrap(); assert_eq!(hits, vec!["sub2/mod"]); - let hits = search_candidates(&objects(&repo), index_b, tree_b.id(), "zebra").unwrap(); + let hits = search_candidates(&objects(&repo), index_b, gix(tree_b.id()), "zebra").unwrap(); assert!(hits.is_empty()); - let hits = search_candidates(&objects(&repo), index_b, tree_b.id(), "addition").unwrap(); + let hits = + search_candidates(&objects(&repo), index_b, gix(tree_b.id()), "addition").unwrap(); assert_eq!(hits, vec!["sub3/new"]); } } diff --git a/josh-starlark/Cargo.toml b/josh-starlark/Cargo.toml index 81d351f1b..dcec16daf 100644 --- a/josh-starlark/Cargo.toml +++ b/josh-starlark/Cargo.toml @@ -14,10 +14,13 @@ starlark = "0.14.2" allocative = "0.3" anyhow.workspace = true -git2.workspace = true josh-filter.workspace = true josh-gix-ext.workspace = true +gix-hash.workspace = true gix-object.workspace = true +[dev-dependencies] +git2.workspace = true + diff --git a/josh-starlark/src/evaluate.rs b/josh-starlark/src/evaluate.rs index 04c64c9ef..77a66f8e2 100644 --- a/josh-starlark/src/evaluate.rs +++ b/josh-starlark/src/evaluate.rs @@ -23,7 +23,7 @@ use starlark::{ /// The evaluation is synchronous; no threads are spawned and no values escape. pub fn evaluate( script: &str, - tree_oid: git2::Oid, + tree_oid: gix_hash::ObjectId, objects: &dyn gix_object::Find, ) -> anyhow::Result { // Parse the starlark script diff --git a/josh-starlark/src/filter.rs b/josh-starlark/src/filter.rs index 539018e69..269ce1827 100644 --- a/josh-starlark/src/filter.rs +++ b/josh-starlark/src/filter.rs @@ -8,19 +8,15 @@ use starlark::{ use std::fmt::{self, Display}; use std::path::PathBuf; -/// Opaque Filter type for Starlark -/// We wrap Filter in a newtype that implements the required traits +/// Opaque Starlark filter. #[derive(Debug, Clone, Copy, ProvidesStaticType, NoSerialize)] pub struct StarlarkFilter { pub filter: Filter, } -// Implement Allocative manually since Filter doesn't implement it -// Filter is just a wrapper around git2::Oid which is Copy and small +// Filter owns no heap allocations. impl Allocative for StarlarkFilter { - fn visit<'a, 'b: 'a>(&self, _visitor: &'a mut allocative::Visitor<'b>) { - // Filter contains only a git2::Oid which is Copy and doesn't need visiting - } + fn visit<'a, 'b: 'a>(&self, _visitor: &'a mut allocative::Visitor<'b>) {} } starlark_simple_value!(StarlarkFilter); diff --git a/josh-starlark/src/tests.rs b/josh-starlark/src/tests.rs index 6087fd024..9aafb0c9f 100644 --- a/josh-starlark/src/tests.rs +++ b/josh-starlark/src/tests.rs @@ -1,12 +1,13 @@ use crate::evaluate::evaluate; use josh_filter::spec; +use std::str::FromStr; #[test] fn test_simple_filter() -> anyhow::Result<()> { let temp_dir = std::env::temp_dir().join("josh_starlark_test"); let _ = std::fs::remove_dir_all(&temp_dir); let repo = git2::Repository::init(&temp_dir)?; - let empty_tree_oid = git2::Oid::from_str("4b825dc642cb6eb9a060e54bf8d69288fbee4904")?; + let empty_tree_oid = gix_hash::ObjectId::from_str("4b825dc642cb6eb9a060e54bf8d69288fbee4904")?; let script = r#" filter = filter.subdir("src") @@ -23,7 +24,7 @@ fn test_chain_filter() -> anyhow::Result<()> { let temp_dir = std::env::temp_dir().join("josh_starlark_test2"); let _ = std::fs::remove_dir_all(&temp_dir); let repo = git2::Repository::init(&temp_dir)?; - let empty_tree_oid = git2::Oid::from_str("4b825dc642cb6eb9a060e54bf8d69288fbee4904")?; + let empty_tree_oid = gix_hash::ObjectId::from_str("4b825dc642cb6eb9a060e54bf8d69288fbee4904")?; let script = r#" filter = filter.subdir("src").prefix("lib") @@ -40,7 +41,7 @@ fn test_file_filter() -> anyhow::Result<()> { let temp_dir = std::env::temp_dir().join("josh_starlark_test3"); let _ = std::fs::remove_dir_all(&temp_dir); let repo = git2::Repository::init(&temp_dir)?; - let empty_tree_oid = git2::Oid::from_str("4b825dc642cb6eb9a060e54bf8d69288fbee4904")?; + let empty_tree_oid = gix_hash::ObjectId::from_str("4b825dc642cb6eb9a060e54bf8d69288fbee4904")?; let script = r#" filter = filter.file("README.md") @@ -58,7 +59,7 @@ fn test_compose() -> anyhow::Result<()> { let temp_dir = std::env::temp_dir().join("josh_starlark_test4"); let _ = std::fs::remove_dir_all(&temp_dir); let repo = git2::Repository::init(&temp_dir)?; - let empty_tree_oid = git2::Oid::from_str("4b825dc642cb6eb9a060e54bf8d69288fbee4904")?; + let empty_tree_oid = gix_hash::ObjectId::from_str("4b825dc642cb6eb9a060e54bf8d69288fbee4904")?; let script = r#" f1 = filter.subdir("src") @@ -74,7 +75,7 @@ filter = compose([f1, f2]) } // Helper function to create a test repository with files and directories -fn create_test_repo() -> anyhow::Result<(git2::Repository, git2::Oid)> { +fn create_test_repo() -> anyhow::Result<(git2::Repository, gix_hash::ObjectId)> { // Process id and a counter keep the directory unique across parallel tests; the // timestamp alone collides when two tests read the clock in the same tick. static DIR_COUNTER: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); @@ -119,7 +120,7 @@ fn create_test_repo() -> anyhow::Result<(git2::Repository, git2::Oid)> { root_builder.write()? }; - Ok((repo, root_tree_oid)) + Ok((repo, josh_gix_ext::gix_oid(root_tree_oid))) } #[test] diff --git a/josh-starlark/src/tree.rs b/josh-starlark/src/tree.rs index ae204e436..a3615d312 100644 --- a/josh-starlark/src/tree.rs +++ b/josh-starlark/src/tree.rs @@ -13,7 +13,7 @@ use std::path::PathBuf; /// We wrap a git tree by storing its OID and a raw pointer to the object source it came from. #[derive(Clone, ProvidesStaticType, NoSerialize)] pub(crate) struct StarlarkTree { - pub tree_oid: git2::Oid, + pub tree_oid: gix_hash::ObjectId, // SAFETY: StarlarkTree is only constructed inside `evaluate()`, which is // synchronous and spawns no threads. The referenced object source must // remain alive and at a stable address for that entire duration so this raw @@ -68,7 +68,7 @@ impl StarlarkTree { /// construct `StarlarkTree` values in contexts that guarantee the object source /// outlives the tree and all of its clones, such as the synchronous /// `evaluate()` flow described in the struct-level safety comments. - pub(crate) fn new(tree_oid: git2::Oid, objects: &dyn gix_object::Find) -> Self { + pub(crate) fn new(tree_oid: gix_hash::ObjectId, objects: &dyn gix_object::Find) -> Self { let objects: *const (dyn gix_object::Find + '_) = objects; Self { tree_oid, @@ -85,12 +85,12 @@ impl StarlarkTree { } /// Get empty tree OID - fn empty_tree_oid() -> git2::Oid { - git2::Oid::from_str("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap() + fn empty_tree_oid() -> gix_hash::ObjectId { + gix_hash::ObjectId::empty_tree(gix_hash::Kind::Sha1) } /// Navigate to a path in the tree, returning the OID of the tree at that path - fn navigate_to_path_oid(&self, path: &str) -> anyhow::Result { + fn navigate_to_path_oid(&self, path: &str) -> anyhow::Result { if path.is_empty() { return Ok(self.tree_oid); } @@ -113,7 +113,7 @@ impl StarlarkTree { return Err(anyhow!("Path component '{}' is not a directory", component)); } - current_tree_oid = josh_gix_ext::git2_oid(&entry.oid); + current_tree_oid = entry.oid; } Ok(current_tree_oid) @@ -130,7 +130,7 @@ impl StarlarkTree { if !entry.mode.is_blob() { return String::new(); } - josh_gix_ext::blob_text(objects, josh_gix_ext::git2_oid(&entry.oid)) + josh_gix_ext::blob_text(objects, entry.oid) } /// The full paths of the entries at `path` that satisfy `keep`, in stored tree order. diff --git a/josh-templates/Cargo.toml b/josh-templates/Cargo.toml index 0399b9b48..6793c5ab7 100644 --- a/josh-templates/Cargo.toml +++ b/josh-templates/Cargo.toml @@ -13,7 +13,7 @@ handlebars = "6.4.3" form_urlencoded.workspace = true anyhow.workspace = true -git2.workspace = true +gix-hash.workspace = true juniper.workspace = true serde_json.workspace = true diff --git a/josh-templates/src/templates.rs b/josh-templates/src/templates.rs index d4715601e..9e83b1517 100644 --- a/josh-templates/src/templates.rs +++ b/josh-templates/src/templates.rs @@ -7,7 +7,7 @@ struct GraphQLHelper { repo_path: std::path::PathBuf, cache: std::sync::Arc, ref_prefix: String, - commit_id: git2::Oid, + commit_id: gix_hash::ObjectId, } impl GraphQLHelper { @@ -53,7 +53,7 @@ impl GraphQLHelper { let tree = josh_core::objects::CommitData::read(odb, self.commit_id)?.tree_id()?; let entry = josh_core::objects::path_entry(odb, tree, &path)? .ok_or_else(|| anyhow!("no such path: {}", path.display()))?; - let query = josh_core::objects::blob_text(odb, josh_core::objects::git2_oid(&entry.oid)); + let query = josh_core::objects::blob_text(odb, entry.oid.to_owned()); let mut variables = juniper::Variables::new(); @@ -125,7 +125,7 @@ pub fn render( transaction: &cache::Transaction, cache: std::sync::Arc, ref_prefix: &str, - commit_id: git2::Oid, + commit_id: gix_hash::ObjectId, query_and_params: &str, split_odb: bool, ) -> anyhow::Result)>> { @@ -160,7 +160,7 @@ pub fn render( }; let template = if entry.mode.is_blob() { - let content = josh_core::objects::blob_text(odb, josh_core::objects::git2_oid(&entry.oid)); + let content = josh_core::objects::blob_text(odb, entry.oid.to_owned()); let file = content.as_str(); if cmd == "get" { return Ok(Some((file.to_string(), params))); From b2dbb1dfdf3ec5b36ae2a3bba7a95f8a3e4ac57e Mon Sep 17 00:00:00 2001 From: John Campion Jr Date: Tue, 25 Aug 2026 19:10:15 -0400 Subject: [PATCH 5/5] Support building and running josh on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit josh-proxy and the josh / josh-filter CLIs do not build on Windows: unix-only code is compiled unconditionally (issue #2235). Nothing here changes unix behavior. - josh-gix-ext: add component_bytes, the portable form of the path-component conversion used to match git tree entry names. On unix an OsStr is already bytes; on Windows tree names are conventionally UTF-8, so the UTF-8 encoding is the equivalent, and a component that is not valid Unicode panics rather than silently matching something else. josh-gix-ext, josh-search and josh-core all used std::os::unix::ffi::OsStrExt::as_bytes for this. (rustc suggests std::os::windows::prelude::OsStrExt here, which is not equivalent: that trait yields UTF-16 code units, not bytes.) josh-search gains josh-gix-ext as a dependency; it was already in its build graph via josh-core and in its dev-dependencies. - josh-core: write .gitmodules with LF on every platform. gix-config writes the platform's newline, and the result becomes a blob, so the same filter applied to the same input produced different objects on Windows than elsewhere. - josh-rpc: gate the tokio_fd module behind cfg(unix). It wraps raw fds with libc for the SSH shell; josh-proxy itself only uses josh_rpc::calls, which is plain serde types. This is the first compile error (E0433). - josh-proxy: split serve_namespace cfg(unix)/cfg(not(unix)). SSH serving passes git stdio through unix sockets created by josh-ssh-shell, so non-unix builds return an "unsupported on this platform" error from that endpoint instead of failing to compile. HTTP serving is unaffected. - josh-proxy: install hooks portably. Symlink on unix (unchanged), sh shim on Windows, where symlinking needs elevated privileges. Hook mode is dispatched by argv[0], which a shim cannot fake for a native executable, so the shim names the hook in JOSH_PROXY_HOOK, read once and only on Windows. The shim runs an absolute, quoted path: git runs hooks with GIT_DIR as the working directory. The hooks directory is created with fs::create_dir_all rather than shelling out to mkdir, which is not an executable on Windows. - josh-proxy: bind the listener dual-stack, in make_listener. A bare [::] socket accepts IPv4 on Linux (bindv6only defaults off) but is v6-only on Windows: the proxy starts, logs its address and looks healthy while every client dialing 127.0.0.1 is refused. - josh-cli: refuse `josh compose` on Windows, where podman is not supported, rather than inventing a uid/gid for it. - josh-cli: build valid file:// URLs from local paths with dunce::canonicalize. std's returns an extended-length path, which git rejects inside a file:// URL — issue #2288. - Two tests are gated to unix: one identifies a ref file by inode, and one builds a ref path containing a reserved Windows device name, which Windows cannot represent at all. Adds a Windows CI job in its own workflow, leaving rust.yml untouched. It runs on x86-64 and arm64, builds the supported binaries, runs the unit tests of the crates that build there, and runs functional tests against the built binaries: tests/windows/cli.sh drives the CLI against a local repository, and tests/windows/proxy.sh drives josh-proxy through a filtered clone, a pinned-SHA fetch, a reverse-filter push and reuse of its cache across a restart, with relative and space-laden cache paths as separate cases. josh-proxy needs an http upstream, so tests/windows/serve-git.ps1 hosts git http-backend behind HttpListener; the job installs nothing that Windows does not ship. On arm64 it builds the C dependencies with clang-cl, which the runner image provides: aws-lc-sys' ARM assembly is GNU-syntax and MSVC cannot assemble it. Adds docs/src/contributing/windows.md: setup, build, and the limitations. Everything above passes on Windows x86-64 and ARM64, and on macOS and Linux. Change: windows-support Assisted-By: anthropic/claude-fable-5 Assisted-By: anthropic/claude-opus-5 --- .github/workflows/rust-windows.yml | 77 ++++++++++++++++++ Cargo.lock | 3 + Cargo.toml | 2 + docs/src/SUMMARY.md | 1 + docs/src/contributing/windows.md | 45 +++++++++++ josh-cli/Cargo.toml | 1 + josh-cli/src/bin/josh.rs | 12 ++- josh-cli/src/commands/run.rs | 7 ++ josh-compose-podman/src/lib.rs | 7 ++ josh-core/src/cache/transaction.rs | 5 ++ josh-core/src/filter/tree.rs | 2 +- josh-core/src/submodules.rs | 5 +- josh-gix-ext/src/lib.rs | 17 +++- josh-proxy/Cargo.toml | 2 + josh-proxy/src/bin/josh-proxy.rs | 37 ++++++++- josh-proxy/src/service.rs | 66 +++++++++++++--- josh-rpc/src/lib.rs | 2 + josh-search/Cargo.toml | 2 +- josh-search/src/lib.rs | 2 +- tests/windows/cli.sh | 73 ++++++++++++++++++ tests/windows/proxy.sh | 105 +++++++++++++++++++++++++ tests/windows/run.ps1 | 95 +++++++++++++++++++++++ tests/windows/serve-git.ps1 | 120 +++++++++++++++++++++++++++++ 23 files changed, 667 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/rust-windows.yml create mode 100644 docs/src/contributing/windows.md create mode 100755 tests/windows/cli.sh create mode 100755 tests/windows/proxy.sh create mode 100644 tests/windows/run.ps1 create mode 100644 tests/windows/serve-git.ps1 diff --git a/.github/workflows/rust-windows.yml b/.github/workflows/rust-windows.yml new file mode 100644 index 000000000..bd194a073 --- /dev/null +++ b/.github/workflows/rust-windows.yml @@ -0,0 +1,77 @@ +name: rust-windows + +on: + push: + branches: [ master ] + pull_request: + branches: [ '**' ] + types: [opened, synchronize, reopened, ready_for_review] + merge_group: + +env: + CARGO_TERM_COLOR: always + # aws-lc-sys (rustls' default provider) assembles with NASM on x86-64; the + # crate ships prebuilt objects behind this switch, so the runner needs no + # extra install. ARM64 hosts need clang-cl instead. + AWS_LC_SYS_PREBUILT_NASM: 1 + +jobs: + windows: + strategy: + fail-fast: false + matrix: + include: + - { arch: x86-64, os: windows-latest } + - { arch: arm64, os: windows-11-arm } + runs-on: ${{ matrix.os }} + name: Windows ${{ matrix.arch }} + if: github.event_name != 'pull_request' || github.event.pull_request.draft == false + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # stable + with: + toolchain: stable + # aws-lc-sys' ARM assembly is GNU-syntax, which MSVC cannot assemble: the + # build then fails late in lib.exe, archiving objects that were never + # written. clang-cl assembles it and stays ABI-compatible with the MSVC + # toolchain the Rust side links with. The runner image ships it; the step + # reports what it found, and warns loudly if it ever has to install one. + - name: Use clang-cl for C dependencies + if: matrix.arch == 'arm64' + shell: pwsh + run: | + $clang = Get-Command clang-cl -ErrorAction SilentlyContinue + if ($clang) { + Write-Host "clang-cl: $($clang.Source)" + } else { + $found = Get-ChildItem "$env:ProgramFiles\Microsoft Visual Studio", "$env:ProgramFiles\LLVM" ` + -Recurse -Filter clang-cl.exe -ErrorAction SilentlyContinue | + Select-Object -First 1 -ExpandProperty DirectoryName + if ($found) { + Write-Host "::warning::clang-cl was not on PATH; using $found" + Add-Content $env:GITHUB_PATH $found + } else { + Write-Host "::warning::the runner image no longer ships clang-cl; installing LLVM" + choco install llvm -y --no-progress + Add-Content $env:GITHUB_PATH "$env:ProgramFiles\LLVM\bin" + } + } + # cc-rs splits these on whitespace, so the bare name has to resolve on PATH. + Add-Content $env:GITHUB_ENV "CC=clang-cl" + Add-Content $env:GITHUB_ENV "CXX=clang-cl" + + # josh-ssh-shell is unix-only (unix sockets, fifos, raw fds), so the + # workspace does not build here. + - name: Build + run: cargo build --locked -p josh-proxy -p josh-cli + # josh compose needs podman and does not run on Windows, so the .t suites + # are out of reach; these are the crates whose unit tests run here. + - name: Unit tests + run: cargo test --locked -p josh-core -p josh-filter -p josh-gix-ext -p josh-git-serde -p josh-search -p josh-memodb + # Drives the built binaries, since the .t suites cannot run here: the CLI + # against a local repository, and josh-proxy against a git server hosted by + # HttpListener, so the job needs nothing that Windows does not ship. + - name: Functional tests + run: pwsh tests/windows/run.ps1 target/debug -PathForms diff --git a/Cargo.lock b/Cargo.lock index 557045a49..22d79184c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3329,6 +3329,7 @@ dependencies = [ "clap", "defer", "dirs", + "dunce", "env_logger", "git2", "gix-hash", @@ -3696,6 +3697,7 @@ dependencies = [ "base64 0.23.0", "bon", "clap", + "dunce", "futures", "git2", "gix", @@ -3723,6 +3725,7 @@ dependencies = [ "serde", "serde_json", "sha2", + "socket2", "tempfile", "thiserror 2.0.19", "tokio", diff --git a/Cargo.toml b/Cargo.toml index de203f3e4..0eae93c86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,6 +78,8 @@ serde_json = "1.0.151" serde_yaml = "0.9.34" toml = "1.1.4" tracing-subscriber = { version = "0.3.23", features = ["env-filter"] } +dunce = "1.0.5" +socket2 = "0.6.3" tempfile = "3.27.0" hex = "0.4.3" secret-vault-value = "^1" diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index c8cdd511f..0165810ff 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -29,5 +29,6 @@ # Contributing - [Testing](./contributing/testing.md) - [Development tools](./contributing/dev-tools.md) +- [Windows](./contributing/windows.md) - [josh run](./contributing/josh-run.md) - [Tracing]() diff --git a/docs/src/contributing/windows.md b/docs/src/contributing/windows.md new file mode 100644 index 000000000..b72329d05 --- /dev/null +++ b/docs/src/contributing/windows.md @@ -0,0 +1,45 @@ +# Windows + +Windows support is experimental. `josh-proxy` and the `josh` / `josh-filter` CLIs build and run; +SSH serving and `josh compose` do not (see [Limitations](#limitations)). + +## Setup + +```powershell +winget install Rustlang.Rustup +winget install Microsoft.VisualStudio.2022.BuildTools --override "--wait --passive --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended" +winget install Git.Git +``` + +Git for Windows is needed at runtime, not just to clone: josh shells out to `git`, and the hooks +it installs are `sh` shims that git runs with the bundled sh. + +On ARM64, also install LLVM and build with clang-cl: the `aws-lc-sys` dependency has GNU-syntax +ARM assembly that MSVC cannot assemble, and the build otherwise fails late in `lib.exe` with +LNK1181, archiving object files that were never written. + +```powershell +winget install LLVM.LLVM +$env:Path += ';C:\Program Files\LLVM\bin'; $env:CC='clang-cl'; $env:CXX='clang-cl' +``` + +Pass the bare name via `PATH` rather than a full path in `CC`: cc-rs splits that variable on +whitespace. + +## Build + +`josh-ssh-shell` is unix-only, so build the supported binaries rather than the workspace: + +```powershell +cargo build --release -p josh-proxy -p josh-cli +``` + +## Limitations + +* SSH is not supported on Windows. +* `josh compose run` is not supported on Windows, so the repository's own test suite does not + run there. +* An upstream whose path contains a reserved Windows device name (`aux`, `con`, `nul`, + `com1`..`com9`, `lpt1`..`lpt9`) cannot be mirrored: the namespace becomes a path, and Windows + has no such filename. +* Windows support is experimental. diff --git a/josh-cli/Cargo.toml b/josh-cli/Cargo.toml index 5c7864534..9f25740a6 100644 --- a/josh-cli/Cargo.toml +++ b/josh-cli/Cargo.toml @@ -17,6 +17,7 @@ log.workspace = true serde_json.workspace = true defer.workspace = true clap.workspace = true +dunce.workspace = true juniper.workspace = true git2.workspace = true gix-hash.workspace = true diff --git a/josh-cli/src/bin/josh.rs b/josh-cli/src/bin/josh.rs index 085c5225a..19f7fb8ed 100644 --- a/josh-cli/src/bin/josh.rs +++ b/josh-cli/src/bin/josh.rs @@ -336,12 +336,20 @@ fn to_absolute_remote_url(url: &str) -> anyhow::Result { { Ok(url.to_owned()) } else { - // For local paths, make them absolute - let path = std::fs::canonicalize(url) + // dunce, not std: on Windows std::fs::canonicalize returns an extended-length path + // (\\?\C:\...), which git rejects inside a file:// URL (issue #2288). + let path = dunce::canonicalize(url) .with_context(|| format!("Failed to resolve path {}", url))? .display() .to_string(); + // A UNC path keeps its authority (file://server/share/...); a drive path does not. + #[cfg(windows)] + let path = match path.strip_prefix(r"\\") { + Some(unc) => unc.replace('\\', "/"), + None => format!("/{}", path.replace('\\', "/")), + }; + Ok(format!("file://{}", path)) } } diff --git a/josh-cli/src/commands/run.rs b/josh-cli/src/commands/run.rs index d4bf40306..78b550359 100644 --- a/josh-cli/src/commands/run.rs +++ b/josh-cli/src/commands/run.rs @@ -21,6 +21,13 @@ pub fn handle_compose( args: &ComposeArgs, transaction: &josh_core::cache::Transaction, ) -> anyhow::Result<()> { + #[cfg(windows)] + { + let _ = (args, transaction); + anyhow::bail!("josh compose is not supported on Windows"); + } + + #[cfg(not(windows))] match &args.command { ComposeCommand::Run(run_args) => handle_run(run_args, transaction), ComposeCommand::ListImages(list_args) => handle_list_images(list_args, transaction), diff --git a/josh-compose-podman/src/lib.rs b/josh-compose-podman/src/lib.rs index 24bddb6fc..69940b2d5 100644 --- a/josh-compose-podman/src/lib.rs +++ b/josh-compose-podman/src/lib.rs @@ -38,12 +38,19 @@ impl Default for PodmanRuntime { /// Host uid/gid of the invoking user — the identity container steps run as and /// artifacts are chowned to. This is a container mechanic; the scheduler never /// needs to know it. +#[cfg(unix)] fn host_uid_gid() -> (u32, u32) { let uid = unsafe { libc::getuid() }; let gid = unsafe { libc::getgid() }; (uid, gid) } +/// Unreachable: `josh compose` is refused on Windows before any container runs. +#[cfg(windows)] +fn host_uid_gid() -> (u32, u32) { + unreachable!("josh compose is not supported on Windows") +} + fn host_identity() -> String { let (uid, gid) = host_uid_gid(); format!("{uid}:{gid}") diff --git a/josh-core/src/cache/transaction.rs b/josh-core/src/cache/transaction.rs index 461bde238..dde803552 100644 --- a/josh-core/src/cache/transaction.rs +++ b/josh-core/src/cache/transaction.rs @@ -1841,6 +1841,9 @@ mod tests { assert!(seen.is_empty()); } + // Windows cannot hold this ref at all: a device name is illegal as a path component, so + // an upstream whose path contains one cannot be mirrored there. + #[cfg(unix)] #[test] fn for_each_ref_prefixed_takes_a_prefix_no_worktree_could_hold() { // Upstream namespaces are percent-encoded repository paths, so a prefix component @@ -1865,6 +1868,8 @@ mod tests { assert_eq!(seen, ["refs/josh/upstream/aux/refs/heads/main"]); } + // Identifies the file by inode, so it only makes sense on unix. + #[cfg(unix)] #[test] fn update_ref_to_the_value_a_ref_already_has_writes_nothing() { use std::os::unix::fs::MetadataExt; diff --git a/josh-core/src/filter/tree.rs b/josh-core/src/filter/tree.rs index 61ce8e4ca..21985d67d 100644 --- a/josh-core/src/filter/tree.rs +++ b/josh-core/src/filter/tree.rs @@ -765,7 +765,7 @@ fn intersect_inner( /// The raw bytes of a path component, for matching against tree entry names. fn component_bytes(c: &std::ffi::OsStr) -> &[u8] { - std::os::unix::ffi::OsStrExt::as_bytes(c) + josh_gix_ext::component_bytes(c) } /// Read `oid` as raw tree bytes, or `None` if it is missing or not a tree. Uncached: the diff --git a/josh-core/src/submodules.rs b/josh-core/src/submodules.rs index 27fda2c62..95ac45585 100644 --- a/josh-core/src/submodules.rs +++ b/josh-core/src/submodules.rs @@ -104,7 +104,10 @@ pub fn update_gitmodules( .write_to(&mut output) .context("Failed to write gitmodules")?; - String::from_utf8(output).context("Invalid UTF-8 in gitmodules") + let content = String::from_utf8(output).context("Invalid UTF-8 in gitmodules")?; + // gix-config writes the platform's newline, but this ends up in a blob: the same + // filter has to produce the same object on every platform. + Ok(content.replace("\r\n", "\n")) } #[cfg(test)] diff --git a/josh-gix-ext/src/lib.rs b/josh-gix-ext/src/lib.rs index 2d87e218e..1a48a0408 100644 --- a/josh-gix-ext/src/lib.rs +++ b/josh-gix-ext/src/lib.rs @@ -4,6 +4,21 @@ use std::collections::HashMap; use gix_object::WriteTo; +/// The raw bytes of a path component, for matching against git tree entry names. +#[cfg(unix)] +pub fn component_bytes(c: &std::ffi::OsStr) -> &[u8] { + std::os::unix::ffi::OsStrExt::as_bytes(c) +} + +/// The raw bytes of a path component, for matching against git tree entry names: their UTF-8 +/// encoding. Panics on a component that is not valid Unicode, which cannot name a tree entry. +#[cfg(windows)] +pub fn component_bytes(c: &std::ffi::OsStr) -> &[u8] { + c.to_str() + .expect("path component is not valid Unicode") + .as_bytes() +} + pub mod graph; pub mod merge; pub mod revwalk; @@ -121,7 +136,7 @@ pub fn path_entry( return Ok(None); } let parsed = gix_object::TreeRef::from_bytes(&buffer, gix_hash::Kind::Sha1)?; - let name = std::os::unix::ffi::OsStrExt::as_bytes(component.as_os_str()); + let name = component_bytes(component.as_os_str()); let Some(entry) = parsed.entries.iter().find(|e| e.filename == name) else { return Ok(None); }; diff --git a/josh-proxy/Cargo.toml b/josh-proxy/Cargo.toml index eb1b48ff6..aacbeebe5 100644 --- a/josh-proxy/Cargo.toml +++ b/josh-proxy/Cargo.toml @@ -47,6 +47,8 @@ toml.workspace = true tracing.workspace = true tracing-subscriber.workspace = true tokio-util.workspace = true +dunce.workspace = true +socket2.workspace = true tempfile.workspace = true gix.workspace = true juniper.workspace = true diff --git a/josh-proxy/src/bin/josh-proxy.rs b/josh-proxy/src/bin/josh-proxy.rs index a365b550c..3801e4935 100644 --- a/josh-proxy/src/bin/josh-proxy.rs +++ b/josh-proxy/src/bin/josh-proxy.rs @@ -177,7 +177,7 @@ async fn run_proxy(args: josh_proxy::cli::Args) -> anyhow::Result { let (shutdown_tx, _shutdown_rx) = broadcast::channel(1); let addr: SocketAddr = format!("[::]:{}", args.port).parse()?; - let listener = tokio::net::TcpListener::bind(addr).await?; + let listener = make_listener(addr)?; let server_future = async move { axum::serve(listener, app).await.context("Server error") }; @@ -252,6 +252,37 @@ fn update_hook(refname: &str, old: &str, new: &str) -> anyhow::Result { } } +/// Bind the listener dual-stack: a bare [::] socket accepts IPv4 on Linux, where bindv6only +/// defaults off, but is v6-only on Windows. +fn make_listener(addr: SocketAddr) -> anyhow::Result { + let socket = socket2::Socket::new( + socket2::Domain::IPV6, + socket2::Type::STREAM, + Some(socket2::Protocol::TCP), + )?; + socket.set_only_v6(false)?; + socket.bind(&addr.into())?; + socket.listen(1024)?; + socket.set_nonblocking(true)?; + Ok(tokio::net::TcpListener::from_std(socket.into())?) +} + +/// The hook this process was invoked as, for the Windows shim hooks. On unix the hooks are +/// symlinks and argv[0] names them, so the environment is not consulted. +fn hook_from_env() -> Option<&'static str> { + #[cfg(windows)] + { + static JOSH_PROXY_HOOK: std::sync::LazyLock> = + std::sync::LazyLock::new(|| std::env::var("JOSH_PROXY_HOOK").ok()); + JOSH_PROXY_HOOK.as_deref() + } + + #[cfg(not(windows))] + { + None + } +} + fn pre_receive_hook() -> anyhow::Result { let repo_update = repo_update_from_env()?; @@ -286,13 +317,13 @@ fn main() -> std::process::ExitCode { // process to do the actual computation while taking advantage of the // cached data already loaded into the main process's memory. if let [a0, a1, a2, a3, ..] = &std::env::args().collect::>().as_slice() - && a0.ends_with("/update") + && (a0.ends_with("/update") || hook_from_env() == Some("update")) { return std::process::ExitCode::from(update_hook(a1, a2, a3).unwrap_or(1) as u8); } if let [a0, ..] = &std::env::args().collect::>().as_slice() - && a0.ends_with("/pre-receive") + && (a0.ends_with("/pre-receive") || hook_from_env() == Some("pre-receive")) { eprintln!("josh-proxy: pre-receive hook"); return std::process::ExitCode::from(match pre_receive_hook() { diff --git a/josh-proxy/src/service.rs b/josh-proxy/src/service.rs index 722a27881..a59014c85 100644 --- a/josh-proxy/src/service.rs +++ b/josh-proxy/src/service.rs @@ -399,6 +399,38 @@ fn create_repo_base(path: &PathBuf) -> anyhow::Result { Ok(shell) } +#[cfg(unix)] +fn install_hook(josh_executable: &std::path::Path, hook: &std::path::Path) -> std::io::Result<()> { + std::os::unix::fs::symlink(josh_executable, hook) +} + +/// Install the josh executable as a git hook, as a shim: symlinking requires elevated +/// privileges on Windows. Dispatch keys off argv[0], which a shim cannot fake, so the shim +/// names the hook in JOSH_PROXY_HOOK instead. +#[cfg(windows)] +fn install_hook(josh_executable: &std::path::Path, hook: &std::path::Path) -> std::io::Result<()> { + use std::fmt::Write as _; + + let name = hook + .file_name() + .map(|n| n.to_string_lossy()) + .unwrap_or_default(); + // Absolute, because git runs hooks with GIT_DIR as the working directory; quoted, so no + // part of the path is a shell expansion. + let exe = dunce::canonicalize(josh_executable) + .unwrap_or_else(|_| josh_executable.to_path_buf()) + .to_string_lossy() + .replace('\\', "/") + .replace('\'', r"'\''"); + + let mut script = String::new(); + let _ = writeln!(script, "#!/bin/sh"); + let _ = writeln!(script, "JOSH_PROXY_HOOK={name}"); + let _ = writeln!(script, "export JOSH_PROXY_HOOK"); + let _ = writeln!(script, "exec '{exe}' \"$@\""); + std::fs::write(hook, script) +} + pub fn create_repo( path: &std::path::Path, josh_executable: Option<&std::path::Path>, @@ -409,23 +441,20 @@ pub fn create_repo( let overlay_path = path.join("overlay"); tracing::debug!("init overlay repo: {:?}", overlay_path); - let overlay_shell = create_repo_base(&overlay_path)?; - overlay_shell.command(&["mkdir", "hooks"]); + create_repo_base(&overlay_path)?; + std::fs::create_dir_all(overlay_path.join("hooks")).expect("can't create hooks dir"); let josh_executable = josh_executable .map(|p| p.to_path_buf()) .unwrap_or_else(|| std::env::current_exe().expect("can't find path to exe")); - std::os::unix::fs::symlink( - josh_executable.clone(), - overlay_path.join("hooks").join("update"), - ) - .expect("can't symlink update hook"); + install_hook(&josh_executable, &overlay_path.join("hooks").join("update")) + .expect("can't install update hook"); - std::os::unix::fs::symlink( - josh_executable, - overlay_path.join("hooks").join("pre-receive"), + install_hook( + &josh_executable, + &overlay_path.join("hooks").join("pre-receive"), ) - .expect("can't symlink pre-receive hook"); + .expect("can't install pre-receive hook"); if std::env::var_os("JOSH_KEEP_NS").is_none() { std::fs::remove_dir_all(overlay_path.join("refs/namespaces")).ok(); @@ -737,6 +766,21 @@ async fn ssh_list_refs( Ok(refs) } +/// SSH serving relays git's stdio over the unix sockets josh-ssh-shell sets up, so there is +/// nothing to connect to here. +#[cfg(not(unix))] +async fn serve_namespace( + _params: &josh_rpc::calls::ServeNamespace, + _repo_path: std::path::PathBuf, + _namespace: &str, + _repo_update: RepoUpdate, +) -> anyhow::Result<()> { + Err(anyhow!( + "SSH serving requires unix sockets, which this platform does not support" + )) +} + +#[cfg(unix)] async fn serve_namespace( params: &josh_rpc::calls::ServeNamespace, repo_path: std::path::PathBuf, diff --git a/josh-rpc/src/lib.rs b/josh-rpc/src/lib.rs index a6ecda363..47872601a 100644 --- a/josh-rpc/src/lib.rs +++ b/josh-rpc/src/lib.rs @@ -1,2 +1,4 @@ pub mod calls; +// Raw-fd async IO for the SSH shell; unix-only by nature (RawFd, fcntl). +#[cfg(unix)] pub mod tokio_fd; diff --git a/josh-search/Cargo.toml b/josh-search/Cargo.toml index e9806aaef..127e73cb8 100644 --- a/josh-search/Cargo.toml +++ b/josh-search/Cargo.toml @@ -17,10 +17,10 @@ harness = false anyhow.workspace = true gix-hash.workspace = true gix-object.workspace = true +josh-gix-ext.workspace = true [dev-dependencies] git2.workspace = true -josh-gix-ext.workspace = true gix.workspace = true criterion2 = { version = "3.0.4" } rand = "0.10.2" diff --git a/josh-search/src/lib.rs b/josh-search/src/lib.rs index b06e06020..9d96b7029 100644 --- a/josh-search/src/lib.rs +++ b/josh-search/src/lib.rs @@ -852,7 +852,7 @@ fn path_entry( return Ok(None); } let parsed = gix_object::TreeRef::from_bytes(&buffer, gix_hash::Kind::Sha1)?; - let name = std::os::unix::ffi::OsStrExt::as_bytes(component.as_os_str()); + let name = josh_gix_ext::component_bytes(component.as_os_str()); let Some(entry) = parsed.entries.iter().find(|e| e.filename == name) else { return Ok(None); }; diff --git a/tests/windows/cli.sh b/tests/windows/cli.sh new file mode 100755 index 000000000..9c9f8000e --- /dev/null +++ b/tests/windows/cli.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# Functional test for the josh CLI on a platform where `josh compose` cannot +# run. Exercises filtering, cloning, pulling and pushing against a local bare +# repository: no server, no network, nothing but git. +# +# tests/windows/cli.sh +# +# The clone deliberately targets a relative directory, which is what turns a +# path into a remote URL internally — the case that was broken on Windows. +set -euo pipefail + +BIN_DIR="$(cd "$1" && pwd)" +EXE="" +[ -f "$BIN_DIR/josh.exe" ] && EXE=".exe" +JOSH="$BIN_DIR/josh$EXE" +JOSH_FILTER="$BIN_DIR/josh-filter$EXE" +[ -f "$JOSH" ] || { echo "FAIL: $JOSH not found" >&2; exit 1; } +[ -f "$JOSH_FILTER" ] || { echo "FAIL: $JOSH_FILTER not found" >&2; exit 1; } + +WORK="$(mktemp -d)" +BRANCH="main" +fail() { echo "FAIL: $*" >&2; exit 1; } + +echo "== setup: local upstream" +git init -q --bare -b "$BRANCH" "$WORK/upstream.git" +git init -q -b "$BRANCH" "$WORK/seed" +git -C "$WORK/seed" config user.email t@t +git -C "$WORK/seed" config user.name t +echo hello > "$WORK/seed/README.md" +git -C "$WORK/seed" add . && git -C "$WORK/seed" commit -qm "c1: readme" +mkdir -p "$WORK/seed/src" && echo lib > "$WORK/seed/src/lib.txt" +git -C "$WORK/seed" add . && git -C "$WORK/seed" commit -qm "c2: lib" +git -C "$WORK/seed" push -q "$WORK/upstream.git" "$BRANCH" + +echo "== josh-filter" +git clone -q "$WORK/upstream.git" "$WORK/plain" +(cd "$WORK/plain" && "$JOSH_FILTER" ":prefix=lib" "$BRANCH") || fail "josh-filter" +git -C "$WORK/plain" ls-tree --name-only -r FILTERED_HEAD | grep -qx "lib/README.md" \ + || fail "josh-filter: prefix missing from FILTERED_HEAD" +[ "$(git -C "$WORK/plain" rev-list --count FILTERED_HEAD)" = 2 ] \ + || fail "josh-filter: expected 2 commits" + +echo "== josh clone, into a relative directory" +mkdir -p "$WORK/cli" && cd "$WORK/cli" +"$JOSH" clone "$WORK/upstream.git" ":prefix=lib" ./clone || fail "josh clone" +[ -f "$WORK/cli/clone/lib/README.md" ] || fail "josh clone: prefix missing" +[ "$(git -C "$WORK/cli/clone" rev-list --count HEAD)" = 2 ] \ + || fail "josh clone: expected 2 commits" + +echo "== josh changes pull" +echo more >> "$WORK/seed/src/lib.txt" +git -C "$WORK/seed" commit -qam "c3: more lib" +C3="$(git -C "$WORK/seed" rev-parse HEAD)" +git -C "$WORK/seed" push -q "$WORK/upstream.git" "$BRANCH" +(cd "$WORK/cli/clone" && "$JOSH" changes pull) || fail "josh changes pull" +grep -q more "$WORK/cli/clone/lib/src/lib.txt" \ + || fail "josh changes pull: upstream change did not arrive through the filter" + +echo "== josh push" +cd "$WORK/cli/clone" +git config user.email t@t +git config user.name t +echo change >> lib/src/lib.txt +git commit -qam "c4: change through the filter" +"$JOSH" push origin "HEAD:refs/heads/roundtrip" --base "$BRANCH" || fail "josh push" +RT="$(git -C "$WORK/upstream.git" rev-parse refs/heads/roundtrip)" \ + || fail "josh push: branch missing upstream" +git -C "$WORK/upstream.git" show "$RT:src/lib.txt" | grep -q change \ + || fail "josh push: change not reverse-filtered to src/lib.txt" +[ "$(git -C "$WORK/upstream.git" rev-parse "$RT^")" = "$C3" ] \ + || fail "josh push: pushed commit is not rooted on the upstream tip" + +echo "PASS" diff --git a/tests/windows/proxy.sh b/tests/windows/proxy.sh new file mode 100755 index 000000000..ec876375e --- /dev/null +++ b/tests/windows/proxy.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +# Functional test for josh-proxy on a platform where `josh compose` cannot run. +# Drives a real proxy against a real upstream: filtered clone, pinned-SHA fetch, +# reverse-filter push, and reuse of the --local cache across a restart. +# +# UPSTREAM_URL=http://127.0.0.1:8177 tests/windows/proxy.sh [cache-dir] +# +# UPSTREAM_URL is the base URL of a git server exporting the repositories in +# UPSTREAM_ROOT (tests/windows/serve-git.ps1 provides one on Windows). The +# optional cache directory lets a caller exercise unusual path forms. +set -euo pipefail + +JOSH_PROXY="$1" +UPSTREAM_ROOT="${UPSTREAM_ROOT:?set UPSTREAM_ROOT to the served directory}" +UPSTREAM_URL="${UPSTREAM_URL:?set UPSTREAM_URL to the serving base URL}" +WORK="$(mktemp -d)" +LOCAL_DIR="${2:-$WORK/local}" +PORT="${JOSH_PORT:-42190}" +BRANCH="main" + +JOSH_PID="" +cleanup() { [ -n "$JOSH_PID" ] && kill "$JOSH_PID" 2>/dev/null || true; } +trap cleanup EXIT + +fail() { + echo "FAIL: $*" >&2 + [ -s "$WORK/josh.log" ] && { echo "--- josh-proxy log:" >&2; cat "$WORK/josh.log" >&2; } + exit 1 +} + +start_proxy() { + "$JOSH_PROXY" --local "$LOCAL_DIR" --remote "$UPSTREAM_URL" \ + "--port=$PORT" --no-background >>"$WORK/josh.log" 2>&1 & + JOSH_PID=$! + for _ in $(seq 1 100); do + curl -s -o /dev/null --max-time 1 "http://127.0.0.1:$PORT/" && return 0 + [ "$?" -ne 7 ] && return 0 + sleep 0.1 + done + fail "josh-proxy did not start" +} + +stop_proxy() { + kill "$JOSH_PID" 2>/dev/null || true + for _ in $(seq 1 20); do kill -0 "$JOSH_PID" 2>/dev/null || break; sleep 0.1; done + kill -0 "$JOSH_PID" 2>/dev/null && fail "josh-proxy did not exit when terminated" + JOSH_PID="" +} + +echo "== setup: upstream repository" +rm -rf "$UPSTREAM_ROOT/upstream.git" +git init -q --bare -b "$BRANCH" "$UPSTREAM_ROOT/upstream.git" +git -C "$UPSTREAM_ROOT/upstream.git" config http.receivepack true +git init -q -b "$BRANCH" "$WORK/seed" +git -C "$WORK/seed" config user.email t@t +git -C "$WORK/seed" config user.name t +echo hello > "$WORK/seed/README.md" +git -C "$WORK/seed" add . && git -C "$WORK/seed" commit -qm "c1: readme" +C1="$(git -C "$WORK/seed" rev-parse HEAD)" +mkdir -p "$WORK/seed/src" && echo lib > "$WORK/seed/src/lib.txt" +git -C "$WORK/seed" add . && git -C "$WORK/seed" commit -qm "c2: lib" +C2="$(git -C "$WORK/seed" rev-parse HEAD)" +git -C "$WORK/seed" push -q "$UPSTREAM_ROOT/upstream.git" "$BRANCH" + +echo "== boot" +start_proxy +FILTERED="http://127.0.0.1:$PORT/upstream.git:prefix=lib.git" + +echo "== filtered clone" +git clone -q "$FILTERED" "$WORK/clone" || fail "filtered clone" +[ -f "$WORK/clone/lib/README.md" ] || fail "prefix missing from the clone" +[ "$(git -C "$WORK/clone" rev-list --count HEAD)" = 2 ] || fail "expected 2 commits" + +echo "== pinned-SHA fetch" +# The filter separator is also exercised percent-encoded, as clients send it. +git -C "$WORK/clone" fetch -q "http://127.0.0.1:$PORT/upstream.git@$C1%3Aprefix=lib.git" HEAD \ + || fail "pinned fetch" +git -C "$WORK/clone" ls-tree --name-only -r FETCH_HEAD | grep -qx "lib/README.md" \ + || fail "pinned fetch: README missing" +git -C "$WORK/clone" ls-tree --name-only -r FETCH_HEAD | grep -q "lib/src" \ + && fail "pinned fetch resolved past the pinned commit" + +echo "== reverse push" +git -C "$WORK/clone" config user.email t@t +git -C "$WORK/clone" config user.name t +echo change >> "$WORK/clone/lib/src/lib.txt" +git -C "$WORK/clone" commit -qam "c3: change through the filter" +git -C "$WORK/clone" push -q -o "base=refs/heads/$BRANCH" origin HEAD:refs/heads/roundtrip \ + || fail "reverse push" +RT="$(git -C "$UPSTREAM_ROOT/upstream.git" rev-parse refs/heads/roundtrip)" \ + || fail "reverse push: branch missing upstream" +git -C "$UPSTREAM_ROOT/upstream.git" show "$RT:src/lib.txt" | grep -q change \ + || fail "reverse push: change not reverse-filtered to src/lib.txt" +[ "$(git -C "$UPSTREAM_ROOT/upstream.git" rev-parse "$RT^")" = "$C2" ] \ + || fail "reverse push: pushed commit is not rooted on the upstream tip" + +echo "== cache reuse across a restart" +# Consumers run one proxy per operation rather than a daemon, so the --local +# cache has to survive a clean stop and serve the next instance. +stop_proxy +start_proxy +git -C "$WORK/clone" fetch -q origin || fail "fetch against the reused cache" + +stop_proxy +echo "PASS" diff --git a/tests/windows/run.ps1 b/tests/windows/run.ps1 new file mode 100644 index 000000000..f34920031 --- /dev/null +++ b/tests/windows/run.ps1 @@ -0,0 +1,95 @@ +<# +.SYNOPSIS +Run the Windows functional tests against built josh binaries. + +.DESCRIPTION +`josh compose` needs podman and does not run on Windows, so the .t suites are +out of reach there. These drive the built binaries directly: the CLI against a +local repository, and josh-proxy against a git server hosted by serve-git.ps1. + +.PARAMETER BinDir +Directory holding josh-proxy.exe, josh.exe and josh-filter.exe. + +.PARAMETER PathForms +Also run the proxy tests with relative, space-laden and junction cache +directories. + +.EXAMPLE +tests\windows\run.ps1 target\release +#> +param( + [Parameter(Mandatory = $true)][string]$BinDir, + [switch]$PathForms +) + +$ErrorActionPreference = 'Stop' + +$bash = @( + "$env:ProgramFiles\Git\bin\bash.exe", + "${env:ProgramFiles(x86)}\Git\bin\bash.exe", + "$env:LOCALAPPDATA\Programs\Git\bin\bash.exe" +) | Where-Object { Test-Path $_ } | Select-Object -First 1 +if (-not $bash) { throw "Git Bash not found; install Git for Windows" } + +$BinDir = (Resolve-Path $BinDir).Path +$here = $PSScriptRoot +$port = 8177 + +# Git Bash passes these to native tools, which want /c/... spellings. +function To-BashPath([string]$p) { + $p = $p -replace '\\', '/' + if ($p -match '^([A-Za-z]):(.*)$') { return "/$($Matches[1].ToLower())$($Matches[2])" } + return $p +} + +$results = [ordered]@{} +function Run-Test([string]$name, [string[]]$bashArgs, [hashtable]$extraEnv = @{}) { + Write-Host "`n=== $name" -ForegroundColor Cyan + foreach ($k in $extraEnv.Keys) { Set-Item "env:$k" $extraEnv[$k] } + & $bash @bashArgs + $results[$name] = ($LASTEXITCODE -eq 0) +} + +Run-Test 'cli' @((To-BashPath "$here\cli.sh"), (To-BashPath $BinDir)) + +$served = Join-Path ([System.IO.Path]::GetTempPath()) "josh-served-$PID" +New-Item -ItemType Directory -Force -Path $served | Out-Null +$server = Start-Process pwsh -PassThru -WindowStyle Hidden -ArgumentList @( + '-NoProfile', '-File', "$here\serve-git.ps1", '-Root', $served, '-Port', $port) + +try { + $ready = $false + foreach ($i in 1..100) { + try { (New-Object Net.Sockets.TcpClient('127.0.0.1', $port)).Close(); $ready = $true; break } + catch { Start-Sleep -Milliseconds 100 } + } + if (-not $ready) { throw "git server did not start on port $port" } + + $proxy = Join-Path $BinDir 'josh-proxy.exe' + $env:UPSTREAM_URL = "http://127.0.0.1:$port" + + Run-Test 'proxy' @((To-BashPath "$here\proxy.sh"), (To-BashPath $proxy)) ` + @{ UPSTREAM_ROOT = (To-BashPath $served) } + + if ($PathForms) { + $tmp = $env:TEMP + foreach ($case in @( + @{ name = 'proxy: relative cache path'; dir = './josh-rel' }, + @{ name = 'proxy: cache path with spaces'; dir = (To-BashPath (Join-Path $tmp 'josh cache spaces')) } + )) { + Run-Test $case.name @((To-BashPath "$here\proxy.sh"), (To-BashPath $proxy), $case.dir) ` + @{ UPSTREAM_ROOT = (To-BashPath $served) } + } + } +} finally { + if ($server -and -not $server.HasExited) { Stop-Process -Id $server.Id -Force } + Remove-Item -Recurse -Force $served -ErrorAction SilentlyContinue +} + +Write-Host "`n=== verdict" -ForegroundColor Cyan +foreach ($k in $results.Keys) { + if ($results[$k]) { Write-Host " PASS $k" -ForegroundColor Green } + else { Write-Host " FAIL $k" -ForegroundColor Red } +} +if ($results.Values -contains $false) { exit 1 } +Write-Host "`nAll Windows functional tests passed." -ForegroundColor Green diff --git a/tests/windows/serve-git.ps1 b/tests/windows/serve-git.ps1 new file mode 100644 index 000000000..5ec350049 --- /dev/null +++ b/tests/windows/serve-git.ps1 @@ -0,0 +1,120 @@ +<# +.SYNOPSIS +Serve bare git repositories over smart HTTP, for tests. + +.DESCRIPTION +josh-proxy only accepts an http(s) or ssh upstream, so testing it needs a git +server. Rather than add a dependency, this hosts git's own http-backend as CGI +behind System.Net.HttpListener, which ships with Windows. + +Prints the URL it is serving on, then runs until stopped. + +.PARAMETER Root +Directory holding bare repositories (GIT_PROJECT_ROOT). + +.PARAMETER Port +Port to listen on. 127.0.0.1 only. +#> +param( + [Parameter(Mandatory = $true)][string]$Root, + [int]$Port = 8177 +) + +$ErrorActionPreference = 'Stop' + +$git = (Get-Command git).Source +$root = (Resolve-Path $Root).Path + +$listener = [System.Net.HttpListener]::new() +$listener.Prefixes.Add("http://127.0.0.1:$Port/") +$listener.Start() +Write-Host "serving $root on http://127.0.0.1:$Port/" + +try { + while ($listener.IsListening) { + $context = $listener.GetContext() + $request = $context.Request + $response = $context.Response + + $psi = [System.Diagnostics.ProcessStartInfo]::new($git, 'http-backend') + $psi.UseShellExecute = $false + $psi.RedirectStandardInput = $true + $psi.RedirectStandardOutput = $true + $psi.RedirectStandardError = $true + + $psi.Environment['GIT_PROJECT_ROOT'] = $root + $psi.Environment['GIT_HTTP_EXPORT_ALL'] = '1' + $psi.Environment['REQUEST_METHOD'] = $request.HttpMethod + $psi.Environment['PATH_INFO'] = [Uri]::UnescapeDataString($request.Url.AbsolutePath) + $psi.Environment['QUERY_STRING'] = $request.Url.Query.TrimStart('?') + $psi.Environment['REMOTE_ADDR'] = $request.RemoteEndPoint.Address.ToString() + $psi.Environment['REMOTE_USER'] = 'test' + if ($request.ContentType) { $psi.Environment['CONTENT_TYPE'] = $request.ContentType } + if ($request.ContentLength64 -ge 0) { $psi.Environment['CONTENT_LENGTH'] = "$($request.ContentLength64)" } + # http-backend inflates the body itself when told the encoding, and serves + # protocol v2 only when the client's version is passed through. + if ($request.Headers['Content-Encoding']) { + $psi.Environment['HTTP_CONTENT_ENCODING'] = $request.Headers['Content-Encoding'] + } + if ($request.Headers['Git-Protocol']) { + $psi.Environment['GIT_PROTOCOL'] = $request.Headers['Git-Protocol'] + } + + $process = [System.Diagnostics.Process]::Start($psi) + + # git speaks binary over HTTP: every copy is bytes, and stderr is drained on + # its own so a chatty backend cannot fill the pipe and block. + $stderrTask = $process.StandardError.ReadToEndAsync() + if ($request.HasEntityBody) { + $request.InputStream.CopyTo($process.StandardInput.BaseStream) + } + $process.StandardInput.Close() + + $captured = New-Object System.IO.MemoryStream + $process.StandardOutput.BaseStream.CopyTo($captured) + $process.WaitForExit() + $bytes = $captured.ToArray() + + # CGI replies with headers, a blank line, then the body. Buffering the whole + # reply keeps the body's bytes intact and lets the response carry a real + # Content-Length, which the git client needs to know where it ends. + $split = -1 + for ($i = 0; $i -lt $bytes.Length - 1; $i++) { + if ($bytes[$i] -eq 10 -and $bytes[$i + 1] -eq 10) { $split = $i + 2; break } + if ($i -lt $bytes.Length - 3 -and $bytes[$i] -eq 13 -and $bytes[$i + 1] -eq 10 ` + -and $bytes[$i + 2] -eq 13 -and $bytes[$i + 3] -eq 10) { $split = $i + 4; break } + } + if ($split -lt 0) { + $response.StatusCode = 500 + $message = [System.Text.Encoding]::UTF8.GetBytes("no CGI reply from git http-backend`n$($stderrTask.Result)") + $response.ContentLength64 = $message.Length + $response.OutputStream.Write($message, 0, $message.Length) + $response.OutputStream.Close() + continue + } + + $headerText = [System.Text.Encoding]::ASCII.GetString($bytes, 0, $split) + $body = New-Object byte[] ($bytes.Length - $split) + [Array]::Copy($bytes, $split, $body, 0, $body.Length) + + $response.StatusCode = 200 + foreach ($line in ($headerText -split "`r?`n")) { + if (-not $line) { continue } + $name, $value = $line -split ':\s*', 2 + switch ($name) { + 'Status' { $response.StatusCode = [int]($value -split ' ')[0] } + 'Content-Type' { $response.ContentType = $value } + 'Content-Length' { } # taken from the body below + default { try { $response.Headers[$name] = $value } catch { } } + } + } + + $response.SendChunked = $false + $response.KeepAlive = $false + $response.ContentLength64 = $body.Length + if ($body.Length -gt 0) { $response.OutputStream.Write($body, 0, $body.Length) } + $response.OutputStream.Close() + } +} finally { + $listener.Stop() +}