1use std::path::PathBuf;
4
5use async_trait::async_trait;
6use git2::{Oid, Repository};
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)]
15pub struct CherrypickOutput {
16 pub oid: String,
17 pub applied: bool,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct CherrypickConflictOutput {
22 pub has_conflicts: bool,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct RevertOutput {
27 pub oid: String,
28 pub reverted: bool,
29}
30
31pub struct Cherrypick {
43 repo_path: PathBuf,
44 oid: String,
45}
46
47impl Cherrypick {
48 pub fn new(repo_path: impl Into<PathBuf>, oid: impl Into<String>) -> Self {
50 Self {
51 repo_path: repo_path.into(),
52 oid: oid.into(),
53 }
54 }
55
56 pub async fn run(&self, _ctx: &OperationContext) -> Result<CherrypickOutput, OperationError> {
58 let repo_path = self.repo_path.clone();
59 let oid_str = self.oid.clone();
60 blocking(move || {
61 let repo = Repository::open(&repo_path)?;
62 let oid = Oid::from_str(&oid_str)?;
63 let commit = repo.find_commit(oid)?;
64 repo.cherrypick(&commit, None)?;
65 Ok(CherrypickOutput {
66 oid: oid_str,
67 applied: true,
68 })
69 })
70 .await
71 }
72}
73
74#[async_trait]
75impl Operation for Cherrypick {
76 fn kind(&self) -> &str {
77 "git"
78 }
79 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
80 to_value(&self.run(ctx).await?)
81 }
82 fn input(&self) -> Option<Value> {
83 Some(serde_json::json!({ "repo_path": self.repo_path, "oid": self.oid }))
84 }
85}
86
87impl TypedOperation for Cherrypick {
88 type Output = CherrypickOutput;
89}
90
91pub struct CherrypickCommit {
103 repo_path: PathBuf,
104 cherrypick_oid: String,
105 our_oid: String,
106}
107
108impl CherrypickCommit {
109 pub fn new(
111 repo_path: impl Into<PathBuf>,
112 cherrypick_oid: impl Into<String>,
113 our_oid: impl Into<String>,
114 ) -> Self {
115 Self {
116 repo_path: repo_path.into(),
117 cherrypick_oid: cherrypick_oid.into(),
118 our_oid: our_oid.into(),
119 }
120 }
121
122 pub async fn run(
124 &self,
125 _ctx: &OperationContext,
126 ) -> Result<CherrypickConflictOutput, OperationError> {
127 let repo_path = self.repo_path.clone();
128 let cp_oid = self.cherrypick_oid.clone();
129 let our_oid = self.our_oid.clone();
130 blocking(move || {
131 let repo = Repository::open(&repo_path)?;
132 let cp_commit = repo.find_commit(Oid::from_str(&cp_oid)?)?;
133 let our_commit = repo.find_commit(Oid::from_str(&our_oid)?)?;
134 let index = repo.cherrypick_commit(&cp_commit, &our_commit, 0, None)?;
135 Ok(CherrypickConflictOutput {
136 has_conflicts: index.has_conflicts(),
137 })
138 })
139 .await
140 }
141}
142
143#[async_trait]
144impl Operation for CherrypickCommit {
145 fn kind(&self) -> &str {
146 "git"
147 }
148 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
149 to_value(&self.run(ctx).await?)
150 }
151 fn input(&self) -> Option<Value> {
152 Some(
153 serde_json::json!({ "repo_path": self.repo_path, "cherrypick": self.cherrypick_oid, "our": self.our_oid }),
154 )
155 }
156}
157
158impl TypedOperation for CherrypickCommit {
159 type Output = CherrypickConflictOutput;
160}
161
162pub struct Revert {
174 repo_path: PathBuf,
175 oid: String,
176}
177
178impl Revert {
179 pub fn new(repo_path: impl Into<PathBuf>, oid: impl Into<String>) -> Self {
181 Self {
182 repo_path: repo_path.into(),
183 oid: oid.into(),
184 }
185 }
186
187 pub async fn run(&self, _ctx: &OperationContext) -> Result<RevertOutput, OperationError> {
189 let repo_path = self.repo_path.clone();
190 let oid_str = self.oid.clone();
191 blocking(move || {
192 let repo = Repository::open(&repo_path)?;
193 let oid = Oid::from_str(&oid_str)?;
194 let commit = repo.find_commit(oid)?;
195 repo.revert(&commit, None)?;
196 Ok(RevertOutput {
197 oid: oid_str,
198 reverted: true,
199 })
200 })
201 .await
202 }
203}
204
205#[async_trait]
206impl Operation for Revert {
207 fn kind(&self) -> &str {
208 "git"
209 }
210 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
211 to_value(&self.run(ctx).await?)
212 }
213 fn input(&self) -> Option<Value> {
214 Some(serde_json::json!({ "repo_path": self.repo_path, "oid": self.oid }))
215 }
216}
217
218impl TypedOperation for Revert {
219 type Output = RevertOutput;
220}
221
222pub struct RevertCommit {
234 repo_path: PathBuf,
235 revert_oid: String,
236 our_oid: String,
237}
238
239impl RevertCommit {
240 pub fn new(
242 repo_path: impl Into<PathBuf>,
243 revert_oid: impl Into<String>,
244 our_oid: impl Into<String>,
245 ) -> Self {
246 Self {
247 repo_path: repo_path.into(),
248 revert_oid: revert_oid.into(),
249 our_oid: our_oid.into(),
250 }
251 }
252
253 pub async fn run(
255 &self,
256 _ctx: &OperationContext,
257 ) -> Result<CherrypickConflictOutput, OperationError> {
258 let repo_path = self.repo_path.clone();
259 let rv_oid = self.revert_oid.clone();
260 let our_oid = self.our_oid.clone();
261 blocking(move || {
262 let repo = Repository::open(&repo_path)?;
263 let rv_commit = repo.find_commit(Oid::from_str(&rv_oid)?)?;
264 let our_commit = repo.find_commit(Oid::from_str(&our_oid)?)?;
265 let index = repo.revert_commit(&rv_commit, &our_commit, 0, None)?;
266 Ok(CherrypickConflictOutput {
267 has_conflicts: index.has_conflicts(),
268 })
269 })
270 .await
271 }
272}
273
274#[async_trait]
275impl Operation for RevertCommit {
276 fn kind(&self) -> &str {
277 "git"
278 }
279 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
280 to_value(&self.run(ctx).await?)
281 }
282 fn input(&self) -> Option<Value> {
283 Some(
284 serde_json::json!({ "repo_path": self.repo_path, "revert": self.revert_oid, "our": self.our_oid }),
285 )
286 }
287}
288
289impl TypedOperation for RevertCommit {
290 type Output = CherrypickConflictOutput;
291}
292
293#[cfg(test)]
294mod tests {
295 use ironflow_core::operation::Operation;
296
297 use super::*;
298 use crate::test_helpers::{ctx, make_two_commits};
299
300 #[tokio::test]
301 async fn cherrypick_commit_no_conflict() {
302 let tmp = tempfile::tempdir().unwrap();
303 let (c1, c2) = make_two_commits(tmp.path());
304 let result = CherrypickCommit::new(tmp.path(), &c2, &c1)
305 .run(&ctx())
306 .await
307 .unwrap();
308 assert!(!result.has_conflicts);
309 }
310
311 #[tokio::test]
312 async fn revert_commit_no_conflict() {
313 let tmp = tempfile::tempdir().unwrap();
314 let (_c1, c2) = make_two_commits(tmp.path());
315 let result = RevertCommit::new(tmp.path(), &c2, &c2)
316 .run(&ctx())
317 .await
318 .unwrap();
319 assert!(!result.has_conflicts);
320 }
321
322 #[tokio::test]
323 async fn cherrypick_applies_to_workdir() {
324 let tmp = tempfile::tempdir().unwrap();
325 let (_, c2) = make_two_commits(tmp.path());
326 let result = Cherrypick::new(tmp.path(), &c2).run(&ctx()).await.unwrap();
327 assert!(result.applied);
328 assert_eq!(result.oid, c2);
329 }
330
331 #[tokio::test]
332 async fn cherrypick_invalid_oid_fails() {
333 let tmp = tempfile::tempdir().unwrap();
334 make_two_commits(tmp.path());
335 assert!(
336 Cherrypick::new(tmp.path(), "bad-oid")
337 .run(&ctx())
338 .await
339 .is_err()
340 );
341 }
342
343 #[tokio::test]
344 async fn execute_serializes_correctly() {
345 let tmp = tempfile::tempdir().unwrap();
346 let (c1, c2) = make_two_commits(tmp.path());
347 let value = CherrypickCommit::new(tmp.path(), &c2, &c1)
348 .execute(&ctx())
349 .await
350 .unwrap();
351 assert!(!value["has_conflicts"].as_bool().unwrap());
352 }
353}