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