diff --git a/gix/src/commit.rs b/gix/src/commit.rs index 6b940e6cb80..e8dd662bfb3 100644 --- a/gix/src/commit.rs +++ b/gix/src/commit.rs @@ -6,7 +6,7 @@ use std::convert::Infallible; /// An empty array of a type usable with the `gix::easy` API to help declaring no parents should be used pub const NO_PARENT_IDS: [gix_hash::ObjectId; 0] = []; -/// The error returned by [`commit(…)`][crate::Repository::commit()]. +/// The error returned by [`commit(…)`](crate::Repository::commit()). #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { diff --git a/gix/src/lib.rs b/gix/src/lib.rs index 17b30d7893c..d5c6ff4407f 100644 --- a/gix/src/lib.rs +++ b/gix/src/lib.rs @@ -86,7 +86,7 @@ doc = ::document_features::document_features!() )] #![cfg_attr(all(doc, feature = "document-features"), feature(doc_cfg, doc_auto_cfg))] -#![deny(missing_docs, rust_2018_idioms, unsafe_code)] +#![deny(missing_docs, unsafe_code)] #![allow(clippy::result_large_err)] // Re-exports to make this a potential one-stop shop crate avoiding people from having to reference various crates themselves. diff --git a/gix/src/repository/mod.rs b/gix/src/repository/mod.rs index 869616f7948..ae1b55b64d0 100644 --- a/gix/src/repository/mod.rs +++ b/gix/src/repository/mod.rs @@ -63,6 +63,36 @@ mod submodule; mod thread_safe; mod worktree; +/// +mod new_commit { + /// The error returned by [`new_commit(…)`](crate::Repository::new_commit()). + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error(transparent)] + ParseTime(#[from] crate::config::time::Error), + #[error("Committer identity is not configured")] + CommitterMissing, + #[error("Author identity is not configured")] + AuthorMissing, + #[error(transparent)] + NewCommitAs(#[from] crate::repository::new_commit_as::Error), + } +} + +/// +mod new_commit_as { + /// The error returned by [`new_commit_as(…)`](crate::Repository::new_commit_as()). + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error(transparent)] + WriteObject(#[from] crate::object::write::Error), + #[error(transparent)] + FindCommit(#[from] crate::object::find::existing::Error), + } +} + /// #[cfg(feature = "blame")] pub mod blame_file { diff --git a/gix/src/repository/object.rs b/gix/src/repository/object.rs index 2078c3e98d2..74bbe4242b5 100644 --- a/gix/src/repository/object.rs +++ b/gix/src/repository/object.rs @@ -10,6 +10,7 @@ use gix_ref::{ }; use smallvec::SmallVec; +use crate::repository::{new_commit, new_commit_as}; use crate::{commit, ext::ObjectIdExt, object, tag, Blob, Commit, Id, Object, Reference, Tag, Tree}; /// Tree editing @@ -264,7 +265,7 @@ 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. /// - /// It will be created with `constraint` which is most commonly to [only create it][PreviousValue::MustNotExist] + /// It will be created with `constraint` which is most commonly to [only create it](PreviousValue::MustNotExist) /// or to [force overwriting a possibly existing tag](PreviousValue::Any). pub fn tag( &self, @@ -406,6 +407,49 @@ impl crate::Repository { self.commit_as(committer, author, reference, message, tree, parents) } + /// Create a new commit object with `message` referring to `tree` with `parents`, and write it to the object database. + /// Do not, however, update any references. + /// + /// The commit is created without message encoding field, which can be assumed to be UTF-8. + /// `author` and `committer` fields are pre-set from the configuration, which can be altered + /// [temporarily](crate::Repository::config_snapshot_mut()) before the call if required. + pub fn new_commit( + &self, + message: impl AsRef, + tree: impl Into, + parents: impl IntoIterator>, + ) -> Result, new_commit::Error> { + let author = self.author().ok_or(new_commit::Error::AuthorMissing)??; + let committer = self.committer().ok_or(new_commit::Error::CommitterMissing)??; + Ok(self.new_commit_as(committer, author, message, tree, parents)?) + } + + /// Create a nwe commit object with `message` referring to `tree` with `parents`, using the specified + /// `committer` and `author`, and write it to the object database. Do not, however, update any references. + /// + /// This forces setting the commit time and author time by hand. Note that typically, committer and author are the same. + /// The commit is created without message encoding field, which can be assumed to be UTF-8. + pub fn new_commit_as<'a, 'c>( + &self, + committer: impl Into>, + author: impl Into>, + message: impl AsRef, + tree: impl Into, + parents: impl IntoIterator>, + ) -> Result, new_commit_as::Error> { + let commit = gix_object::Commit { + message: message.as_ref().into(), + tree: tree.into(), + author: author.into().into(), + committer: committer.into().into(), + encoding: None, + parents: parents.into_iter().map(Into::into).collect(), + extra_headers: Default::default(), + }; + let id = self.write_object(commit)?; + Ok(id.object()?.into_commit()) + } + /// Return an empty tree object, suitable for [getting changes](Tree::changes()). /// /// Note that the returned object is special and doesn't necessarily physically exist in the object database. diff --git a/gix/tests/gix/repository/object.rs b/gix/tests/gix/repository/object.rs index 70d7c3fbec6..3ef570606a6 100644 --- a/gix/tests/gix/repository/object.rs +++ b/gix/tests/gix/repository/object.rs @@ -1,8 +1,9 @@ +use gix_date::parse::TimeBuf; use gix_odb::Header; use gix_pack::Find; use gix_testtools::tempfile; -use crate::util::named_subrepo_opts; +use crate::util::{hex_to_id, named_subrepo_opts}; mod object_database_impl { use gix_object::{Exists, Find, FindHeader}; @@ -786,6 +787,68 @@ mod commit { } } +#[test] +fn new_commit_as() -> crate::Result { + let repo = empty_bare_in_memory_repo()?; + let empty_tree = repo.empty_tree(); + let committer = gix::actor::Signature { + name: "c".into(), + email: "c@example.com".into(), + time: gix_date::parse_header("1 +0030").unwrap(), + }; + let author = gix::actor::Signature { + name: "a".into(), + email: "a@example.com".into(), + time: gix_date::parse_header("3 +0100").unwrap(), + }; + + let commit = repo.new_commit_as( + committer.to_ref(&mut TimeBuf::default()), + author.to_ref(&mut TimeBuf::default()), + "message", + empty_tree.id, + gix::commit::NO_PARENT_IDS, + )?; + + assert_eq!( + commit.id, + hex_to_id("b51277f2b2ea77676dd6fa877b5eb5ba2f7094d9"), + "The commit-id is stable as the author/committer is controlled" + ); + + let commit = commit.decode()?; + + let mut buf = TimeBuf::default(); + assert_eq!(commit.committer, committer.to_ref(&mut buf)); + assert_eq!(commit.author, author.to_ref(&mut buf)); + assert_eq!(commit.message, "message"); + assert_eq!(commit.tree(), empty_tree.id); + assert_eq!(commit.parents.len(), 0); + + assert!(repo.head()?.is_unborn(), "The head-ref wasn't touched"); + Ok(()) +} + +#[test] +fn new_commit() -> crate::Result { + let mut repo = empty_bare_in_memory_repo()?; + let mut config = repo.config_snapshot_mut(); + config.set_value(&gix::config::tree::User::NAME, "user")?; + config.set_value(&gix::config::tree::User::EMAIL, "user@example.com")?; + config.commit()?; + + let empty_tree_id = repo.object_hash().empty_tree(); + let commit = repo.new_commit("initial", empty_tree_id, gix::commit::NO_PARENT_IDS)?; + let commit = commit.decode()?; + + assert_eq!(commit.message, "initial"); + assert_eq!(commit.tree(), empty_tree_id); + assert_eq!(commit.parents.len(), 0); + + assert!(repo.head()?.is_unborn(), "The head-ref wasn't touched"); + Ok(()) +} + fn empty_bare_in_memory_repo() -> crate::Result { Ok(named_subrepo_opts("make_basic_repo.sh", "bare.git", gix::open::Options::isolated())?.with_object_memory()) }