1use std::path::PathBuf;
4
5use async_trait::async_trait;
6use git2::build::CheckoutBuilder;
7use git2::{BranchType, Repository};
8use ironflow_core::error::OperationError;
9use ironflow_core::operation::{Operation, OperationContext, TypedOperation};
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12
13use crate::helpers::{blocking, to_value};
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct BranchCreateOutput {
17 pub name: String,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct BranchDeleteOutput {
22 pub name: String,
23 pub deleted: bool,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct BranchRenameOutput {
28 pub old_name: String,
29 pub new_name: String,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct BranchEntry {
35 pub name: String,
36 pub is_head: bool,
37 #[serde(rename = "type")]
38 pub branch_type: String,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct BranchListOutput {
43 pub branches: Vec<BranchEntry>,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct BranchLookupOutput {
48 pub name: String,
49 pub is_head: bool,
50 pub target: Option<String>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct BranchIsHeadOutput {
55 pub name: String,
56 pub is_head: bool,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct BranchSetUpstreamOutput {
61 pub branch: String,
62 pub upstream: String,
63}
64
65pub struct BranchCreate {
77 repo_path: PathBuf,
78 name: String,
79}
80
81impl BranchCreate {
82 pub fn new(repo_path: impl Into<PathBuf>, name: impl Into<String>) -> Self {
84 Self {
85 repo_path: repo_path.into(),
86 name: name.into(),
87 }
88 }
89
90 pub async fn run(&self, _ctx: &OperationContext) -> Result<BranchCreateOutput, OperationError> {
92 let repo_path = self.repo_path.clone();
93 let name = self.name.clone();
94 blocking(move || {
95 let repo = Repository::open(&repo_path)?;
96 let head = repo.head()?.peel_to_commit()?;
97 repo.branch(&name, &head, false)?;
98 Ok(BranchCreateOutput { name })
99 })
100 .await
101 }
102}
103
104#[async_trait]
105impl Operation for BranchCreate {
106 fn kind(&self) -> &str {
107 "git"
108 }
109 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
110 to_value(&self.run(ctx).await?)
111 }
112 fn input(&self) -> Option<Value> {
113 Some(serde_json::json!({ "repo_path": self.repo_path, "name": self.name }))
114 }
115}
116
117impl TypedOperation for BranchCreate {
118 type Output = BranchCreateOutput;
119}
120
121pub struct BranchDelete {
133 repo_path: PathBuf,
134 name: String,
135 remote: bool,
136}
137
138impl BranchDelete {
139 pub fn new(repo_path: impl Into<PathBuf>, name: impl Into<String>, remote: bool) -> Self {
143 Self {
144 repo_path: repo_path.into(),
145 name: name.into(),
146 remote,
147 }
148 }
149
150 pub async fn run(&self, _ctx: &OperationContext) -> Result<BranchDeleteOutput, OperationError> {
152 let repo_path = self.repo_path.clone();
153 let name = self.name.clone();
154 let branch_type = if self.remote {
155 BranchType::Remote
156 } else {
157 BranchType::Local
158 };
159 blocking(move || {
160 let repo = Repository::open(&repo_path)?;
161 let mut branch = repo.find_branch(&name, branch_type)?;
162 branch.delete()?;
163 Ok(BranchDeleteOutput {
164 name,
165 deleted: true,
166 })
167 })
168 .await
169 }
170}
171
172#[async_trait]
173impl Operation for BranchDelete {
174 fn kind(&self) -> &str {
175 "git"
176 }
177 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
178 to_value(&self.run(ctx).await?)
179 }
180 fn input(&self) -> Option<Value> {
181 Some(
182 serde_json::json!({ "repo_path": self.repo_path, "name": self.name, "remote": self.remote }),
183 )
184 }
185}
186
187impl TypedOperation for BranchDelete {
188 type Output = BranchDeleteOutput;
189}
190
191pub struct BranchRename {
203 repo_path: PathBuf,
204 old_name: String,
205 new_name: String,
206 force: bool,
207}
208
209impl BranchRename {
210 pub fn new(
212 repo_path: impl Into<PathBuf>,
213 old_name: impl Into<String>,
214 new_name: impl Into<String>,
215 force: bool,
216 ) -> Self {
217 Self {
218 repo_path: repo_path.into(),
219 old_name: old_name.into(),
220 new_name: new_name.into(),
221 force,
222 }
223 }
224
225 pub async fn run(&self, _ctx: &OperationContext) -> Result<BranchRenameOutput, OperationError> {
227 let repo_path = self.repo_path.clone();
228 let old = self.old_name.clone();
229 let new = self.new_name.clone();
230 let force = self.force;
231 blocking(move || {
232 let repo = Repository::open(&repo_path)?;
233 let mut branch = repo.find_branch(&old, BranchType::Local)?;
234 branch.rename(&new, force)?;
235 Ok(BranchRenameOutput {
236 old_name: old,
237 new_name: new,
238 })
239 })
240 .await
241 }
242}
243
244#[async_trait]
245impl Operation for BranchRename {
246 fn kind(&self) -> &str {
247 "git"
248 }
249 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
250 to_value(&self.run(ctx).await?)
251 }
252 fn input(&self) -> Option<Value> {
253 Some(serde_json::json!({
254 "repo_path": self.repo_path,
255 "old_name": self.old_name,
256 "new_name": self.new_name,
257 }))
258 }
259}
260
261impl TypedOperation for BranchRename {
262 type Output = BranchRenameOutput;
263}
264
265pub struct BranchList {
277 repo_path: PathBuf,
278 filter: Option<BranchType>,
279}
280
281impl BranchList {
282 pub fn local(repo_path: impl Into<PathBuf>) -> Self {
284 Self {
285 repo_path: repo_path.into(),
286 filter: Some(BranchType::Local),
287 }
288 }
289
290 pub fn remote(repo_path: impl Into<PathBuf>) -> Self {
292 Self {
293 repo_path: repo_path.into(),
294 filter: Some(BranchType::Remote),
295 }
296 }
297
298 pub fn all(repo_path: impl Into<PathBuf>) -> Self {
300 Self {
301 repo_path: repo_path.into(),
302 filter: None,
303 }
304 }
305
306 pub async fn run(&self, _ctx: &OperationContext) -> Result<BranchListOutput, OperationError> {
308 let repo_path = self.repo_path.clone();
309 let filter = self.filter;
310 blocking(move || {
311 let repo = Repository::open(&repo_path)?;
312 let branches = repo.branches(filter)?;
313 let list = branches
314 .filter_map(|b| b.ok())
315 .map(|(branch, bt)| BranchEntry {
316 name: branch.name().ok().flatten().unwrap_or("").to_string(),
317 is_head: branch.is_head(),
318 branch_type: match bt {
319 BranchType::Local => "local".to_string(),
320 BranchType::Remote => "remote".to_string(),
321 },
322 })
323 .collect();
324 Ok(BranchListOutput { branches: list })
325 })
326 .await
327 }
328}
329
330#[async_trait]
331impl Operation for BranchList {
332 fn kind(&self) -> &str {
333 "git"
334 }
335 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
336 to_value(&self.run(ctx).await?)
337 }
338 fn input(&self) -> Option<Value> {
339 Some(serde_json::json!({ "repo_path": self.repo_path }))
340 }
341}
342
343impl TypedOperation for BranchList {
344 type Output = BranchListOutput;
345}
346
347pub struct BranchLookup {
359 repo_path: PathBuf,
360 name: String,
361 remote: bool,
362}
363
364impl BranchLookup {
365 pub fn new(repo_path: impl Into<PathBuf>, name: impl Into<String>, remote: bool) -> Self {
367 Self {
368 repo_path: repo_path.into(),
369 name: name.into(),
370 remote,
371 }
372 }
373
374 pub async fn run(&self, _ctx: &OperationContext) -> Result<BranchLookupOutput, OperationError> {
376 let repo_path = self.repo_path.clone();
377 let name = self.name.clone();
378 let bt = if self.remote {
379 BranchType::Remote
380 } else {
381 BranchType::Local
382 };
383 blocking(move || {
384 let repo = Repository::open(&repo_path)?;
385 let branch = repo.find_branch(&name, bt)?;
386 let target = branch.get().target().map(|o| o.to_string());
387 Ok(BranchLookupOutput {
388 name,
389 is_head: branch.is_head(),
390 target,
391 })
392 })
393 .await
394 }
395}
396
397#[async_trait]
398impl Operation for BranchLookup {
399 fn kind(&self) -> &str {
400 "git"
401 }
402 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
403 to_value(&self.run(ctx).await?)
404 }
405 fn input(&self) -> Option<Value> {
406 Some(serde_json::json!({ "repo_path": self.repo_path, "name": self.name }))
407 }
408}
409
410impl TypedOperation for BranchLookup {
411 type Output = BranchLookupOutput;
412}
413
414pub struct BranchIsHead {
426 repo_path: PathBuf,
427 name: String,
428}
429
430impl BranchIsHead {
431 pub fn new(repo_path: impl Into<PathBuf>, name: impl Into<String>) -> Self {
433 Self {
434 repo_path: repo_path.into(),
435 name: name.into(),
436 }
437 }
438
439 pub async fn run(&self, _ctx: &OperationContext) -> Result<BranchIsHeadOutput, OperationError> {
441 let repo_path = self.repo_path.clone();
442 let name = self.name.clone();
443 blocking(move || {
444 let repo = Repository::open(&repo_path)?;
445 let branch = repo.find_branch(&name, BranchType::Local)?;
446 Ok(BranchIsHeadOutput {
447 name,
448 is_head: branch.is_head(),
449 })
450 })
451 .await
452 }
453}
454
455#[async_trait]
456impl Operation for BranchIsHead {
457 fn kind(&self) -> &str {
458 "git"
459 }
460 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
461 to_value(&self.run(ctx).await?)
462 }
463 fn input(&self) -> Option<Value> {
464 Some(serde_json::json!({ "repo_path": self.repo_path, "name": self.name }))
465 }
466}
467
468impl TypedOperation for BranchIsHead {
469 type Output = BranchIsHeadOutput;
470}
471
472pub struct BranchSetUpstream {
484 repo_path: PathBuf,
485 branch_name: String,
486 upstream_name: String,
487}
488
489impl BranchSetUpstream {
490 pub fn new(
492 repo_path: impl Into<PathBuf>,
493 branch_name: impl Into<String>,
494 upstream_name: impl Into<String>,
495 ) -> Self {
496 Self {
497 repo_path: repo_path.into(),
498 branch_name: branch_name.into(),
499 upstream_name: upstream_name.into(),
500 }
501 }
502
503 pub async fn run(
505 &self,
506 _ctx: &OperationContext,
507 ) -> Result<BranchSetUpstreamOutput, OperationError> {
508 let repo_path = self.repo_path.clone();
509 let branch_name = self.branch_name.clone();
510 let upstream = self.upstream_name.clone();
511 blocking(move || {
512 let repo = Repository::open(&repo_path)?;
513 let mut branch = repo.find_branch(&branch_name, BranchType::Local)?;
514 branch.set_upstream(Some(&upstream))?;
515 Ok(BranchSetUpstreamOutput {
516 branch: branch_name,
517 upstream,
518 })
519 })
520 .await
521 }
522}
523
524#[async_trait]
525impl Operation for BranchSetUpstream {
526 fn kind(&self) -> &str {
527 "git"
528 }
529 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
530 to_value(&self.run(ctx).await?)
531 }
532 fn input(&self) -> Option<Value> {
533 Some(serde_json::json!({
534 "repo_path": self.repo_path,
535 "branch": self.branch_name,
536 "upstream": self.upstream_name,
537 }))
538 }
539}
540
541impl TypedOperation for BranchSetUpstream {
542 type Output = BranchSetUpstreamOutput;
543}
544
545#[derive(Debug, Clone, Serialize, Deserialize)]
546pub struct BranchCheckoutOutput {
547 pub name: String,
548}
549
550pub struct BranchCheckout {
562 repo_path: PathBuf,
563 name: String,
564}
565
566impl BranchCheckout {
567 pub fn new(repo_path: impl Into<PathBuf>, name: impl Into<String>) -> Self {
569 Self {
570 repo_path: repo_path.into(),
571 name: name.into(),
572 }
573 }
574
575 pub async fn run(
582 &self,
583 _ctx: &OperationContext,
584 ) -> Result<BranchCheckoutOutput, OperationError> {
585 let repo_path = self.repo_path.clone();
586 let name = self.name.clone();
587 blocking(move || {
588 let repo = Repository::open(&repo_path)?;
589 repo.find_branch(&name, BranchType::Local)?;
590 let refname = format!("refs/heads/{name}");
591 repo.set_head(&refname)?;
592 repo.checkout_head(Some(CheckoutBuilder::new().force()))?;
593 Ok(BranchCheckoutOutput { name })
594 })
595 .await
596 }
597}
598
599#[async_trait]
600impl Operation for BranchCheckout {
601 fn kind(&self) -> &str {
602 "git"
603 }
604 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
605 to_value(&self.run(ctx).await?)
606 }
607 fn input(&self) -> Option<Value> {
608 Some(serde_json::json!({ "repo_path": self.repo_path, "name": self.name }))
609 }
610}
611
612impl TypedOperation for BranchCheckout {
613 type Output = BranchCheckoutOutput;
614}