Skip to content

Commit cf15fd0

Browse files
committed
feat: add tree-editing capabilities to Tree and Repository.
TBD
1 parent b236373 commit cf15fd0

File tree

9 files changed

+462
-6
lines changed

9 files changed

+462
-6
lines changed

Diff for: Cargo.lock

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

Diff for: 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
]

Diff for: gix/src/object/tree/editor.rs

+228
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
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 ToComponents {
63+
/// Return an iterator over the components of a path, without the separator.
64+
fn to_components(&self) -> impl Iterator<Item = &BStr>;
65+
}
66+
67+
impl ToComponents for &str {
68+
fn to_components(&self) -> impl Iterator<Item = &BStr> {
69+
self.split('/').map(Into::into)
70+
}
71+
}
72+
73+
impl ToComponents for String {
74+
fn to_components(&self) -> impl Iterator<Item = &BStr> {
75+
self.split('/').map(Into::into)
76+
}
77+
}
78+
79+
impl ToComponents for &String {
80+
fn to_components(&self) -> impl Iterator<Item = &BStr> {
81+
self.split('/').map(Into::into)
82+
}
83+
}
84+
85+
impl ToComponents for BString {
86+
fn to_components(&self) -> impl Iterator<Item = &BStr> {
87+
self.split(|b| *b == b'/').map(Into::into)
88+
}
89+
}
90+
91+
impl ToComponents for &BString {
92+
fn to_components(&self) -> impl Iterator<Item = &BStr> {
93+
self.split(|b| *b == b'/').map(Into::into)
94+
}
95+
}
96+
97+
impl ToComponents for &BStr {
98+
fn to_components(&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 ToComponents,
122+
) -> Result<Cursor<'_, 'repo>, gix_object::find::existing_object::Error> {
123+
Ok(Cursor {
124+
inner: self.inner.cursor_at(rela_path.to_components())?,
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 ToComponents,
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_components(), 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 ToComponents,
146+
) -> Result<&mut Self, gix_object::find::existing_object::Error> {
147+
self.inner.remove(rela_path.to_components())?;
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+
/// Set the root tree of the modification to `root`, assuring it has a well-known state.
160+
///
161+
/// Note that this erases all previous edits.
162+
///
163+
/// This is useful if the same editor is re-used for various trees.
164+
pub fn set_root(&mut self, root: &crate::Tree<'repo>) -> Result<&mut Self, init::Error> {
165+
*self = super::Editor::new(root)?;
166+
Ok(self)
167+
}
168+
/// Insert a new entry of `kind` with `id` at `rela_path`, an iterator over each path component in the tree,
169+
/// like `a/b/c`. Names are matched case-sensitively.
170+
///
171+
/// Existing leaf-entries will be overwritten unconditionally, and it is assumed that `id` is available in the object database
172+
/// or will be made available at a later point to assure the integrity of the produced tree.
173+
///
174+
/// Intermediate trees will be created if they don't exist in the object database, otherwise they will be loaded and entries
175+
/// will be inserted into them instead.
176+
///
177+
/// Note that `id` can be [null](ObjectId::null()) to create a placeholder. These will not be written, and paths leading
178+
/// through them will not be considered a problem.
179+
///
180+
/// `id` can also be an empty tree, along with [the respective `kind`](EntryKind::Tree), even though that's normally not allowed
181+
/// in Git trees.
182+
///
183+
/// Validation of path-components will not be performed here, but when [writing the tree](Self::write()).
184+
pub fn upsert(
185+
&mut self,
186+
rela_path: impl ToComponents,
187+
kind: EntryKind,
188+
id: impl Into<ObjectId>,
189+
) -> Result<&mut Self, gix_object::find::existing_object::Error> {
190+
self.inner.upsert(rela_path.to_components(), kind, id.into())?;
191+
Ok(self)
192+
}
193+
194+
/// Remove the entry at `rela_path`, loading all trees on the path accordingly.
195+
/// It's no error if the entry doesn't exist, or if `rela_path` doesn't lead to an existing entry at all.
196+
pub fn remove(
197+
&mut self,
198+
rela_path: impl ToComponents,
199+
) -> Result<&mut Self, gix_object::find::existing_object::Error> {
200+
self.inner.remove(rela_path.to_components())?;
201+
Ok(self)
202+
}
203+
204+
/// Write the entire in-memory state of all changed trees (and only changed trees) to the object database.
205+
/// Note that the returned object id *can* be the empty tree if everything was removed or if nothing
206+
/// was added to the tree.
207+
///
208+
/// The last call to `out` will be the changed root tree, whose object-id will also be returned.
209+
/// `out` is free to do any kind of additional validation, like to assure that all entries in the tree exist.
210+
/// We don't assure that as there is no validation that inserted entries are valid object ids.
211+
///
212+
/// Future calls to [`upsert`](Self::upsert) or similar will keep working on the last seen state of the
213+
/// just-written root-tree.
214+
/// If this is not desired, use [set_root()](Self::set_root()).
215+
///
216+
/// Before writing a tree, all of its entries (not only added ones), will be validated to assure they are
217+
/// correct.
218+
pub fn write(&mut self) -> Result<Id<'repo>, write::Error> {
219+
write_cursor(&mut self.to_cursor())
220+
}
221+
}
222+
223+
fn write_cursor<'repo>(cursor: &mut Cursor<'_, 'repo>) -> Result<Id<'repo>, write::Error> {
224+
cursor
225+
.inner
226+
.write(|tree| -> Result<ObjectId, write::Error> { Ok(cursor.repo.write_object(tree)?.detach()) })
227+
.map(|id| id.attach(cursor.repo))
228+
}

Diff for: 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;

Diff for: 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 {

Diff for: 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
///

Diff for: 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)