Skip to content

Commit e8a9531

Browse files
committed
feat: add tree-editing capabilities to Tree and Repository.
Create a tree editor using `Tree::edit()` or `Repository::edit_tree(id)`.
1 parent ffa782b commit e8a9531

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 {

0 commit comments

Comments
 (0)