1use std::path::PathBuf;
4
5use async_trait::async_trait;
6use git2::{BranchType, RebaseOperationType, Repository, Signature};
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
14fn rebase_op_label(kind: Option<RebaseOperationType>) -> &'static str {
15 match kind {
16 Some(RebaseOperationType::Pick) => "pick",
17 Some(RebaseOperationType::Reword) => "reword",
18 Some(RebaseOperationType::Edit) => "edit",
19 Some(RebaseOperationType::Squash) => "squash",
20 Some(RebaseOperationType::Fixup) => "fixup",
21 Some(RebaseOperationType::Exec) => "exec",
22 None => "unknown",
23 }
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct RebaseInitOutput {
28 pub branch: String,
29 pub upstream: String,
30 pub operations: usize,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct RebaseNextOutput {
35 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
36 pub op_type: Option<String>,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub id: Option<String>,
39 pub has_more: bool,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct RebaseCommitOutput {
44 pub oid: String,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct RebaseAbortOutput {
49 pub aborted: bool,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct RebaseFinishOutput {
54 pub finished: bool,
55}
56
57pub struct RebaseInit {
69 repo_path: PathBuf,
70 branch: String,
71 upstream: String,
72}
73
74impl RebaseInit {
75 pub fn new(
77 repo_path: impl Into<PathBuf>,
78 branch: impl Into<String>,
79 upstream: impl Into<String>,
80 ) -> Self {
81 Self {
82 repo_path: repo_path.into(),
83 branch: branch.into(),
84 upstream: upstream.into(),
85 }
86 }
87
88 pub async fn run(&self, _ctx: &OperationContext) -> Result<RebaseInitOutput, OperationError> {
90 let repo_path = self.repo_path.clone();
91 let branch = self.branch.clone();
92 let upstream = self.upstream.clone();
93 blocking(move || {
94 let repo = Repository::open(&repo_path)?;
95 let branch_ref = repo.find_branch(&branch, BranchType::Local)?;
96 let branch_commit = branch_ref.get().peel_to_commit()?;
97 let branch_annotated = repo.find_annotated_commit(branch_commit.id())?;
98 let upstream_ref = repo.find_branch(&upstream, BranchType::Local)?;
99 let upstream_commit = upstream_ref.get().peel_to_commit()?;
100 let upstream_annotated = repo.find_annotated_commit(upstream_commit.id())?;
101 let rebase = repo.rebase(
102 Some(&branch_annotated),
103 Some(&upstream_annotated),
104 None,
105 None,
106 )?;
107 let count = rebase.len();
108 Ok(RebaseInitOutput {
109 branch,
110 upstream,
111 operations: count,
112 })
113 })
114 .await
115 }
116}
117
118#[async_trait]
119impl Operation for RebaseInit {
120 fn kind(&self) -> &str {
121 "git"
122 }
123 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
124 to_value(&self.run(ctx).await?)
125 }
126 fn input(&self) -> Option<Value> {
127 Some(
128 serde_json::json!({ "repo_path": self.repo_path, "branch": self.branch, "upstream": self.upstream }),
129 )
130 }
131}
132
133impl TypedOperation for RebaseInit {
134 type Output = RebaseInitOutput;
135}
136
137pub struct RebaseNext {
149 repo_path: PathBuf,
150}
151
152impl RebaseNext {
153 pub fn new(repo_path: impl Into<PathBuf>) -> Self {
155 Self {
156 repo_path: repo_path.into(),
157 }
158 }
159
160 pub async fn run(&self, _ctx: &OperationContext) -> Result<RebaseNextOutput, OperationError> {
162 let repo_path = self.repo_path.clone();
163 blocking(move || {
164 let repo = Repository::open(&repo_path)?;
165 let mut rebase = repo.open_rebase(None)?;
166 let op = rebase.next();
167 match op {
168 Some(Ok(operation)) => Ok(RebaseNextOutput {
169 op_type: Some(rebase_op_label(operation.kind()).to_string()),
170 id: Some(operation.id().to_string()),
171 has_more: true,
172 }),
173 Some(Err(e)) => Err(e),
174 None => Ok(RebaseNextOutput {
175 op_type: None,
176 id: None,
177 has_more: false,
178 }),
179 }
180 })
181 .await
182 }
183}
184
185#[async_trait]
186impl Operation for RebaseNext {
187 fn kind(&self) -> &str {
188 "git"
189 }
190 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
191 to_value(&self.run(ctx).await?)
192 }
193 fn input(&self) -> Option<Value> {
194 Some(serde_json::json!({ "repo_path": self.repo_path }))
195 }
196}
197
198impl TypedOperation for RebaseNext {
199 type Output = RebaseNextOutput;
200}
201
202pub struct RebaseCommit {
214 repo_path: PathBuf,
215 author_name: String,
216 author_email: String,
217}
218
219impl RebaseCommit {
220 pub fn new(
222 repo_path: impl Into<PathBuf>,
223 author_name: impl Into<String>,
224 author_email: impl Into<String>,
225 ) -> Self {
226 Self {
227 repo_path: repo_path.into(),
228 author_name: author_name.into(),
229 author_email: author_email.into(),
230 }
231 }
232
233 pub async fn run(&self, _ctx: &OperationContext) -> Result<RebaseCommitOutput, OperationError> {
235 let repo_path = self.repo_path.clone();
236 let name = self.author_name.clone();
237 let email = self.author_email.clone();
238 blocking(move || {
239 let repo = Repository::open(&repo_path)?;
240 let mut rebase = repo.open_rebase(None)?;
241 let sig = Signature::now(&name, &email)?;
242 let oid = rebase.commit(None, &sig, None)?;
243 Ok(RebaseCommitOutput {
244 oid: oid.to_string(),
245 })
246 })
247 .await
248 }
249}
250
251#[async_trait]
252impl Operation for RebaseCommit {
253 fn kind(&self) -> &str {
254 "git"
255 }
256 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
257 to_value(&self.run(ctx).await?)
258 }
259 fn input(&self) -> Option<Value> {
260 Some(serde_json::json!({ "repo_path": self.repo_path }))
261 }
262}
263
264impl TypedOperation for RebaseCommit {
265 type Output = RebaseCommitOutput;
266}
267
268pub struct RebaseAbort {
280 repo_path: PathBuf,
281}
282
283impl RebaseAbort {
284 pub fn new(repo_path: impl Into<PathBuf>) -> Self {
286 Self {
287 repo_path: repo_path.into(),
288 }
289 }
290
291 pub async fn run(&self, _ctx: &OperationContext) -> Result<RebaseAbortOutput, OperationError> {
293 let repo_path = self.repo_path.clone();
294 blocking(move || {
295 let repo = Repository::open(&repo_path)?;
296 let mut rebase = repo.open_rebase(None)?;
297 rebase.abort()?;
298 Ok(RebaseAbortOutput { aborted: true })
299 })
300 .await
301 }
302}
303
304#[async_trait]
305impl Operation for RebaseAbort {
306 fn kind(&self) -> &str {
307 "git"
308 }
309 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
310 to_value(&self.run(ctx).await?)
311 }
312 fn input(&self) -> Option<Value> {
313 Some(serde_json::json!({ "repo_path": self.repo_path }))
314 }
315}
316
317impl TypedOperation for RebaseAbort {
318 type Output = RebaseAbortOutput;
319}
320
321pub struct RebaseFinish {
333 repo_path: PathBuf,
334 author_name: String,
335 author_email: String,
336}
337
338impl RebaseFinish {
339 pub fn new(
341 repo_path: impl Into<PathBuf>,
342 author_name: impl Into<String>,
343 author_email: impl Into<String>,
344 ) -> Self {
345 Self {
346 repo_path: repo_path.into(),
347 author_name: author_name.into(),
348 author_email: author_email.into(),
349 }
350 }
351
352 pub async fn run(&self, _ctx: &OperationContext) -> Result<RebaseFinishOutput, OperationError> {
354 let repo_path = self.repo_path.clone();
355 let name = self.author_name.clone();
356 let email = self.author_email.clone();
357 blocking(move || {
358 let repo = Repository::open(&repo_path)?;
359 let mut rebase = repo.open_rebase(None)?;
360 let sig = Signature::now(&name, &email)?;
361 rebase.finish(Some(&sig))?;
362 Ok(RebaseFinishOutput { finished: true })
363 })
364 .await
365 }
366}
367
368#[async_trait]
369impl Operation for RebaseFinish {
370 fn kind(&self) -> &str {
371 "git"
372 }
373 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
374 to_value(&self.run(ctx).await?)
375 }
376 fn input(&self) -> Option<Value> {
377 Some(serde_json::json!({ "repo_path": self.repo_path }))
378 }
379}
380
381impl TypedOperation for RebaseFinish {
382 type Output = RebaseFinishOutput;
383}
384
385#[cfg(test)]
386mod tests {
387 use std::fs;
388 use std::path::Path;
389
390 use git2::{Repository, Signature};
391 use ironflow_core::operation::Operation;
392
393 use super::*;
394 use crate::test_helpers::ctx;
395
396 fn init_with_branch(path: &Path) {
397 use git2::build::CheckoutBuilder;
398 let repo = Repository::init(path).unwrap();
399 let sig = Signature::now("Test", "t@t.com").unwrap();
400 fs::write(path.join("f.txt"), "base").unwrap();
401 let mut idx = repo.index().unwrap();
402 idx.add_path(Path::new("f.txt")).unwrap();
403 idx.write().unwrap();
404 let tree = repo.find_tree(idx.write_tree().unwrap()).unwrap();
405 let c1 = repo
406 .commit(Some("HEAD"), &sig, &sig, "base", &tree, &[])
407 .unwrap();
408 let base = repo.find_commit(c1).unwrap();
409 repo.branch("feature", &base, false).unwrap();
410
411 let mut tb = repo.treebuilder(Some(&tree)).unwrap();
412 tb.insert("main.txt", repo.blob(b"main-only").unwrap(), 0o100644)
413 .unwrap();
414 let main_tree = repo.find_tree(tb.write().unwrap()).unwrap();
415 repo.commit(
416 Some("HEAD"),
417 &sig,
418 &sig,
419 "main commit",
420 &main_tree,
421 &[&base],
422 )
423 .unwrap();
424
425 let mut tb2 = repo.treebuilder(Some(&tree)).unwrap();
426 tb2.insert("feat.txt", repo.blob(b"feat-only").unwrap(), 0o100644)
427 .unwrap();
428 let feat_tree = repo.find_tree(tb2.write().unwrap()).unwrap();
429 repo.commit(
430 Some("refs/heads/feature"),
431 &sig,
432 &sig,
433 "feat commit",
434 &feat_tree,
435 &[&base],
436 )
437 .unwrap();
438
439 repo.checkout_head(Some(CheckoutBuilder::new().force()))
440 .unwrap();
441 }
442
443 #[tokio::test]
444 async fn rebase_init_counts_operations() {
445 let tmp = tempfile::tempdir().unwrap();
446 init_with_branch(tmp.path());
447 let result = RebaseInit::new(tmp.path(), "feature", "master")
448 .run(&ctx())
449 .await
450 .unwrap();
451 assert_eq!(result.branch, "feature");
452 assert!(result.operations > 0);
453 }
454
455 #[tokio::test]
456 async fn rebase_init_and_abort() {
457 let tmp = tempfile::tempdir().unwrap();
458 init_with_branch(tmp.path());
459 RebaseInit::new(tmp.path(), "feature", "master")
460 .run(&ctx())
461 .await
462 .unwrap();
463 let result = RebaseAbort::new(tmp.path()).run(&ctx()).await.unwrap();
464 assert!(result.aborted);
465 }
466
467 #[tokio::test]
468 async fn execute_serializes_correctly() {
469 let tmp = tempfile::tempdir().unwrap();
470 init_with_branch(tmp.path());
471 let value = RebaseInit::new(tmp.path(), "feature", "master")
472 .execute(&ctx())
473 .await
474 .unwrap();
475 assert_eq!(value["branch"], "feature");
476 assert!(value["operations"].is_number());
477 }
478}