Skip to content

Commit

Permalink
feat: add tree-editing capabilities to Tree and Repository.
Browse files Browse the repository at this point in the history
TBD
  • Loading branch information
Byron committed Sep 4, 2024
1 parent b236373 commit cf15fd0
Show file tree
Hide file tree
Showing 9 changed files with 462 additions and 6 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 14 additions & 1 deletion gix/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -384,19 +393,23 @@ 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"
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 = [
"document-features",
"max-performance",
"blocking-network-client",
"blocking-http-transport-curl",
"need-more-recent-msrv",
"serde",
]
228 changes: 228 additions & 0 deletions gix/src/object/tree/editor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
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),
}
}

///
pub mod write {
/// 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),
}
}

/// 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>,
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<Self, init::Error> {
let tree_ref = tree.decode()?;
let repo = tree.repo;
Ok(super::Editor {
inner: gix_object::tree::Editor::new(tree_ref.into(), &repo.objects, repo.object_hash()),
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<super::Editor<'repo>, 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<Item = &BStr>;
}

impl ToComponents for &str {
fn to_components(&self) -> impl Iterator<Item = &BStr> {
self.split('/').map(Into::into)
}
}

impl ToComponents for String {
fn to_components(&self) -> impl Iterator<Item = &BStr> {
self.split('/').map(Into::into)
}
}

impl ToComponents for &String {
fn to_components(&self) -> impl Iterator<Item = &BStr> {
self.split('/').map(Into::into)
}
}

impl ToComponents for BString {
fn to_components(&self) -> impl Iterator<Item = &BStr> {
self.split(|b| *b == b'/').map(Into::into)
}
}

impl ToComponents for &BString {
fn to_components(&self) -> impl Iterator<Item = &BStr> {
self.split(|b| *b == b'/').map(Into::into)
}
}

impl ToComponents for &BStr {
fn to_components(&self) -> impl Iterator<Item = &BStr> {
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(),
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<Cursor<'_, 'repo>, gix_object::find::existing_object::Error> {
Ok(Cursor {
inner: self.inner.cursor_at(rela_path.to_components())?,
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<ObjectId>,
) -> Result<&mut Self, gix_object::find::existing_object::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::find::existing_object::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<Id<'repo>, 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> {
*self = super::Editor::new(root)?;
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<ObjectId>,
) -> Result<&mut Self, gix_object::find::existing_object::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::find::existing_object::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.
pub fn write(&mut self) -> Result<Id<'repo>, write::Error> {
write_cursor(&mut self.to_cursor())
}
}

fn write_cursor<'repo>(cursor: &mut Cursor<'_, 'repo>) -> Result<Id<'repo>, write::Error> {
cursor
.inner
.write(|tree| -> Result<ObjectId, write::Error> { Ok(cursor.repo.write_object(tree)?.detach()) })
.map(|id| id.attach(cursor.repo))
}
11 changes: 11 additions & 0 deletions gix/src/object/tree/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ 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>,
repo: &'repo crate::Repository,
}

/// Initialization
impl<'repo> Tree<'repo> {
/// Obtain a tree instance by handing in all components that it is made up of.
Expand Down Expand Up @@ -163,6 +170,10 @@ impl<'repo> Tree<'repo> {
}
}

///
#[cfg(feature = "tree-editor")]
pub mod editor;

///
#[cfg(feature = "blob-diff")]
pub mod diff;
Expand Down
14 changes: 14 additions & 0 deletions gix/src/repository/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
24 changes: 23 additions & 1 deletion gix/src/repository/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ObjectId>,
) -> Result<object::tree::Editor<'_>, 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.
///
Expand Down Expand Up @@ -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<u8>> {
let mut bufs = self.bufs.borrow_mut();
if bufs.last().is_none() {
Expand Down Expand Up @@ -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.
///
Expand Down
2 changes: 1 addition & 1 deletion gix/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8>,
Expand Down
Loading

0 comments on commit cf15fd0

Please sign in to comment.