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