From b279957beaf581c16293343dbdb2121bd1d4dd1c Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 4 Sep 2024 12:05:19 +0200 Subject: [PATCH] feat: add tree-editing capabilities to `Tree` and `Repository`. Create a tree editor using `Tree::edit()` or `Repository::edit_tree(id)`. --- Cargo.lock | 2 + gix/Cargo.toml | 15 +- gix/src/config/cache/access.rs | 2 +- gix/src/object/tree/editor.rs | 271 +++++++++++++++++++++++++++++++++ gix/src/object/tree/mod.rs | 12 ++ gix/src/repository/mod.rs | 14 ++ gix/src/repository/object.rs | 24 ++- gix/src/types.rs | 2 +- gix/tests/repository/object.rs | 205 +++++++++++++++++++++++++ justfile | 6 +- 10 files changed, 546 insertions(+), 7 deletions(-) create mode 100644 gix/src/object/tree/editor.rs diff --git a/Cargo.lock b/Cargo.lock index 9ddc5a3c30c..c4c7d061430 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1303,6 +1303,7 @@ dependencies = [ "anyhow", "async-std", "document-features", + "gix", "gix-actor 0.32.0", "gix-archive", "gix-attributes 0.22.5", @@ -1360,6 +1361,7 @@ dependencies = [ "serial_test", "signal-hook", "smallvec", + "termtree", "thiserror", "walkdir", ] diff --git a/gix/Cargo.toml b/gix/Cargo.toml index bffb10dc27a..db6616465c2 100644 --- a/gix/Cargo.toml +++ b/gix/Cargo.toml @@ -64,9 +64,12 @@ extras = [ "credentials", "interrupt", "status", - "dirwalk", + "dirwalk" ] +## A collection of features that need a larger MSRV, and thus are disabled by default. +need-more-recent-msrv = ["tree-editor"] + ## Various progress-related features that improve the look of progress message units. comfort = [ "gix-features/progress-unit-bytes", @@ -103,6 +106,12 @@ worktree-mutation = ["attributes", "dep:gix-worktree-state"] ## Retrieve a worktree stack for querying exclude information excludes = ["dep:gix-ignore", "dep:gix-worktree", "index"] +## Provide facilities to edit trees conveniently. +## +## Not that currently, this requires [Rust 1.75](https://caniuse.rs/features/return_position_impl_trait_in_trait). +## This feature toggle is likely going away then. +tree-editor = [] + ## Query attributes and excludes. Enables access to pathspecs, worktree checkouts, filter-pipelines and submodules. attributes = [ "excludes", @@ -384,6 +393,8 @@ parking_lot = { version = "0.12.1", optional = true } document-features = { version = "0.2.0", optional = true } [dev-dependencies] +# For additional features that aren't enabled by default due to MSRV +gix = { path = ".", default-features = false, features = ["tree-editor"] } pretty_assertions = "1.4.0" gix-testtools = { path = "../tests/tools" } is_ci = "1.1.1" @@ -391,6 +402,7 @@ anyhow = "1" walkdir = "2.3.2" serial_test = { version = "3.1.0", default-features = false } async-std = { version = "1.12.0", features = ["attributes"] } +termtree = "0.5.1" [package.metadata.docs.rs] features = [ @@ -398,5 +410,6 @@ features = [ "max-performance", "blocking-network-client", "blocking-http-transport-curl", + "need-more-recent-msrv", "serde", ] diff --git a/gix/src/config/cache/access.rs b/gix/src/config/cache/access.rs index 2ebf2c5c06b..2575901df0e 100644 --- a/gix/src/config/cache/access.rs +++ b/gix/src/config/cache/access.rs @@ -251,7 +251,7 @@ impl Cache { }) } - #[cfg(feature = "index")] + #[cfg(any(feature = "index", feature = "tree-editor"))] pub(crate) fn protect_options(&self) -> Result { const IS_WINDOWS: bool = cfg!(windows); const IS_MACOS: bool = cfg!(target_os = "macos"); diff --git a/gix/src/object/tree/editor.rs b/gix/src/object/tree/editor.rs new file mode 100644 index 00000000000..fdcdcf87d40 --- /dev/null +++ b/gix/src/object/tree/editor.rs @@ -0,0 +1,271 @@ +use crate::bstr::{BStr, BString}; +use crate::prelude::ObjectIdExt; +use crate::{Id, Repository}; +use gix_hash::ObjectId; +use gix_object::tree::EntryKind; + +/// +pub mod init { + /// The error returned by [`Editor::new()](crate::object::tree::Editor::new()). + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error(transparent)] + DecodeTree(#[from] gix_object::decode::Error), + #[error(transparent)] + ValidationOptions(#[from] crate::config::boolean::Error), + } +} + +/// +pub mod write { + use crate::bstr::BString; + + /// The error returned by [`Editor::write()](crate::object::tree::Editor::write()) and [`Cursor::write()](super::Cursor::write). + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error(transparent)] + WriteTree(#[from] crate::object::write::Error), + #[error("The object {} ({}) at '{}' could not be found", id, kind.as_octal_str(), filename)] + MissingObject { + filename: BString, + kind: gix_object::tree::EntryKind, + id: gix_hash::ObjectId, + }, + #[error("The object {} ({}) has an invalid filename: '{}'", id, kind.as_octal_str(), filename)] + InvalidFilename { + filename: BString, + kind: gix_object::tree::EntryKind, + id: gix_hash::ObjectId, + source: gix_validate::path::component::Error, + }, + } +} + +/// A cursor at a specific portion of a tree to [edit](super::Editor). +pub struct Cursor<'a, 'repo> { + inner: gix_object::tree::editor::Cursor<'a, 'repo>, + validate: gix_validate::path::component::Options, + repo: &'repo Repository, +} + +/// Lifecycle +impl<'repo> super::Editor<'repo> { + /// Initialize a new editor from the given `tree`. + pub fn new(tree: &crate::Tree<'repo>) -> Result { + let tree_ref = tree.decode()?; + let repo = tree.repo; + let validate = repo.config.protect_options()?; + Ok(super::Editor { + inner: gix_object::tree::Editor::new(tree_ref.into(), &repo.objects, repo.object_hash()), + validate, + repo, + }) + } +} + +/// Tree editing +#[cfg(feature = "tree-editor")] +impl<'repo> crate::Tree<'repo> { + /// Start editing a new tree based on this one. + #[doc(alias = "treebuilder", alias = "git2")] + pub fn edit(&self) -> Result, init::Error> { + super::Editor::new(self) + } +} + +/// Obtain an iterator over `BStr`-components. +/// +/// Note that the implementation is simple, and it's mainly meant for statically known strings +/// or locations obtained during a merge. +pub trait ToComponents { + /// Return an iterator over the components of a path, without the separator. + fn to_components(&self) -> impl Iterator; +} + +impl ToComponents for &str { + fn to_components(&self) -> impl Iterator { + self.split('/').map(Into::into) + } +} + +impl ToComponents for String { + fn to_components(&self) -> impl Iterator { + self.split('/').map(Into::into) + } +} + +impl ToComponents for &String { + fn to_components(&self) -> impl Iterator { + self.split('/').map(Into::into) + } +} + +impl ToComponents for BString { + fn to_components(&self) -> impl Iterator { + self.split(|b| *b == b'/').map(Into::into) + } +} + +impl ToComponents for &BString { + fn to_components(&self) -> impl Iterator { + self.split(|b| *b == b'/').map(Into::into) + } +} + +impl ToComponents for &BStr { + fn to_components(&self) -> impl Iterator { + self.split(|b| *b == b'/').map(Into::into) + } +} + +/// Cursor Handling +impl<'repo> super::Editor<'repo> { + /// Turn ourselves as a cursor, which points to the same tree as the editor. + /// + /// This is useful if a method takes a [`Cursor`], not an [`Editor`](super::Editor). + pub fn to_cursor(&mut self) -> Cursor<'_, 'repo> { + Cursor { + inner: self.inner.to_cursor(), + validate: self.validate, + repo: self.repo, + } + } + + /// Create a cursor at the given `rela_path`, which must be a tree or is turned into a tree as its own edit. + /// + /// The returned cursor will then allow applying edits to the tree at `rela_path` as root. + /// If `rela_path` is a single empty string, it is equivalent to using the current instance itself. + pub fn cursor_at( + &mut self, + rela_path: impl ToComponents, + ) -> Result, gix_object::tree::editor::Error> { + Ok(Cursor { + inner: self.inner.cursor_at(rela_path.to_components())?, + validate: self.validate, + repo: self.repo, + }) + } +} +/// Operations +impl<'repo> Cursor<'_, 'repo> { + /// Like [`Editor::upsert()`](super::Editor::upsert()), but with the constraint of only editing in this cursor's tree. + pub fn upsert( + &mut self, + rela_path: impl ToComponents, + kind: EntryKind, + id: impl Into, + ) -> Result<&mut Self, gix_object::tree::editor::Error> { + self.inner.upsert(rela_path.to_components(), kind, id.into())?; + Ok(self) + } + + /// Like [`Editor::remove()`](super::Editor::remove), but with the constraint of only editing in this cursor's tree. + pub fn remove(&mut self, rela_path: impl ToComponents) -> Result<&mut Self, gix_object::tree::editor::Error> { + self.inner.remove(rela_path.to_components())?; + Ok(self) + } + + /// Like [`Editor::write()`](super::Editor::write()), but will write only the subtree of the cursor. + pub fn write(&mut self) -> Result, write::Error> { + write_cursor(self) + } +} + +/// Operations +impl<'repo> super::Editor<'repo> { + /// Set the root tree of the modification to `root`, assuring it has a well-known state. + /// + /// Note that this erases all previous edits. + /// + /// This is useful if the same editor is re-used for various trees. + pub fn set_root(&mut self, root: &crate::Tree<'repo>) -> Result<&mut Self, init::Error> { + let new_editor = super::Editor::new(root)?; + self.inner = new_editor.inner; + self.repo = new_editor.repo; + Ok(self) + } + /// Insert a new entry of `kind` with `id` at `rela_path`, an iterator over each path component in the tree, + /// like `a/b/c`. Names are matched case-sensitively. + /// + /// Existing leaf-entries will be overwritten unconditionally, and it is assumed that `id` is available in the object database + /// or will be made available at a later point to assure the integrity of the produced tree. + /// + /// Intermediate trees will be created if they don't exist in the object database, otherwise they will be loaded and entries + /// will be inserted into them instead. + /// + /// Note that `id` can be [null](ObjectId::null()) to create a placeholder. These will not be written, and paths leading + /// through them will not be considered a problem. + /// + /// `id` can also be an empty tree, along with [the respective `kind`](EntryKind::Tree), even though that's normally not allowed + /// in Git trees. + /// + /// Validation of path-components will not be performed here, but when [writing the tree](Self::write()). + pub fn upsert( + &mut self, + rela_path: impl ToComponents, + kind: EntryKind, + id: impl Into, + ) -> Result<&mut Self, gix_object::tree::editor::Error> { + self.inner.upsert(rela_path.to_components(), kind, id.into())?; + Ok(self) + } + + /// Remove the entry at `rela_path`, loading all trees on the path accordingly. + /// It's no error if the entry doesn't exist, or if `rela_path` doesn't lead to an existing entry at all. + pub fn remove(&mut self, rela_path: impl ToComponents) -> Result<&mut Self, gix_object::tree::editor::Error> { + self.inner.remove(rela_path.to_components())?; + Ok(self) + } + + /// Write the entire in-memory state of all changed trees (and only changed trees) to the object database. + /// Note that the returned object id *can* be the empty tree if everything was removed or if nothing + /// was added to the tree. + /// + /// The last call to `out` will be the changed root tree, whose object-id will also be returned. + /// `out` is free to do any kind of additional validation, like to assure that all entries in the tree exist. + /// We don't assure that as there is no validation that inserted entries are valid object ids. + /// + /// Future calls to [`upsert`](Self::upsert) or similar will keep working on the last seen state of the + /// just-written root-tree. + /// If this is not desired, use [set_root()](Self::set_root()). + /// + /// Before writing a tree, all of its entries (not only added ones), will be validated to assure they are + /// correct. The objects pointed to by entries also have to exist already. + pub fn write(&mut self) -> Result, write::Error> { + write_cursor(&mut self.to_cursor()) + } +} + +fn write_cursor<'repo>(cursor: &mut Cursor<'_, 'repo>) -> Result, write::Error> { + cursor + .inner + .write(|tree| -> Result { + for entry in &tree.entries { + gix_validate::path::component( + entry.filename.as_ref(), + entry + .mode + .is_link() + .then_some(gix_validate::path::component::Mode::Symlink), + cursor.validate, + ) + .map_err(|err| write::Error::InvalidFilename { + filename: entry.filename.clone(), + kind: entry.mode.into(), + id: entry.oid, + source: err, + })?; + if !cursor.repo.has_object(entry.oid) { + return Err(write::Error::MissingObject { + filename: entry.filename.clone(), + kind: entry.mode.into(), + id: entry.oid, + }); + } + } + Ok(cursor.repo.write_object(tree)?.detach()) + }) + .map(|id| id.attach(cursor.repo)) +} diff --git a/gix/src/object/tree/mod.rs b/gix/src/object/tree/mod.rs index 2f7df7ec33e..65906c42fda 100644 --- a/gix/src/object/tree/mod.rs +++ b/gix/src/object/tree/mod.rs @@ -4,6 +4,14 @@ use gix_object::{bstr::BStr, FindExt, TreeRefIter}; use crate::{object::find, Id, ObjectDetached, Tree}; +/// All state needed to conveniently edit a tree, using only [update-or-insert](Editor::upsert()) and [removals](Editor::remove()). +#[cfg(feature = "tree-editor")] +pub struct Editor<'repo> { + inner: gix_object::tree::Editor<'repo>, + validate: gix_validate::path::component::Options, + repo: &'repo crate::Repository, +} + /// Initialization impl<'repo> Tree<'repo> { /// Obtain a tree instance by handing in all components that it is made up of. @@ -163,6 +171,10 @@ impl<'repo> Tree<'repo> { } } +/// +#[cfg(feature = "tree-editor")] +pub mod editor; + /// #[cfg(feature = "blob-diff")] pub mod diff; diff --git a/gix/src/repository/mod.rs b/gix/src/repository/mod.rs index 77467f77f9a..41b2aaeca52 100644 --- a/gix/src/repository/mod.rs +++ b/gix/src/repository/mod.rs @@ -72,6 +72,20 @@ mod submodule; mod thread_safe; mod worktree; +/// +#[cfg(feature = "tree-editor")] +pub mod edit_tree { + /// The error returned by [Repository::edit_tree()](crate::Repository::edit_tree). + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error(transparent)] + FindTree(#[from] crate::object::find::existing::with_conversion::Error), + #[error(transparent)] + InitEditor(#[from] crate::object::tree::editor::init::Error), + } +} + /// #[cfg(feature = "revision")] pub mod merge_base { diff --git a/gix/src/repository/object.rs b/gix/src/repository/object.rs index 6230556b3c5..29d3bdbfcd1 100644 --- a/gix/src/repository/object.rs +++ b/gix/src/repository/object.rs @@ -12,7 +12,23 @@ use smallvec::SmallVec; use crate::{commit, ext::ObjectIdExt, object, tag, Blob, Commit, Id, Object, Reference, Tag, Tree}; -/// Methods related to object creation. +/// Tree editing +#[cfg(feature = "tree-editor")] +impl crate::Repository { + /// Return an editor for adjusting the tree at `id`. + /// + /// This can be the [empty tree id](ObjectId::empty_tree) to build a tree from scratch. + #[doc(alias = "treebuilder", alias = "git2")] + pub fn edit_tree( + &self, + id: impl Into, + ) -> Result, crate::repository::edit_tree::Error> { + let tree = self.find_tree(id)?; + Ok(tree.edit()?) + } +} + +/// Find objects of various kins impl crate::Repository { /// Find the object with `id` in the object database or return an error if it could not be found. /// @@ -138,7 +154,10 @@ impl crate::Repository { None => Ok(None), } } +} +/// Write objects of any type. +impl crate::Repository { pub(crate) fn shared_empty_buf(&self) -> std::cell::RefMut<'_, Vec> { let mut bufs = self.bufs.borrow_mut(); if bufs.last().is_none() { @@ -217,7 +236,10 @@ impl crate::Repository { .map_err(Into::into) .map(|oid| oid.attach(self)) } +} +/// Create commits and tags +impl crate::Repository { /// Create a tag reference named `name` (without `refs/tags/` prefix) pointing to a newly created tag object /// which in turn points to `target` and return the newly created reference. /// diff --git a/gix/src/types.rs b/gix/src/types.rs index fc726309b17..3e5edd1abd0 100644 --- a/gix/src/types.rs +++ b/gix/src/types.rs @@ -67,7 +67,7 @@ impl<'a> Drop for Blob<'a> { /// A decoded tree object with access to its owning repository. #[derive(Clone)] pub struct Tree<'repo> { - /// The id of the tree + /// Thek[ id of the tree pub id: ObjectId, /// The fully decoded tree data pub data: Vec, diff --git a/gix/tests/repository/object.rs b/gix/tests/repository/object.rs index 2aa2607cc69..341b1f9fea1 100644 --- a/gix/tests/repository/object.rs +++ b/gix/tests/repository/object.rs @@ -1,5 +1,210 @@ use gix_testtools::tempfile; +#[cfg(feature = "tree-editor")] +mod edit_tree { + use crate::util::hex_to_id; + use gix::bstr::{BStr, BString}; + use gix_object::tree::EntryKind; + + #[test] + // Some part of the test validation the implementation for this exists, but it's needless nonetheless. + #[allow(clippy::needless_borrows_for_generic_args)] + fn from_head_tree() -> crate::Result { + let (repo, _tmp) = crate::repo_rw("make_packed_and_loose.sh")?; + let head_tree_id = repo.head_tree_id()?; + assert_eq!( + display_tree(head_tree_id, &repo), + "24374df94315568adfaee119d038f710d1f45397 +├── that ce013625030ba8dba906f756967f9e9ca394464a.100644 +└── this 317e9677c3bcffd006f9fc84bbb0a54ef1676197.100644 +" + ); + let this_id = hex_to_id("317e9677c3bcffd006f9fc84bbb0a54ef1676197"); + let that_id = hex_to_id("ce013625030ba8dba906f756967f9e9ca394464a"); + let mut editor = repo.edit_tree(head_tree_id)?; + let actual = editor + .upsert("a/b", EntryKind::Blob, this_id)? + .upsert(String::from("this/subdir/that"), EntryKind::Blob, this_id)? + .upsert(BString::from("that/other/that"), EntryKind::Blob, that_id)? + .remove(BStr::new("that"))? + .remove(&String::from("that"))? + .remove(&BString::from("that"))? + .write()?; + + assert_eq!( + display_tree(actual, &repo), + "fe02a8bd15e4c0476d938f772f1eece6d164b1bd +├── a +│ └── b 317e9677c3bcffd006f9fc84bbb0a54ef1676197.100644 +└── this + └── subdir + └── that 317e9677c3bcffd006f9fc84bbb0a54ef1676197.100644 +", + "all trees are actually written, or else we couldn't visualize them." + ); + + let actual = editor + .upsert("a/b", EntryKind::Blob, that_id)? + .upsert(String::from("this/subdir/that"), EntryKind::Blob, this_id)? + .remove(BStr::new("does-not-exist"))? + .write()?; + assert_eq!( + display_tree(actual, &repo), + "219596ff52fc84b6b39bc327f202d408cc02e1db +├── a +│ └── b ce013625030ba8dba906f756967f9e9ca394464a.100644 +└── this + └── subdir + └── that 317e9677c3bcffd006f9fc84bbb0a54ef1676197.100644 +", + "existing blobs can also be changed" + ); + + let mut cursor = editor.cursor_at("something/very/nested/to/add/entries/to")?; + let actual = cursor + .upsert("a/b", EntryKind::Blob, this_id)? + .upsert(String::from("this/subdir/that"), EntryKind::Blob, that_id)? + .upsert(BString::from("that/other/that"), EntryKind::Blob, that_id)? + .remove(BStr::new("that"))? + .write()?; + + assert_eq!( + display_tree(actual, &repo), + "35ea623106198f21b6959dd2731740e5153db2bb +├── a +│ └── b 317e9677c3bcffd006f9fc84bbb0a54ef1676197.100644 +└── this + └── subdir + └── that ce013625030ba8dba906f756967f9e9ca394464a.100644 +", + "all remaining subtrees are written from the cursor position" + ); + + let actual = editor.write()?; + assert_eq!( + display_tree(actual, &repo), + "9ebdc2c1d22e91636fa876a51521464f8a88dd6f +├── a +│ └── b ce013625030ba8dba906f756967f9e9ca394464a.100644 +├── something +│ └── very +│ └── nested +│ └── to +│ └── add +│ └── entries +│ └── to +│ ├── a +│ │ └── b 317e9677c3bcffd006f9fc84bbb0a54ef1676197.100644 +│ └── this +│ └── subdir +│ └── that ce013625030ba8dba906f756967f9e9ca394464a.100644 +└── this + └── subdir + └── that 317e9677c3bcffd006f9fc84bbb0a54ef1676197.100644 +", + "it looks as it should when seen from the root tree" + ); + + editor.set_root(&head_tree_id.object()?.into_tree())?; + let actual = editor.write()?; + assert_eq!( + display_tree(actual, &repo), + "24374df94315568adfaee119d038f710d1f45397 +├── that ce013625030ba8dba906f756967f9e9ca394464a.100644 +└── this 317e9677c3bcffd006f9fc84bbb0a54ef1676197.100644 +", + "it's possible to set the editor to any tree after creating it, could help with memory re-use" + ); + Ok(()) + } + + #[test] + fn missing_objects_and_illformed_path_components_trigger_error() -> crate::Result { + let (repo, _tmp) = crate::repo_rw("make_packed_and_loose.sh")?; + let tree = repo.head_tree_id()?.object()?.into_tree(); + let mut editor = tree.edit()?; + let actual = editor + .upsert("non-existing", EntryKind::Blob, repo.object_hash().null())? + .write()?; + assert_eq!( + actual, + tree.id(), + "nulls are pruned before writing the tree, so it just rewrites the same tree" + ); + + let err = editor + .upsert( + "non-existing", + EntryKind::Blob, + hex_to_id("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + )? + .write() + .unwrap_err(); + assert_eq!( + err.to_string(), + "The object aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa (100644) at 'non-existing' could not be found", + "each entry to be written is checked for existence" + ); + + let this_id = hex_to_id("317e9677c3bcffd006f9fc84bbb0a54ef1676197"); + let err = editor + .remove("non-existing")? + .upsert(".git", EntryKind::Blob, this_id)? + .write() + .expect_err(".git is universally forbidden in trees"); + assert_eq!( + err.to_string(), + "The object 317e9677c3bcffd006f9fc84bbb0a54ef1676197 (100644) has an invalid filename: '.git'", + "each component is validated" + ); + + Ok(()) + } + + mod utils { + use gix::bstr::{BStr, ByteSlice}; + use gix::Repository; + use gix_hash::ObjectId; + + fn display_tree_recursive( + tree_id: ObjectId, + repo: &Repository, + name: Option<&BStr>, + ) -> anyhow::Result> { + let tree = repo.find_tree(tree_id)?.decode()?.to_owned(); + let mut termtree = termtree::Tree::new(if let Some(name) = name { + if tree.entries.is_empty() { + format!("{name} (empty)") + } else { + name.to_string() + } + } else { + tree_id.to_string() + }); + + for entry in &tree.entries { + if entry.mode.is_tree() { + termtree.push(display_tree_recursive(entry.oid, repo, Some(entry.filename.as_bstr()))?); + } else { + termtree.push(format!( + "{} {}.{}", + entry.filename, + entry.oid, + entry.mode.kind().as_octal_str() + )); + } + } + Ok(termtree) + } + + pub(super) fn display_tree(tree_id: impl Into, odb: &Repository) -> String { + display_tree_recursive(tree_id.into(), odb, None) + .expect("tree exists and everything was written") + .to_string() + } + } + use utils::display_tree; +} mod write_object { use crate::repository::object::empty_bare_repo; diff --git a/justfile b/justfile index 4a215d02f0a..758646d0f2c 100755 --- a/justfile +++ b/justfile @@ -146,8 +146,8 @@ check: # Run cargo doc on all crates doc $RUSTDOCFLAGS="-D warnings": - cargo doc --all --no-deps - cargo doc --features=max,lean,small --all --no-deps + cargo doc --all --no-deps --features need-more-recent-msrv + cargo doc --features=max,lean,small --all --no-deps --features need-more-recent-msrv # run all unit tests unit-tests: @@ -183,7 +183,7 @@ unit-tests: cargo nextest run -p gix-protocol --features blocking-client cargo nextest run -p gix-protocol --features async-client cargo nextest run -p gix --no-default-features - cargo nextest run -p gix --no-default-features --features basic,extras,comfort + cargo nextest run -p gix --no-default-features --features basic,extras,comfort,need-more-recent-msrv cargo nextest run -p gix --features async-network-client cargo nextest run -p gix --features blocking-network-client cargo nextest run -p gitoxide-core --lib