1use std::path::PathBuf;
4
5use async_trait::async_trait;
6use git2::{Repository, Sort};
7use ironflow_core::error::OperationError;
8use ironflow_core::operation::{Operation, OperationContext, TypedOperation};
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11
12use crate::helpers::{blocking, to_value};
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct LogCommitEntry {
17 pub oid: String,
18 pub message: String,
19 #[serde(skip_serializing_if = "Option::is_none")]
20 pub author: Option<String>,
21 #[serde(skip_serializing_if = "Option::is_none")]
22 pub time: Option<i64>,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct RevwalkOutput {
28 pub commits: Vec<LogCommitEntry>,
29 #[serde(skip_serializing_if = "Option::is_none")]
30 pub range: Option<String>,
31}
32
33pub struct RevwalkNew {
47 repo_path: PathBuf,
48 limit: usize,
49}
50
51impl RevwalkNew {
52 pub fn new(repo_path: impl Into<PathBuf>, limit: usize) -> Self {
54 Self {
55 repo_path: repo_path.into(),
56 limit,
57 }
58 }
59
60 pub async fn run(&self, _ctx: &OperationContext) -> Result<RevwalkOutput, OperationError> {
62 let repo_path = self.repo_path.clone();
63 let limit = self.limit;
64 blocking(move || {
65 let repo = Repository::open(&repo_path)?;
66 let mut revwalk = repo.revwalk()?;
67 revwalk.push_head()?;
68 revwalk.set_sorting(Sort::TIME)?;
69 let commits = revwalk
70 .take(limit)
71 .filter_map(|oid| oid.ok())
72 .filter_map(|oid| repo.find_commit(oid).ok())
73 .map(|c| LogCommitEntry {
74 oid: c.id().to_string(),
75 message: c.message().unwrap_or("").to_string(),
76 author: Some(c.author().name().unwrap_or("").to_string()),
77 time: Some(c.time().seconds()),
78 })
79 .collect();
80 Ok(RevwalkOutput {
81 commits,
82 range: None,
83 })
84 })
85 .await
86 }
87}
88
89#[async_trait]
90impl Operation for RevwalkNew {
91 fn kind(&self) -> &str {
92 "git"
93 }
94 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
95 to_value(&self.run(ctx).await?)
96 }
97 fn input(&self) -> Option<Value> {
98 Some(serde_json::json!({ "repo_path": self.repo_path, "limit": self.limit }))
99 }
100}
101
102impl TypedOperation for RevwalkNew {
103 type Output = RevwalkOutput;
104}
105
106pub struct RevwalkPushRange {
118 repo_path: PathBuf,
119 range: String,
120 limit: usize,
121}
122
123impl RevwalkPushRange {
124 pub fn new(repo_path: impl Into<PathBuf>, range: impl Into<String>, limit: usize) -> Self {
126 Self {
127 repo_path: repo_path.into(),
128 range: range.into(),
129 limit,
130 }
131 }
132
133 pub async fn run(&self, _ctx: &OperationContext) -> Result<RevwalkOutput, OperationError> {
135 let repo_path = self.repo_path.clone();
136 let range = self.range.clone();
137 let limit = self.limit;
138 blocking(move || {
139 let repo = Repository::open(&repo_path)?;
140 let mut revwalk = repo.revwalk()?;
141 revwalk.push_range(&range)?;
142 let commits = revwalk
143 .take(limit)
144 .filter_map(|oid| oid.ok())
145 .filter_map(|oid| repo.find_commit(oid).ok())
146 .map(|c| LogCommitEntry {
147 oid: c.id().to_string(),
148 message: c.message().unwrap_or("").to_string(),
149 author: Some(c.author().name().unwrap_or("").to_string()),
150 time: Some(c.time().seconds()),
151 })
152 .collect();
153 Ok(RevwalkOutput {
154 commits,
155 range: Some(range),
156 })
157 })
158 .await
159 }
160}
161
162#[async_trait]
163impl Operation for RevwalkPushRange {
164 fn kind(&self) -> &str {
165 "git"
166 }
167 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
168 to_value(&self.run(ctx).await?)
169 }
170 fn input(&self) -> Option<Value> {
171 Some(serde_json::json!({ "repo_path": self.repo_path, "range": self.range }))
172 }
173}
174
175impl TypedOperation for RevwalkPushRange {
176 type Output = RevwalkOutput;
177}
178
179pub struct RevwalkSimplifyFirstParent {
191 repo_path: PathBuf,
192 limit: usize,
193}
194
195impl RevwalkSimplifyFirstParent {
196 pub fn new(repo_path: impl Into<PathBuf>, limit: usize) -> Self {
198 Self {
199 repo_path: repo_path.into(),
200 limit,
201 }
202 }
203
204 pub async fn run(&self, _ctx: &OperationContext) -> Result<RevwalkOutput, OperationError> {
206 let repo_path = self.repo_path.clone();
207 let limit = self.limit;
208 blocking(move || {
209 let repo = Repository::open(&repo_path)?;
210 let mut revwalk = repo.revwalk()?;
211 revwalk.push_head()?;
212 revwalk.simplify_first_parent()?;
213 let commits = revwalk
214 .take(limit)
215 .filter_map(|oid| oid.ok())
216 .filter_map(|oid| repo.find_commit(oid).ok())
217 .map(|c| LogCommitEntry {
218 oid: c.id().to_string(),
219 message: c.message().unwrap_or("").to_string(),
220 author: Some(c.author().name().unwrap_or("").to_string()),
221 time: Some(c.time().seconds()),
222 })
223 .collect();
224 Ok(RevwalkOutput {
225 commits,
226 range: None,
227 })
228 })
229 .await
230 }
231}
232
233#[async_trait]
234impl Operation for RevwalkSimplifyFirstParent {
235 fn kind(&self) -> &str {
236 "git"
237 }
238 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
239 to_value(&self.run(ctx).await?)
240 }
241 fn input(&self) -> Option<Value> {
242 Some(serde_json::json!({ "repo_path": self.repo_path, "limit": self.limit }))
243 }
244}
245
246impl TypedOperation for RevwalkSimplifyFirstParent {
247 type Output = RevwalkOutput;
248}
249
250#[cfg(test)]
251mod tests {
252 use std::fs;
253 use std::path::Path;
254
255 use git2::{Commit, Repository, Signature};
256 use ironflow_core::operation::Operation;
257
258 use super::*;
259 use crate::test_helpers::ctx;
260
261 fn init_repo_n_commits(path: &Path, n: usize) {
262 let repo = Repository::init(path).unwrap();
263 let sig = Signature::now("Test", "test@test.com").unwrap();
264 let mut parent = None;
265 for i in 0..n {
266 fs::write(path.join("file.txt"), format!("v{i}")).unwrap();
267 let mut index = repo.index().unwrap();
268 index.add_path(Path::new("file.txt")).unwrap();
269 index.write().unwrap();
270 let oid = index.write_tree().unwrap();
271 let tree = repo.find_tree(oid).unwrap();
272 let parents: Vec<&Commit<'_>> = parent.iter().collect();
273 let c = repo
274 .commit(
275 Some("HEAD"),
276 &sig,
277 &sig,
278 &format!("commit {i}"),
279 &tree,
280 &parents,
281 )
282 .unwrap();
283 parent = Some(repo.find_commit(c).unwrap());
284 }
285 }
286
287 #[tokio::test]
288 async fn revwalk_returns_commits() {
289 let tmp = tempfile::tempdir().unwrap();
290 init_repo_n_commits(tmp.path(), 3);
291 let result = RevwalkNew::new(tmp.path(), 10).run(&ctx()).await.unwrap();
292 assert_eq!(result.commits.len(), 3);
293 assert_eq!(result.commits[0].message, "commit 2");
294 assert!(result.commits[0].author.is_some());
295 }
296
297 #[tokio::test]
298 async fn revwalk_respects_limit() {
299 let tmp = tempfile::tempdir().unwrap();
300 init_repo_n_commits(tmp.path(), 5);
301 let result = RevwalkNew::new(tmp.path(), 2).run(&ctx()).await.unwrap();
302 assert_eq!(result.commits.len(), 2);
303 }
304
305 #[tokio::test]
306 async fn revwalk_push_range() {
307 let tmp = tempfile::tempdir().unwrap();
308 init_repo_n_commits(tmp.path(), 3);
309 let repo = Repository::open(tmp.path()).unwrap();
310 let mut revwalk = repo.revwalk().unwrap();
311 revwalk.push_head().unwrap();
312 let oids: Vec<_> = revwalk.filter_map(|o| o.ok()).collect();
313 let range = format!("{}..{}", oids[2], oids[0]);
314 let result = RevwalkPushRange::new(tmp.path(), &range, 100)
315 .run(&ctx())
316 .await
317 .unwrap();
318 assert_eq!(result.commits.len(), 2);
319 assert_eq!(result.range.as_deref(), Some(range.as_str()));
320 }
321
322 #[tokio::test]
323 async fn simplify_first_parent() {
324 let tmp = tempfile::tempdir().unwrap();
325 init_repo_n_commits(tmp.path(), 3);
326 let result = RevwalkSimplifyFirstParent::new(tmp.path(), 10)
327 .run(&ctx())
328 .await
329 .unwrap();
330 assert_eq!(result.commits.len(), 3);
331 }
332
333 #[tokio::test]
334 async fn execute_serializes_correctly() {
335 let tmp = tempfile::tempdir().unwrap();
336 init_repo_n_commits(tmp.path(), 2);
337 let value = RevwalkNew::new(tmp.path(), 10)
338 .execute(&ctx())
339 .await
340 .unwrap();
341 assert_eq!(value["commits"].as_array().unwrap().len(), 2);
342 }
343}