gitkraft_core/features/commits/
ops.rs1use anyhow::{Context, Result};
4use git2::Repository;
5
6use super::types::CommitInfo;
7
8pub fn list_commits(repo: &Repository, max_count: usize) -> Result<Vec<CommitInfo>> {
12 let mut revwalk = repo.revwalk().context("failed to create revwalk")?;
13 revwalk
14 .push_head()
15 .context("failed to push HEAD to revwalk")?;
16 revwalk
17 .set_sorting(git2::Sort::TIME | git2::Sort::TOPOLOGICAL)
18 .context("failed to set revwalk sorting")?;
19
20 let mut commits = Vec::with_capacity(max_count.min(256));
21 for oid_result in revwalk {
22 if commits.len() >= max_count {
23 break;
24 }
25 let oid = oid_result.context("revwalk iteration error")?;
26 let commit = repo
27 .find_commit(oid)
28 .with_context(|| format!("failed to find commit {oid}"))?;
29 commits.push(CommitInfo::from_git2_commit(&commit));
30 }
31
32 Ok(commits)
33}
34
35pub fn create_commit(repo: &Repository, message: &str) -> Result<CommitInfo> {
40 let sig = repo.signature().context(
41 "failed to obtain default signature — set user.name and user.email in git config",
42 )?;
43
44 let mut index = repo.index().context("failed to read index")?;
45 let tree_oid = index
46 .write_tree()
47 .context("failed to write index to tree — are there staged changes?")?;
48 let tree = repo
49 .find_tree(tree_oid)
50 .context("failed to find tree written from index")?;
51
52 let parent_commit;
54 let parents: Vec<&git2::Commit<'_>> = if let Ok(head_ref) = repo.head() {
55 let head_oid = head_ref
56 .target()
57 .context("HEAD is not a direct reference")?;
58 parent_commit = repo
59 .find_commit(head_oid)
60 .context("failed to find HEAD commit")?;
61 vec![&parent_commit]
62 } else {
63 vec![]
65 };
66
67 let oid = repo
68 .commit(Some("HEAD"), &sig, &sig, message, &tree, &parents)
69 .context("failed to create commit")?;
70
71 let commit = repo
72 .find_commit(oid)
73 .context("failed to look up newly created commit")?;
74
75 Ok(CommitInfo::from_git2_commit(&commit))
76}
77
78pub fn get_commit_details(repo: &Repository, oid_str: &str) -> Result<CommitInfo> {
80 let oid =
81 git2::Oid::from_str(oid_str).with_context(|| format!("invalid OID string: {oid_str}"))?;
82 let commit = repo
83 .find_commit(oid)
84 .with_context(|| format!("commit {oid_str} not found"))?;
85
86 Ok(CommitInfo::from_git2_commit(&commit))
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92 use tempfile::TempDir;
93
94 fn setup_repo_with_commit() -> (TempDir, Repository) {
96 let dir = TempDir::new().unwrap();
97 let repo = Repository::init(dir.path()).unwrap();
98
99 let mut config = repo.config().unwrap();
101 config.set_str("user.name", "Test User").unwrap();
102 config.set_str("user.email", "test@example.com").unwrap();
103
104 let file_path = dir.path().join("hello.txt");
106 std::fs::write(&file_path, "hello world\n").unwrap();
107 {
108 let mut index = repo.index().unwrap();
109 index.add_path(std::path::Path::new("hello.txt")).unwrap();
110 index.write().unwrap();
111
112 let tree_oid = index.write_tree().unwrap();
113 let tree = repo.find_tree(tree_oid).unwrap();
114 let sig = repo.signature().unwrap();
115 repo.commit(Some("HEAD"), &sig, &sig, "initial commit", &tree, &[])
116 .unwrap();
117 }
118
119 (dir, repo)
120 }
121
122 #[test]
123 fn list_commits_returns_initial_commit() {
124 let (_dir, repo) = setup_repo_with_commit();
125 let commits = list_commits(&repo, 10).unwrap();
126 assert_eq!(commits.len(), 1);
127 assert_eq!(commits[0].summary, "initial commit");
128 assert!(!commits[0].oid.is_empty());
129 assert_eq!(commits[0].short_oid.len(), 7);
130 assert!(commits[0].parent_ids.is_empty());
131 }
132
133 #[test]
134 fn create_commit_works() {
135 let (dir, repo) = setup_repo_with_commit();
136
137 std::fs::write(dir.path().join("hello.txt"), "updated\n").unwrap();
139 let mut index = repo.index().unwrap();
140 index.add_path(std::path::Path::new("hello.txt")).unwrap();
141 index.write().unwrap();
142
143 let info = create_commit(&repo, "second commit").unwrap();
144 assert_eq!(info.summary, "second commit");
145 assert_eq!(info.parent_ids.len(), 1);
146 }
147
148 #[test]
149 fn get_commit_details_works() {
150 let (_dir, repo) = setup_repo_with_commit();
151 let commits = list_commits(&repo, 1).unwrap();
152 let oid_str = &commits[0].oid;
153 let detail = get_commit_details(&repo, oid_str).unwrap();
154 assert_eq!(detail.oid, *oid_str);
155 assert_eq!(detail.summary, "initial commit");
156 }
157
158 #[test]
159 fn get_commit_details_bad_oid() {
160 let (_dir, repo) = setup_repo_with_commit();
161 let result = get_commit_details(&repo, "not-a-valid-oid");
162 assert!(result.is_err());
163 }
164
165 #[test]
166 fn list_commits_respects_max_count() {
167 let (dir, repo) = setup_repo_with_commit();
168
169 std::fs::write(dir.path().join("second.txt"), "two\n").unwrap();
171 let mut index = repo.index().unwrap();
172 index.add_path(std::path::Path::new("second.txt")).unwrap();
173 index.write().unwrap();
174 create_commit(&repo, "second commit").unwrap();
175
176 let one = list_commits(&repo, 1).unwrap();
177 assert_eq!(one.len(), 1);
178 assert_eq!(one[0].summary, "second commit");
179
180 let both = list_commits(&repo, 100).unwrap();
181 assert_eq!(both.len(), 2);
182 }
183}