|
| 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 | +} |
0 commit comments