Skip to content

Commit b279957

Browse files
committedSep 5, 2024
feat: add tree-editing capabilities to Tree and Repository.
Create a tree editor using `Tree::edit()` or `Repository::edit_tree(id)`.
1 parent 7c48556 commit b279957

File tree

10 files changed

+546
-7
lines changed

10 files changed

+546
-7
lines changed
 

‎Cargo.lock

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎gix/Cargo.toml

+14-1
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,12 @@ extras = [
6464
"credentials",
6565
"interrupt",
6666
"status",
67-
"dirwalk",
67+
"dirwalk"
6868
]
6969

70+
## A collection of features that need a larger MSRV, and thus are disabled by default.
71+
need-more-recent-msrv = ["tree-editor"]
72+
7073
## Various progress-related features that improve the look of progress message units.
7174
comfort = [
7275
"gix-features/progress-unit-bytes",
@@ -103,6 +106,12 @@ worktree-mutation = ["attributes", "dep:gix-worktree-state"]
103106
## Retrieve a worktree stack for querying exclude information
104107
excludes = ["dep:gix-ignore", "dep:gix-worktree", "index"]
105108

109+
## Provide facilities to edit trees conveniently.
110+
##
111+
## Not that currently, this requires [Rust 1.75](https://caniuse.rs/features/return_position_impl_trait_in_trait).
112+
## This feature toggle is likely going away then.
113+
tree-editor = []
114+
106115
## Query attributes and excludes. Enables access to pathspecs, worktree checkouts, filter-pipelines and submodules.
107116
attributes = [
108117
"excludes",
@@ -384,19 +393,23 @@ parking_lot = { version = "0.12.1", optional = true }
384393
document-features = { version = "0.2.0", optional = true }
385394

386395
[dev-dependencies]
396+
# For additional features that aren't enabled by default due to MSRV
397+
gix = { path = ".", default-features = false, features = ["tree-editor"] }
387398
pretty_assertions = "1.4.0"
388399
gix-testtools = { path = "../tests/tools" }
389400
is_ci = "1.1.1"
390401
anyhow = "1"
391402
walkdir = "2.3.2"
392403
serial_test = { version = "3.1.0", default-features = false }
393404
async-std = { version = "1.12.0", features = ["attributes"] }
405+
termtree = "0.5.1"
394406

395407
[package.metadata.docs.rs]
396408
features = [
397409
"document-features",
398410
"max-performance",
399411
"blocking-network-client",
400412
"blocking-http-transport-curl",
413+
"need-more-recent-msrv",
401414
"serde",
402415
]

‎gix/src/config/cache/access.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ impl Cache {
251251
})
252252
}
253253

254-
#[cfg(feature = "index")]
254+
#[cfg(any(feature = "index", feature = "tree-editor"))]
255255
pub(crate) fn protect_options(&self) -> Result<gix_validate::path::component::Options, config::boolean::Error> {
256256
const IS_WINDOWS: bool = cfg!(windows);
257257
const IS_MACOS: bool = cfg!(target_os = "macos");

‎gix/src/object/tree/editor.rs

+271
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
use crate::bstr::{BStr, BString};
2+
use crate::prelude::ObjectIdExt;
3+
use crate::{Id, Repository};
4+
use gix_hash::ObjectId;
5+
use gix_object::tree::EntryKind;
6+
7+
///
8+
pub mod init {
9+
/// The error returned by [`Editor::new()](crate::object::tree::Editor::new()).
10+
#[derive(Debug, thiserror::Error)]
11+
#[allow(missing_docs)]
12+
pub enum Error {
13+
#[error(transparent)]
14+
DecodeTree(#[from] gix_object::decode::Error),
15+
#[error(transparent)]
16+
ValidationOptions(#[from] crate::config::boolean::Error),
17+
}
18+
}
19+
20+
///
21+
pub mod write {
22+
use crate::bstr::BString;
23+
24+
/// The error returned by [`Editor::write()](crate::object::tree::Editor::write()) and [`Cursor::write()](super::Cursor::write).
25+
#[derive(Debug, thiserror::Error)]
26+
#[allow(missing_docs)]
27+
pub enum Error {
28+
#[error(transparent)]
29+
WriteTree(#[from] crate::object::write::Error),
30+
#[error("The object {} ({}) at '{}' could not be found", id, kind.as_octal_str(), filename)]
31+
MissingObject {
32+
filename: BString,
33+
kind: gix_object::tree::EntryKind,
34+
id: gix_hash::ObjectId,
35+
},
36+
#[error("The object {} ({}) has an invalid filename: '{}'", id, kind.as_octal_str(), filename)]
37+
InvalidFilename {
38+
filename: BString,
39+
kind: gix_object::tree::EntryKind,
40+
id: gix_hash::ObjectId,
41+
source: gix_validate::path::component::Error,
42+
},
43+
}
44+
}
45+
46+
/// A cursor at a specific portion of a tree to [edit](super::Editor).
47+
pub struct Cursor<'a, 'repo> {
48+
inner: gix_object::tree::editor::Cursor<'a, 'repo>,
49+
validate: gix_validate::path::component::Options,
50+
repo: &'repo Repository,
51+
}
52+
53+
/// Lifecycle
54+
impl<'repo> super::Editor<'repo> {
55+
/// Initialize a new editor from the given `tree`.
56+
pub fn new(tree: &crate::Tree<'repo>) -> Result<Self, init::Error> {
57+
let tree_ref = tree.decode()?;
58+
let repo = tree.repo;
59+
let validate = repo.config.protect_options()?;
60+
Ok(super::Editor {
61+
inner: gix_object::tree::Editor::new(tree_ref.into(), &repo.objects, repo.object_hash()),
62+
validate,
63+
repo,
64+
})
65+
}
66+
}
67+
68+
/// Tree editing
69+
#[cfg(feature = "tree-editor")]
70+
impl<'repo> crate::Tree<'repo> {
71+
/// Start editing a new tree based on this one.
72+
#[doc(alias = "treebuilder", alias = "git2")]
73+
pub fn edit(&self) -> Result<super::Editor<'repo>, init::Error> {
74+
super::Editor::new(self)
75+
}
76+
}
77+
78+
/// Obtain an iterator over `BStr`-components.
79+
///
80+
/// Note that the implementation is simple, and it's mainly meant for statically known strings
81+
/// or locations obtained during a merge.
82+
pub trait ToComponents {
83+
/// Return an iterator over the components of a path, without the separator.
84+
fn to_components(&self) -> impl Iterator<Item = &BStr>;
85+
}
86+
87+
impl ToComponents for &str {
88+
fn to_components(&self) -> impl Iterator<Item = &BStr> {
89+
self.split('/').map(Into::into)
90+
}
91+
}
92+
93+
impl ToComponents for String {
94+
fn to_components(&self) -> impl Iterator<Item = &BStr> {
95+
self.split('/').map(Into::into)
96+
}
97+
}
98+
99+
impl ToComponents for &String {
100+
fn to_components(&self) -> impl Iterator<Item = &BStr> {
101+
self.split('/').map(Into::into)
102+
}
103+
}
104+
105+
impl ToComponents for BString {
106+
fn to_components(&self) -> impl Iterator<Item = &BStr> {
107+
self.split(|b| *b == b'/').map(Into::into)
108+
}
109+
}
110+
111+
impl ToComponents for &BString {
112+
fn to_components(&self) -> impl Iterator<Item = &BStr> {
113+
self.split(|b| *b == b'/').map(Into::into)
114+
}
115+
}
116+
117+
impl ToComponents for &BStr {
118+
fn to_components(&self) -> impl Iterator<Item = &BStr> {
119+
self.split(|b| *b == b'/').map(Into::into)
120+
}
121+
}
122+
123+
/// Cursor Handling
124+
impl<'repo> super::Editor<'repo> {
125+
/// Turn ourselves as a cursor, which points to the same tree as the editor.
126+
///
127+
/// This is useful if a method takes a [`Cursor`], not an [`Editor`](super::Editor).
128+
pub fn to_cursor(&mut self) -> Cursor<'_, 'repo> {
129+
Cursor {
130+
inner: self.inner.to_cursor(),
131+
validate: self.validate,
132+
repo: self.repo,
133+
}
134+
}
135+
136+
/// Create a cursor at the given `rela_path`, which must be a tree or is turned into a tree as its own edit.
137+
///
138+
/// The returned cursor will then allow applying edits to the tree at `rela_path` as root.
139+
/// If `rela_path` is a single empty string, it is equivalent to using the current instance itself.
140+
pub fn cursor_at(
141+
&mut self,
142+
rela_path: impl ToComponents,
143+
) -> Result<Cursor<'_, 'repo>, gix_object::tree::editor::Error> {
144+
Ok(Cursor {
145+
inner: self.inner.cursor_at(rela_path.to_components())?,
146+
validate: self.validate,
147+
repo: self.repo,
148+
})
149+
}
150+
}
151+
/// Operations
152+
impl<'repo> Cursor<'_, 'repo> {
153+
/// Like [`Editor::upsert()`](super::Editor::upsert()), but with the constraint of only editing in this cursor's tree.
154+
pub fn upsert(
155+
&mut self,
156+
rela_path: impl ToComponents,
157+
kind: EntryKind,
158+
id: impl Into<ObjectId>,
159+
) -> Result<&mut Self, gix_object::tree::editor::Error> {
160+
self.inner.upsert(rela_path.to_components(), kind, id.into())?;
161+
Ok(self)
162+
}
163+
164+
/// Like [`Editor::remove()`](super::Editor::remove), but with the constraint of only editing in this cursor's tree.
165+
pub fn remove(&mut self, rela_path: impl ToComponents) -> Result<&mut Self, gix_object::tree::editor::Error> {
166+
self.inner.remove(rela_path.to_components())?;
167+
Ok(self)
168+
}
169+
170+
/// Like [`Editor::write()`](super::Editor::write()), but will write only the subtree of the cursor.
171+
pub fn write(&mut self) -> Result<Id<'repo>, write::Error> {
172+
write_cursor(self)
173+
}
174+
}
175+
176+
/// Operations
177+
impl<'repo> super::Editor<'repo> {
178+
/// Set the root tree of the modification to `root`, assuring it has a well-known state.
179+
///
180+
/// Note that this erases all previous edits.
181+
///
182+
/// This is useful if the same editor is re-used for various trees.
183+
pub fn set_root(&mut self, root: &crate::Tree<'repo>) -> Result<&mut Self, init::Error> {
184+
let new_editor = super::Editor::new(root)?;
185+
self.inner = new_editor.inner;
186+
self.repo = new_editor.repo;
187+
Ok(self)
188+
}
189+
/// Insert a new entry of `kind` with `id` at `rela_path`, an iterator over each path component in the tree,
190+
/// like `a/b/c`. Names are matched case-sensitively.
191+
///
192+
/// Existing leaf-entries will be overwritten unconditionally, and it is assumed that `id` is available in the object database
193+
/// or will be made available at a later point to assure the integrity of the produced tree.
194+
///
195+
/// Intermediate trees will be created if they don't exist in the object database, otherwise they will be loaded and entries
196+
/// will be inserted into them instead.
197+
///
198+
/// Note that `id` can be [null](ObjectId::null()) to create a placeholder. These will not be written, and paths leading
199+
/// through them will not be considered a problem.
200+
///
201+
/// `id` can also be an empty tree, along with [the respective `kind`](EntryKind::Tree), even though that's normally not allowed
202+
/// in Git trees.
203+
///
204+
/// Validation of path-components will not be performed here, but when [writing the tree](Self::write()).
205+
pub fn upsert(
206+
&mut self,
207+
rela_path: impl ToComponents,
208+
kind: EntryKind,
209+
id: impl Into<ObjectId>,
210+
) -> Result<&mut Self, gix_object::tree::editor::Error> {
211+
self.inner.upsert(rela_path.to_components(), kind, id.into())?;
212+
Ok(self)
213+
}
214+
215+
/// Remove the entry at `rela_path`, loading all trees on the path accordingly.
216+
/// It's no error if the entry doesn't exist, or if `rela_path` doesn't lead to an existing entry at all.
217+
pub fn remove(&mut self, rela_path: impl ToComponents) -> Result<&mut Self, gix_object::tree::editor::Error> {
218+
self.inner.remove(rela_path.to_components())?;
219+
Ok(self)
220+
}
221+
222+
/// Write the entire in-memory state of all changed trees (and only changed trees) to the object database.
223+
/// Note that the returned object id *can* be the empty tree if everything was removed or if nothing
224+
/// was added to the tree.
225+
///
226+
/// The last call to `out` will be the changed root tree, whose object-id will also be returned.
227+
/// `out` is free to do any kind of additional validation, like to assure that all entries in the tree exist.
228+
/// We don't assure that as there is no validation that inserted entries are valid object ids.
229+
///
230+
/// Future calls to [`upsert`](Self::upsert) or similar will keep working on the last seen state of the
231+
/// just-written root-tree.
232+
/// If this is not desired, use [set_root()](Self::set_root()).
233+
///
234+
/// Before writing a tree, all of its entries (not only added ones), will be validated to assure they are
235+
/// correct. The objects pointed to by entries also have to exist already.
236+
pub fn write(&mut self) -> Result<Id<'repo>, write::Error> {
237+
write_cursor(&mut self.to_cursor())
238+
}
239+
}
240+
241+
fn write_cursor<'repo>(cursor: &mut Cursor<'_, 'repo>) -> Result<Id<'repo>, write::Error> {
242+
cursor
243+
.inner
244+
.write(|tree| -> Result<ObjectId, write::Error> {
245+
for entry in &tree.entries {
246+
gix_validate::path::component(
247+
entry.filename.as_ref(),
248+
entry
249+
.mode
250+
.is_link()
251+
.then_some(gix_validate::path::component::Mode::Symlink),
252+
cursor.validate,
253+
)
254+
.map_err(|err| write::Error::InvalidFilename {
255+
filename: entry.filename.clone(),
256+
kind: entry.mode.into(),
257+
id: entry.oid,
258+
source: err,
259+
})?;
260+
if !cursor.repo.has_object(entry.oid) {
261+
return Err(write::Error::MissingObject {
262+
filename: entry.filename.clone(),
263+
kind: entry.mode.into(),
264+
id: entry.oid,
265+
});
266+
}
267+
}
268+
Ok(cursor.repo.write_object(tree)?.detach())
269+
})
270+
.map(|id| id.attach(cursor.repo))
271+
}

‎gix/src/object/tree/mod.rs

+12
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ use gix_object::{bstr::BStr, FindExt, TreeRefIter};
44

55
use crate::{object::find, Id, ObjectDetached, Tree};
66

7+
/// All state needed to conveniently edit a tree, using only [update-or-insert](Editor::upsert()) and [removals](Editor::remove()).
8+
#[cfg(feature = "tree-editor")]
9+
pub struct Editor<'repo> {
10+
inner: gix_object::tree::Editor<'repo>,
11+
validate: gix_validate::path::component::Options,
12+
repo: &'repo crate::Repository,
13+
}
14+
715
/// Initialization
816
impl<'repo> Tree<'repo> {
917
/// Obtain a tree instance by handing in all components that it is made up of.
@@ -163,6 +171,10 @@ impl<'repo> Tree<'repo> {
163171
}
164172
}
165173

174+
///
175+
#[cfg(feature = "tree-editor")]
176+
pub mod editor;
177+
166178
///
167179
#[cfg(feature = "blob-diff")]
168180
pub mod diff;

‎gix/src/repository/mod.rs

+14
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,20 @@ mod submodule;
7272
mod thread_safe;
7373
mod worktree;
7474

75+
///
76+
#[cfg(feature = "tree-editor")]
77+
pub mod edit_tree {
78+
/// The error returned by [Repository::edit_tree()](crate::Repository::edit_tree).
79+
#[derive(Debug, thiserror::Error)]
80+
#[allow(missing_docs)]
81+
pub enum Error {
82+
#[error(transparent)]
83+
FindTree(#[from] crate::object::find::existing::with_conversion::Error),
84+
#[error(transparent)]
85+
InitEditor(#[from] crate::object::tree::editor::init::Error),
86+
}
87+
}
88+
7589
///
7690
#[cfg(feature = "revision")]
7791
pub mod merge_base {

‎gix/src/repository/object.rs

+23-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,23 @@ use smallvec::SmallVec;
1212

1313
use crate::{commit, ext::ObjectIdExt, object, tag, Blob, Commit, Id, Object, Reference, Tag, Tree};
1414

15-
/// Methods related to object creation.
15+
/// Tree editing
16+
#[cfg(feature = "tree-editor")]
17+
impl crate::Repository {
18+
/// Return an editor for adjusting the tree at `id`.
19+
///
20+
/// This can be the [empty tree id](ObjectId::empty_tree) to build a tree from scratch.
21+
#[doc(alias = "treebuilder", alias = "git2")]
22+
pub fn edit_tree(
23+
&self,
24+
id: impl Into<ObjectId>,
25+
) -> Result<object::tree::Editor<'_>, crate::repository::edit_tree::Error> {
26+
let tree = self.find_tree(id)?;
27+
Ok(tree.edit()?)
28+
}
29+
}
30+
31+
/// Find objects of various kins
1632
impl crate::Repository {
1733
/// Find the object with `id` in the object database or return an error if it could not be found.
1834
///
@@ -138,7 +154,10 @@ impl crate::Repository {
138154
None => Ok(None),
139155
}
140156
}
157+
}
141158

159+
/// Write objects of any type.
160+
impl crate::Repository {
142161
pub(crate) fn shared_empty_buf(&self) -> std::cell::RefMut<'_, Vec<u8>> {
143162
let mut bufs = self.bufs.borrow_mut();
144163
if bufs.last().is_none() {
@@ -217,7 +236,10 @@ impl crate::Repository {
217236
.map_err(Into::into)
218237
.map(|oid| oid.attach(self))
219238
}
239+
}
220240

241+
/// Create commits and tags
242+
impl crate::Repository {
221243
/// Create a tag reference named `name` (without `refs/tags/` prefix) pointing to a newly created tag object
222244
/// which in turn points to `target` and return the newly created reference.
223245
///

‎gix/src/types.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ impl<'a> Drop for Blob<'a> {
6767
/// A decoded tree object with access to its owning repository.
6868
#[derive(Clone)]
6969
pub struct Tree<'repo> {
70-
/// The id of the tree
70+
/// Thek[ id of the tree
7171
pub id: ObjectId,
7272
/// The fully decoded tree data
7373
pub data: Vec<u8>,

‎gix/tests/repository/object.rs

+205
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,210 @@
11
use gix_testtools::tempfile;
22

3+
#[cfg(feature = "tree-editor")]
4+
mod edit_tree {
5+
use crate::util::hex_to_id;
6+
use gix::bstr::{BStr, BString};
7+
use gix_object::tree::EntryKind;
8+
9+
#[test]
10+
// Some part of the test validation the implementation for this exists, but it's needless nonetheless.
11+
#[allow(clippy::needless_borrows_for_generic_args)]
12+
fn from_head_tree() -> crate::Result {
13+
let (repo, _tmp) = crate::repo_rw("make_packed_and_loose.sh")?;
14+
let head_tree_id = repo.head_tree_id()?;
15+
assert_eq!(
16+
display_tree(head_tree_id, &repo),
17+
"24374df94315568adfaee119d038f710d1f45397
18+
├── that ce013625030ba8dba906f756967f9e9ca394464a.100644
19+
└── this 317e9677c3bcffd006f9fc84bbb0a54ef1676197.100644
20+
"
21+
);
22+
let this_id = hex_to_id("317e9677c3bcffd006f9fc84bbb0a54ef1676197");
23+
let that_id = hex_to_id("ce013625030ba8dba906f756967f9e9ca394464a");
24+
let mut editor = repo.edit_tree(head_tree_id)?;
25+
let actual = editor
26+
.upsert("a/b", EntryKind::Blob, this_id)?
27+
.upsert(String::from("this/subdir/that"), EntryKind::Blob, this_id)?
28+
.upsert(BString::from("that/other/that"), EntryKind::Blob, that_id)?
29+
.remove(BStr::new("that"))?
30+
.remove(&String::from("that"))?
31+
.remove(&BString::from("that"))?
32+
.write()?;
33+
34+
assert_eq!(
35+
display_tree(actual, &repo),
36+
"fe02a8bd15e4c0476d938f772f1eece6d164b1bd
37+
├── a
38+
│ └── b 317e9677c3bcffd006f9fc84bbb0a54ef1676197.100644
39+
└── this
40+
└── subdir
41+
└── that 317e9677c3bcffd006f9fc84bbb0a54ef1676197.100644
42+
",
43+
"all trees are actually written, or else we couldn't visualize them."
44+
);
45+
46+
let actual = editor
47+
.upsert("a/b", EntryKind::Blob, that_id)?
48+
.upsert(String::from("this/subdir/that"), EntryKind::Blob, this_id)?
49+
.remove(BStr::new("does-not-exist"))?
50+
.write()?;
51+
assert_eq!(
52+
display_tree(actual, &repo),
53+
"219596ff52fc84b6b39bc327f202d408cc02e1db
54+
├── a
55+
│ └── b ce013625030ba8dba906f756967f9e9ca394464a.100644
56+
└── this
57+
└── subdir
58+
└── that 317e9677c3bcffd006f9fc84bbb0a54ef1676197.100644
59+
",
60+
"existing blobs can also be changed"
61+
);
62+
63+
let mut cursor = editor.cursor_at("something/very/nested/to/add/entries/to")?;
64+
let actual = cursor
65+
.upsert("a/b", EntryKind::Blob, this_id)?
66+
.upsert(String::from("this/subdir/that"), EntryKind::Blob, that_id)?
67+
.upsert(BString::from("that/other/that"), EntryKind::Blob, that_id)?
68+
.remove(BStr::new("that"))?
69+
.write()?;
70+
71+
assert_eq!(
72+
display_tree(actual, &repo),
73+
"35ea623106198f21b6959dd2731740e5153db2bb
74+
├── a
75+
│ └── b 317e9677c3bcffd006f9fc84bbb0a54ef1676197.100644
76+
└── this
77+
└── subdir
78+
└── that ce013625030ba8dba906f756967f9e9ca394464a.100644
79+
",
80+
"all remaining subtrees are written from the cursor position"
81+
);
82+
83+
let actual = editor.write()?;
84+
assert_eq!(
85+
display_tree(actual, &repo),
86+
"9ebdc2c1d22e91636fa876a51521464f8a88dd6f
87+
├── a
88+
│ └── b ce013625030ba8dba906f756967f9e9ca394464a.100644
89+
├── something
90+
│ └── very
91+
│ └── nested
92+
│ └── to
93+
│ └── add
94+
│ └── entries
95+
│ └── to
96+
│ ├── a
97+
│ │ └── b 317e9677c3bcffd006f9fc84bbb0a54ef1676197.100644
98+
│ └── this
99+
│ └── subdir
100+
│ └── that ce013625030ba8dba906f756967f9e9ca394464a.100644
101+
└── this
102+
└── subdir
103+
└── that 317e9677c3bcffd006f9fc84bbb0a54ef1676197.100644
104+
",
105+
"it looks as it should when seen from the root tree"
106+
);
107+
108+
editor.set_root(&head_tree_id.object()?.into_tree())?;
109+
let actual = editor.write()?;
110+
assert_eq!(
111+
display_tree(actual, &repo),
112+
"24374df94315568adfaee119d038f710d1f45397
113+
├── that ce013625030ba8dba906f756967f9e9ca394464a.100644
114+
└── this 317e9677c3bcffd006f9fc84bbb0a54ef1676197.100644
115+
",
116+
"it's possible to set the editor to any tree after creating it, could help with memory re-use"
117+
);
118+
Ok(())
119+
}
120+
121+
#[test]
122+
fn missing_objects_and_illformed_path_components_trigger_error() -> crate::Result {
123+
let (repo, _tmp) = crate::repo_rw("make_packed_and_loose.sh")?;
124+
let tree = repo.head_tree_id()?.object()?.into_tree();
125+
let mut editor = tree.edit()?;
126+
let actual = editor
127+
.upsert("non-existing", EntryKind::Blob, repo.object_hash().null())?
128+
.write()?;
129+
assert_eq!(
130+
actual,
131+
tree.id(),
132+
"nulls are pruned before writing the tree, so it just rewrites the same tree"
133+
);
134+
135+
let err = editor
136+
.upsert(
137+
"non-existing",
138+
EntryKind::Blob,
139+
hex_to_id("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
140+
)?
141+
.write()
142+
.unwrap_err();
143+
assert_eq!(
144+
err.to_string(),
145+
"The object aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa (100644) at 'non-existing' could not be found",
146+
"each entry to be written is checked for existence"
147+
);
148+
149+
let this_id = hex_to_id("317e9677c3bcffd006f9fc84bbb0a54ef1676197");
150+
let err = editor
151+
.remove("non-existing")?
152+
.upsert(".git", EntryKind::Blob, this_id)?
153+
.write()
154+
.expect_err(".git is universally forbidden in trees");
155+
assert_eq!(
156+
err.to_string(),
157+
"The object 317e9677c3bcffd006f9fc84bbb0a54ef1676197 (100644) has an invalid filename: '.git'",
158+
"each component is validated"
159+
);
160+
161+
Ok(())
162+
}
163+
164+
mod utils {
165+
use gix::bstr::{BStr, ByteSlice};
166+
use gix::Repository;
167+
use gix_hash::ObjectId;
168+
169+
fn display_tree_recursive(
170+
tree_id: ObjectId,
171+
repo: &Repository,
172+
name: Option<&BStr>,
173+
) -> anyhow::Result<termtree::Tree<String>> {
174+
let tree = repo.find_tree(tree_id)?.decode()?.to_owned();
175+
let mut termtree = termtree::Tree::new(if let Some(name) = name {
176+
if tree.entries.is_empty() {
177+
format!("{name} (empty)")
178+
} else {
179+
name.to_string()
180+
}
181+
} else {
182+
tree_id.to_string()
183+
});
184+
185+
for entry in &tree.entries {
186+
if entry.mode.is_tree() {
187+
termtree.push(display_tree_recursive(entry.oid, repo, Some(entry.filename.as_bstr()))?);
188+
} else {
189+
termtree.push(format!(
190+
"{} {}.{}",
191+
entry.filename,
192+
entry.oid,
193+
entry.mode.kind().as_octal_str()
194+
));
195+
}
196+
}
197+
Ok(termtree)
198+
}
199+
200+
pub(super) fn display_tree(tree_id: impl Into<ObjectId>, odb: &Repository) -> String {
201+
display_tree_recursive(tree_id.into(), odb, None)
202+
.expect("tree exists and everything was written")
203+
.to_string()
204+
}
205+
}
206+
use utils::display_tree;
207+
}
3208
mod write_object {
4209
use crate::repository::object::empty_bare_repo;
5210

‎justfile

+3-3
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,8 @@ check:
146146

147147
# Run cargo doc on all crates
148148
doc $RUSTDOCFLAGS="-D warnings":
149-
cargo doc --all --no-deps
150-
cargo doc --features=max,lean,small --all --no-deps
149+
cargo doc --all --no-deps --features need-more-recent-msrv
150+
cargo doc --features=max,lean,small --all --no-deps --features need-more-recent-msrv
151151

152152
# run all unit tests
153153
unit-tests:
@@ -183,7 +183,7 @@ unit-tests:
183183
cargo nextest run -p gix-protocol --features blocking-client
184184
cargo nextest run -p gix-protocol --features async-client
185185
cargo nextest run -p gix --no-default-features
186-
cargo nextest run -p gix --no-default-features --features basic,extras,comfort
186+
cargo nextest run -p gix --no-default-features --features basic,extras,comfort,need-more-recent-msrv
187187
cargo nextest run -p gix --features async-network-client
188188
cargo nextest run -p gix --features blocking-network-client
189189
cargo nextest run -p gitoxide-core --lib

0 commit comments

Comments
 (0)
Please sign in to comment.