Skip to content

Commit 3c0b737

Browse files
committed
feat: Add Repository::new_commit and Repository::new_commit_as() methods.
Co-authored-by: Byron <[email protected]>
1 parent 1a4c84d commit 3c0b737

File tree

3 files changed

+277
-0
lines changed

3 files changed

+277
-0
lines changed

gix/examples/raw-commit-demo.rs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// Demonstrates the difference between regular commit methods and raw commit methods
2+
// Regular commits update references automatically, raw commits just create the object
3+
4+
use anyhow::Context;
5+
use gix::config::tree::{Author, Committer};
6+
7+
fn main() -> anyhow::Result<()> {
8+
let git_dir = std::env::args_os()
9+
.nth(1)
10+
.context("First argument needs to be the directory to initialize the repository in")?;
11+
let mut repo = gix::init_bare(git_dir)?;
12+
13+
println!("Repo (bare): {}", repo.git_dir().display());
14+
15+
// Set up author/committer
16+
let mut config = repo.config_snapshot_mut();
17+
config.set_raw_value(&Author::NAME, "Demo User")?;
18+
config.set_raw_value(&Author::EMAIL, "[email protected]")?;
19+
config.set_raw_value(&Committer::NAME, "Demo User")?;
20+
config.set_raw_value(&Committer::EMAIL, "[email protected]")?;
21+
let repo = config.commit_auto_rollback()?;
22+
23+
let empty_tree_id = repo.write_object(&gix::objs::Tree::empty())?.detach();
24+
25+
println!("\n=== Demonstrating commit_raw ===");
26+
27+
// Create a raw commit - this doesn't update any references
28+
let raw_commit = repo.commit_raw("Raw commit message", empty_tree_id, gix::commit::NO_PARENT_IDS)?;
29+
30+
println!("Created raw commit object (not yet written to database):");
31+
println!(" Message: {}", raw_commit.message);
32+
println!(" Tree: {}", raw_commit.tree);
33+
println!(" Author: {:?}", raw_commit.author.name);
34+
35+
// HEAD should still be unborn at this point
36+
let head_before = match repo.head() {
37+
Ok(_) => "exists",
38+
Err(_) => "unborn"
39+
};
40+
println!("HEAD status before writing raw commit: {}", head_before);
41+
42+
// Now write the commit object to the database
43+
let raw_commit_id = repo.write_object(&raw_commit)?;
44+
println!("Raw commit written to database with ID: {}", raw_commit_id);
45+
46+
// HEAD still shouldn't point to our commit since we didn't update references
47+
let head_after = match repo.head() {
48+
Ok(_) => "exists",
49+
Err(_) => "unborn"
50+
};
51+
println!("HEAD status after writing raw commit: {}", head_after);
52+
53+
println!("\n=== Demonstrating commit_as_raw ===");
54+
55+
// Create specific author/committer signatures
56+
let committer = gix::actor::Signature {
57+
name: "Committer Name".into(),
58+
email: "[email protected]".into(),
59+
time: gix_date::Time::now_local_or_utc(),
60+
};
61+
let author = gix::actor::Signature {
62+
name: "Author Name".into(),
63+
email: "[email protected]".into(),
64+
time: gix_date::Time::now_local_or_utc(),
65+
};
66+
67+
let raw_commit2 = repo.commit_as_raw(
68+
committer.to_ref(&mut Default::default()),
69+
author.to_ref(&mut Default::default()),
70+
"Second raw commit with custom author/committer",
71+
empty_tree_id,
72+
[raw_commit_id.detach()],
73+
)?;
74+
75+
println!("Created second raw commit with custom author/committer:");
76+
println!(" Message: {}", raw_commit2.message);
77+
println!(" Author: {} <{}>", raw_commit2.author.name, raw_commit2.author.email);
78+
println!(" Committer: {} <{}>", raw_commit2.committer.name, raw_commit2.committer.email);
79+
80+
let raw_commit_id2 = repo.write_object(&raw_commit2)?;
81+
println!("Second raw commit written with ID: {}", raw_commit_id2);
82+
83+
println!("\n=== Comparing with regular commit ===");
84+
85+
// First, let's update HEAD to point to our second raw commit so we can demonstrate
86+
// the difference. In practice, you might update references manually.
87+
println!("To demonstrate regular commit, we first need to set HEAD manually:");
88+
89+
// Use the regular commit method which updates HEAD automatically
90+
// For the initial commit, we'll use no parents
91+
let regular_commit_id = repo.commit("HEAD", "Regular commit that updates HEAD", empty_tree_id, gix::commit::NO_PARENT_IDS)?;
92+
println!("Regular commit created with ID: {}", regular_commit_id);
93+
94+
// Now HEAD should point to our commit
95+
let head_final = match repo.head() {
96+
Ok(mut head) => {
97+
match head.try_peel_to_id_in_place().unwrap_or(None) {
98+
Some(id) => format!("points to {}", id),
99+
None => "exists but unborn".to_string(),
100+
}
101+
}
102+
Err(_) => "unborn".to_string()
103+
};
104+
println!("HEAD status after regular commit: {}", head_final);
105+
106+
println!("\n=== Summary ===");
107+
println!("Raw commits allow you to:");
108+
println!("1. Create commit objects without updating any references");
109+
println!("2. Write them to the database when you're ready");
110+
println!("3. Have full control over when and how references are updated");
111+
println!("4. Batch commit operations for better performance");
112+
113+
Ok(())
114+
}

gix/src/repository/object.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,49 @@ impl crate::Repository {
406406
self.commit_as(committer, author, reference, message, tree, parents)
407407
}
408408

409+
/// Create a raw commit object with `message` referring to `tree` with `parents`, without writing it to the object database
410+
/// or updating any references. The commit object can later be written using [`Self::write_object()`].
411+
///
412+
/// The commit is created without message encoding field, which can be assumed to be UTF-8.
413+
/// `author` and `committer` fields are pre-set from the configuration, which can be altered
414+
/// [temporarily](crate::Repository::config_snapshot_mut()) before the call if required.
415+
pub fn commit_raw(
416+
&self,
417+
message: impl AsRef<str>,
418+
tree: impl Into<ObjectId>,
419+
parents: impl IntoIterator<Item = impl Into<ObjectId>>,
420+
) -> Result<gix_object::Commit, commit::Error> {
421+
let author = self.author().ok_or(commit::Error::AuthorMissing)??;
422+
let committer = self.committer().ok_or(commit::Error::CommitterMissing)??;
423+
self.commit_as_raw(committer, author, message, tree, parents)
424+
}
425+
426+
/// Create a raw commit object with `message` referring to `tree` with `parents`, using the specified
427+
/// `committer` and `author`, without writing it to the object database or updating any references.
428+
/// The commit object can later be written using [`Self::write_object()`].
429+
///
430+
/// This forces setting the commit time and author time by hand. Note that typically, committer and author are the same.
431+
/// The commit is created without message encoding field, which can be assumed to be UTF-8.
432+
pub fn commit_as_raw<'a, 'c>(
433+
&self,
434+
committer: impl Into<gix_actor::SignatureRef<'c>>,
435+
author: impl Into<gix_actor::SignatureRef<'a>>,
436+
message: impl AsRef<str>,
437+
tree: impl Into<ObjectId>,
438+
parents: impl IntoIterator<Item = impl Into<ObjectId>>,
439+
) -> Result<gix_object::Commit, commit::Error> {
440+
let commit = gix_object::Commit {
441+
message: message.as_ref().into(),
442+
tree: tree.into(),
443+
author: author.into().into(),
444+
committer: committer.into().into(),
445+
encoding: None,
446+
parents: parents.into_iter().map(Into::into).collect(),
447+
extra_headers: Default::default(),
448+
};
449+
Ok(commit)
450+
}
451+
409452
/// Return an empty tree object, suitable for [getting changes](Tree::changes()).
410453
///
411454
/// Note that the returned object is special and doesn't necessarily physically exist in the object database.

gix/tests/gix/repository/object.rs

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -786,6 +786,126 @@ mod commit {
786786
}
787787
}
788788

789+
mod commit_as_raw {
790+
use gix_date::parse::TimeBuf;
791+
use gix_testtools::tempfile;
792+
793+
#[test]
794+
fn specify_committer_and_author() -> crate::Result {
795+
let tmp = tempfile::tempdir()?;
796+
let repo = gix::ThreadSafeRepository::init_opts(
797+
&tmp,
798+
gix::create::Kind::WithWorktree,
799+
Default::default(),
800+
gix::open::Options::isolated(),
801+
)?
802+
.to_thread_local();
803+
let empty_tree = repo.empty_tree();
804+
let committer = gix::actor::Signature {
805+
name: "c".into(),
806+
email: "[email protected]".into(),
807+
time: gix_date::parse_header("1 +0030").unwrap(),
808+
};
809+
let author = gix::actor::Signature {
810+
name: "a".into(),
811+
email: "[email protected]".into(),
812+
time: gix_date::parse_header("3 +0100").unwrap(),
813+
};
814+
815+
let commit = repo.commit_as_raw(
816+
committer.to_ref(&mut TimeBuf::default()),
817+
author.to_ref(&mut TimeBuf::default()),
818+
"initial",
819+
empty_tree.id,
820+
gix::commit::NO_PARENT_IDS,
821+
)?;
822+
823+
let mut buf = TimeBuf::default();
824+
assert_eq!(commit.committer, committer.to_ref(&mut buf).into());
825+
assert_eq!(commit.author, author.to_ref(&mut buf).into());
826+
assert_eq!(commit.message, "initial");
827+
assert_eq!(commit.tree, empty_tree.id);
828+
assert_eq!(commit.parents.len(), 0);
829+
830+
// Verify we can write the commit separately
831+
let commit_id = repo.write_object(&commit)?;
832+
let written_commit = commit_id.object()?.into_commit();
833+
let decoded = written_commit.decode()?;
834+
assert_eq!(decoded.message, "initial");
835+
Ok(())
836+
}
837+
}
838+
839+
mod commit_raw {
840+
use gix_testtools::tempfile;
841+
842+
use crate::{freeze_time, restricted_and_git, util::hex_to_id};
843+
844+
#[test]
845+
fn creates_commit_without_writing_or_updating_refs() -> crate::Result {
846+
let tmp = tempfile::tempdir()?;
847+
let repo = gix::ThreadSafeRepository::init_opts(
848+
&tmp,
849+
gix::create::Kind::WithWorktree,
850+
Default::default(),
851+
crate::restricted(),
852+
)?
853+
.to_thread_local();
854+
let empty_tree_id = repo.write_object(gix::objs::Tree::empty())?.detach();
855+
856+
let commit = repo.commit_raw("initial", empty_tree_id, gix::commit::NO_PARENT_IDS)?;
857+
858+
assert_eq!(commit.message, "initial");
859+
assert_eq!(commit.tree, empty_tree_id);
860+
assert_eq!(commit.parents.len(), 0);
861+
862+
// Verify the commit object was not automatically written to the database
863+
// by checking that the current HEAD (if exists) doesn't point to our commit
864+
let initial_head_id = repo.head().ok().and_then(|mut head| head.try_peel_to_id_in_place().ok().flatten());
865+
866+
// Verify we can write the commit separately
867+
let commit_id = repo.write_object(&commit)?;
868+
let written_commit = commit_id.object()?.into_commit();
869+
let decoded = written_commit.decode()?;
870+
assert_eq!(decoded.message, "initial");
871+
assert_eq!(decoded.tree(), empty_tree_id);
872+
873+
// Verify that writing the commit object didn't update HEAD automatically
874+
if let Some(head_id) = initial_head_id {
875+
assert_ne!(head_id, commit_id,
876+
"HEAD should not point to our commit since we didn't update references");
877+
}
878+
879+
Ok(())
880+
}
881+
882+
#[test]
883+
#[serial_test::serial]
884+
fn commit_with_parent() -> crate::Result {
885+
let _env = freeze_time();
886+
let (repo, _keep) = crate::repo_rw_opts("make_basic_repo.sh", restricted_and_git())?;
887+
let parent = repo.find_reference("HEAD")?.peel_to_id()?;
888+
let empty_tree_id = parent.object()?.to_commit_ref_iter().tree_id().expect("tree to be set");
889+
890+
let commit = repo.commit_raw("hello there \r\n\nthe body", empty_tree_id, Some(parent))?;
891+
892+
assert_eq!(commit.message, "hello there \r\n\nthe body");
893+
assert_eq!(commit.tree, empty_tree_id);
894+
assert_eq!(commit.parents.len(), 1);
895+
assert_eq!(commit.parents[0], parent);
896+
897+
// Verify we can write the commit and get a stable ID
898+
let commit_id = repo.write_object(&commit)?;
899+
assert_eq!(
900+
commit_id,
901+
hex_to_id("e7c7273539cfc1a52802fa9d61aa578f6ccebcb4"),
902+
"the commit id is stable"
903+
);
904+
905+
Ok(())
906+
}
907+
}
908+
789909
fn empty_bare_in_memory_repo() -> crate::Result<gix::Repository> {
790910
Ok(named_subrepo_opts("make_basic_repo.sh", "bare.git", gix::open::Options::isolated())?.with_object_memory())
791911
}

0 commit comments

Comments
 (0)