Skip to content

Commit 7d2b856

Browse files
committed
feat: add tree-editing capabilities to Tree and Repository.
TBD
1 parent fbfc978 commit 7d2b856

File tree

8 files changed

+370
-2
lines changed

8 files changed

+370
-2
lines changed

Cargo.lock

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

gix/Cargo.toml

+8
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ extras = [
6565
"interrupt",
6666
"status",
6767
"dirwalk",
68+
"tree-editor"
6869
]
6970

7071
## Various progress-related features that improve the look of progress message units.
@@ -103,6 +104,12 @@ worktree-mutation = ["attributes", "dep:gix-worktree-state"]
103104
## Retrieve a worktree stack for querying exclude information
104105
excludes = ["dep:gix-ignore", "dep:gix-worktree", "index"]
105106

107+
## Provide facilities to edit trees conveniently.
108+
##
109+
## Not that currently, this requires [Rust 1.75](https://caniuse.rs/features/return_position_impl_trait_in_trait).
110+
## This feature toggle is likely going away then.
111+
tree-editor = []
112+
106113
## Query attributes and excludes. Enables access to pathspecs, worktree checkouts, filter-pipelines and submodules.
107114
attributes = [
108115
"excludes",
@@ -391,6 +398,7 @@ anyhow = "1"
391398
walkdir = "2.3.2"
392399
serial_test = { version = "3.1.0", default-features = false }
393400
async-std = { version = "1.12.0", features = ["attributes"] }
401+
termtree = "0.5.1"
394402

395403
[package.metadata.docs.rs]
396404
features = [

gix/src/object/tree/editor.rs

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

gix/src/object/tree/mod.rs

+11
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ 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+
repo: &'repo crate::Repository,
12+
}
13+
714
/// Initialization
815
impl<'repo> Tree<'repo> {
916
/// Obtain a tree instance by handing in all components that it is made up of.
@@ -163,6 +170,10 @@ impl<'repo> Tree<'repo> {
163170
}
164171
}
165172

173+
///
174+
#[cfg(feature = "tree-editor")]
175+
pub mod editor;
176+
166177
///
167178
#[cfg(feature = "blob-diff")]
168179
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>,

0 commit comments

Comments
 (0)