Commit

Author:

Hash:

Timestamp:

+107 -0 +/-2 browse

Kevin Schoon [me@kevinschoon.com]

f5c0d4dac09c400f7fd40c1044d8a31710e51f89

Thu, 28 Aug 2025 11:20:45 +0000 (2 months ago)

add init and write_content functions to git wrapper
1diff --git a/crates/git/src/testing.rs b/crates/git/src/testing.rs
2index fe2678e..c342848 100644
3--- a/crates/git/src/testing.rs
4+++ b/crates/git/src/testing.rs
5 @@ -13,11 +13,21 @@ const DEFAULT_GIT_CONFIG: &str = r#"
6 defaultBranch = main
7 "#;
8
9+ pub const DEFAULT_TIMESTAMP: &str = "Sun Sep 13 12:26:40 PM UTC 2020"; // @160000000
10+
11 /// return escaped shell variables with timestamps for use in testing
12 pub fn timestamp_envs(timestamp: &str) -> String {
13 format!("GIT_COMMITTER_DATE='{timestamp}' GIT_AUTHOR_DATE='{timestamp}'",)
14 }
15
16+ /// Initial commit as a shell command
17+ pub fn init_commit() -> String {
18+ let timestamp = timestamp_envs(DEFAULT_TIMESTAMP);
19+ format!(
20+ "echo 'hello world' > README.md && git add README.md && {timestamp} git commit -m 'init'"
21+ )
22+ }
23+
24 /// Initialize a test repository running all of the commands provided within
25 pub fn init(base_dir: &Path, bare: bool, commands: &[&str]) -> Result<PathBuf, std::io::Error> {
26 let source_path = base_dir.join("src");
27 diff --git a/crates/git/src/wrapper.rs b/crates/git/src/wrapper.rs
28index eef0b7b..bd40516 100644
29--- a/crates/git/src/wrapper.rs
30+++ b/crates/git/src/wrapper.rs
31 @@ -59,6 +59,23 @@ impl Wrapper {
32 })
33 }
34
35+ /// Initialize an empty repository with a new commit
36+ pub fn init(&self, username: &str, email: &str, message: &str) -> Result<(), Error> {
37+ let tree_id = {
38+ let mut index = self.repository.index()?;
39+ index.write_tree()?
40+ };
41+ let tree = self.repository.find_tree(tree_id)?;
42+ let now = std::time::SystemTime::now()
43+ .duration_since(std::time::UNIX_EPOCH)
44+ .unwrap();
45+ let timestamp = git2::Time::new(now.as_secs() as i64, 0);
46+ let signature = git2::Signature::new(username, email, &timestamp)?;
47+ self.repository
48+ .commit(Some("HEAD"), &signature, &signature, message, &tree, &[])?;
49+ Ok(())
50+ }
51+
52 pub fn path(&self) -> &Path {
53 self.path.as_path()
54 }
55 @@ -1032,6 +1049,42 @@ impl Wrapper {
56 let output = command.spawn()?.wait()?;
57 Ok(Some(output.success()))
58 }
59+
60+ /// Upsert all of the content into the underlying repository as an automated commit
61+ pub fn write(
62+ &self,
63+ username: &str,
64+ email: &str,
65+ message: &str,
66+ content: &[(&Path, &[u8])],
67+ ) -> Result<(), Error> {
68+ let head = self.repository.head()?;
69+ let latest_commit = head.peel_to_commit()?;
70+ let latest_tree = head.peel_to_tree()?;
71+ let mut update = git2::build::TreeUpdateBuilder::new();
72+ content.iter().try_for_each(|(path, content_bytes)| {
73+ // TODO: If blobs are identical no need to update?
74+ let blob_id = self.repository.blob(content_bytes)?;
75+ update.upsert(path, blob_id, git2::FileMode::Blob);
76+ Ok::<_, Error>(())
77+ })?;
78+ let new_tree_id = update.create_updated(&self.repository, &latest_tree)?;
79+ let new_tree = self.repository.find_tree(new_tree_id).unwrap();
80+ let now = std::time::SystemTime::now()
81+ .duration_since(std::time::UNIX_EPOCH)
82+ .unwrap();
83+ let timestamp = git2::Time::new(now.as_secs() as i64, 0);
84+ let signature = git2::Signature::new(username, email, &timestamp)?;
85+ self.repository.commit(
86+ Some("HEAD"),
87+ &signature,
88+ &signature,
89+ message,
90+ &new_tree,
91+ &[&latest_commit],
92+ )?;
93+ Ok(())
94+ }
95 }
96
97 #[cfg(test)]
98 @@ -1175,4 +1228,48 @@ mod tests {
99 assert!(branch_1.name.trim_end() == "another-branch");
100 assert!(branch_1.origin.clone().unwrap().message.trim_end() == "commit 5");
101 }
102+
103+ #[test]
104+ fn initialize() {
105+ let test_dir = tempfile::tempdir().unwrap();
106+ let test_repo_path = testing::init(test_dir.path(), false, &[]).unwrap();
107+ let repository = Wrapper::new(&test_repo_path).unwrap();
108+ repository
109+ .init("Automated User", "demo@example.org", "Hello World!")
110+ .unwrap();
111+ assert!(repository.latest_hash().unwrap().is_some());
112+ }
113+
114+ #[test]
115+ fn write_content() {
116+ let test_dir = tempfile::tempdir().unwrap();
117+ let test_repo_path =
118+ testing::init(test_dir.path(), false, &[&testing::init_commit()]).unwrap();
119+ let repository = Wrapper::new(&test_repo_path).unwrap();
120+ repository
121+ .write(
122+ "Automated Commit",
123+ "hello@example.org",
124+ "Content Test",
125+ &[
126+ (Path::new("fuu"), "bar".as_bytes()),
127+ (Path::new("a/b/c/baz"), "qux".as_bytes()),
128+ ],
129+ )
130+ .unwrap();
131+ assert!(
132+ repository
133+ .read_string(Path::new("fuu"), None)
134+ .unwrap()
135+ .unwrap()
136+ == "bar"
137+ );
138+ assert!(
139+ repository
140+ .read_string(Path::new("a/b/c/baz"), None)
141+ .unwrap()
142+ .unwrap()
143+ == "qux"
144+ );
145+ }
146 }