1use std::path::PathBuf;
4
5use async_trait::async_trait;
6use git2::{Oid, ReferenceType, 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 RefCreateOutput {
16 pub name: String,
17 pub target: String,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct RefDeleteOutput {
22 pub name: String,
23 pub deleted: bool,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct RefRenameOutput {
28 pub old_name: String,
29 pub new_name: String,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct RefLookupOutput {
34 pub name: String,
35 pub target: Option<String>,
36 pub symbolic: bool,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct RefNameToIdOutput {
41 pub name: String,
42 pub oid: String,
43}
44
45pub struct RefCreate {
57 repo_path: PathBuf,
58 name: String,
59 target: String,
60 log_message: String,
61}
62
63impl RefCreate {
64 pub fn new(
66 repo_path: impl Into<PathBuf>,
67 name: impl Into<String>,
68 target: impl Into<String>,
69 log_message: impl Into<String>,
70 ) -> Self {
71 Self {
72 repo_path: repo_path.into(),
73 name: name.into(),
74 target: target.into(),
75 log_message: log_message.into(),
76 }
77 }
78
79 pub async fn run(&self, _ctx: &OperationContext) -> Result<RefCreateOutput, OperationError> {
81 let repo_path = self.repo_path.clone();
82 let name = self.name.clone();
83 let target = self.target.clone();
84 let msg = self.log_message.clone();
85 blocking(move || {
86 let repo = Repository::open(&repo_path)?;
87 let oid = Oid::from_str(&target)?;
88 repo.reference(&name, oid, false, &msg)?;
89 Ok(RefCreateOutput { name, target })
90 })
91 .await
92 }
93}
94
95#[async_trait]
96impl Operation for RefCreate {
97 fn kind(&self) -> &str {
98 "git"
99 }
100 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
101 to_value(&self.run(ctx).await?)
102 }
103 fn input(&self) -> Option<Value> {
104 Some(
105 serde_json::json!({ "repo_path": self.repo_path, "name": self.name, "target": self.target }),
106 )
107 }
108}
109
110impl TypedOperation for RefCreate {
111 type Output = RefCreateOutput;
112}
113
114pub struct RefDelete {
126 repo_path: PathBuf,
127 name: String,
128}
129
130impl RefDelete {
131 pub fn new(repo_path: impl Into<PathBuf>, name: impl Into<String>) -> Self {
133 Self {
134 repo_path: repo_path.into(),
135 name: name.into(),
136 }
137 }
138
139 pub async fn run(&self, _ctx: &OperationContext) -> Result<RefDeleteOutput, OperationError> {
141 let repo_path = self.repo_path.clone();
142 let name = self.name.clone();
143 blocking(move || {
144 let repo = Repository::open(&repo_path)?;
145 let mut reference = repo.find_reference(&name)?;
146 reference.delete()?;
147 Ok(RefDeleteOutput {
148 name,
149 deleted: true,
150 })
151 })
152 .await
153 }
154}
155
156#[async_trait]
157impl Operation for RefDelete {
158 fn kind(&self) -> &str {
159 "git"
160 }
161 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
162 to_value(&self.run(ctx).await?)
163 }
164 fn input(&self) -> Option<Value> {
165 Some(serde_json::json!({ "repo_path": self.repo_path, "name": self.name }))
166 }
167}
168
169impl TypedOperation for RefDelete {
170 type Output = RefDeleteOutput;
171}
172
173pub struct RefRename {
185 repo_path: PathBuf,
186 old_name: String,
187 new_name: String,
188 log_message: String,
189 force: bool,
190}
191
192impl RefRename {
193 pub fn new(
195 repo_path: impl Into<PathBuf>,
196 old_name: impl Into<String>,
197 new_name: impl Into<String>,
198 log_message: impl Into<String>,
199 force: bool,
200 ) -> Self {
201 Self {
202 repo_path: repo_path.into(),
203 old_name: old_name.into(),
204 new_name: new_name.into(),
205 log_message: log_message.into(),
206 force,
207 }
208 }
209
210 pub async fn run(&self, _ctx: &OperationContext) -> Result<RefRenameOutput, OperationError> {
212 let repo_path = self.repo_path.clone();
213 let old = self.old_name.clone();
214 let new = self.new_name.clone();
215 let msg = self.log_message.clone();
216 let force = self.force;
217 blocking(move || {
218 let repo = Repository::open(&repo_path)?;
219 let mut reference = repo.find_reference(&old)?;
220 reference.rename(&new, force, &msg)?;
221 Ok(RefRenameOutput {
222 old_name: old,
223 new_name: new,
224 })
225 })
226 .await
227 }
228}
229
230#[async_trait]
231impl Operation for RefRename {
232 fn kind(&self) -> &str {
233 "git"
234 }
235 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
236 to_value(&self.run(ctx).await?)
237 }
238 fn input(&self) -> Option<Value> {
239 Some(
240 serde_json::json!({ "repo_path": self.repo_path, "old_name": self.old_name, "new_name": self.new_name }),
241 )
242 }
243}
244
245impl TypedOperation for RefRename {
246 type Output = RefRenameOutput;
247}
248
249pub struct RefLookup {
261 repo_path: PathBuf,
262 name: String,
263}
264
265impl RefLookup {
266 pub fn new(repo_path: impl Into<PathBuf>, name: impl Into<String>) -> Self {
268 Self {
269 repo_path: repo_path.into(),
270 name: name.into(),
271 }
272 }
273
274 pub async fn run(&self, _ctx: &OperationContext) -> Result<RefLookupOutput, OperationError> {
276 let repo_path = self.repo_path.clone();
277 let name = self.name.clone();
278 blocking(move || {
279 let repo = Repository::open(&repo_path)?;
280 let reference = repo.find_reference(&name)?;
281 let target = reference.target().map(|o| o.to_string());
282 let is_symbolic = reference.kind() == Some(ReferenceType::Symbolic);
283 Ok(RefLookupOutput {
284 name,
285 target,
286 symbolic: is_symbolic,
287 })
288 })
289 .await
290 }
291}
292
293#[async_trait]
294impl Operation for RefLookup {
295 fn kind(&self) -> &str {
296 "git"
297 }
298 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
299 to_value(&self.run(ctx).await?)
300 }
301 fn input(&self) -> Option<Value> {
302 Some(serde_json::json!({ "repo_path": self.repo_path, "name": self.name }))
303 }
304}
305
306impl TypedOperation for RefLookup {
307 type Output = RefLookupOutput;
308}
309
310pub struct RefNameToId {
322 repo_path: PathBuf,
323 name: String,
324}
325
326impl RefNameToId {
327 pub fn new(repo_path: impl Into<PathBuf>, name: impl Into<String>) -> Self {
329 Self {
330 repo_path: repo_path.into(),
331 name: name.into(),
332 }
333 }
334
335 pub async fn run(&self, _ctx: &OperationContext) -> Result<RefNameToIdOutput, OperationError> {
337 let repo_path = self.repo_path.clone();
338 let name = self.name.clone();
339 blocking(move || {
340 let repo = Repository::open(&repo_path)?;
341 let oid = repo.refname_to_id(&name)?;
342 Ok(RefNameToIdOutput {
343 name,
344 oid: oid.to_string(),
345 })
346 })
347 .await
348 }
349}
350
351#[async_trait]
352impl Operation for RefNameToId {
353 fn kind(&self) -> &str {
354 "git"
355 }
356 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
357 to_value(&self.run(ctx).await?)
358 }
359 fn input(&self) -> Option<Value> {
360 Some(serde_json::json!({ "repo_path": self.repo_path, "name": self.name }))
361 }
362}
363
364impl TypedOperation for RefNameToId {
365 type Output = RefNameToIdOutput;
366}
367
368#[cfg(test)]
369mod tests {
370 use ironflow_core::operation::Operation;
371
372 use super::*;
373 use crate::test_helpers::{ctx, init_repo};
374
375 #[tokio::test]
376 async fn create_and_lookup() {
377 let tmp = tempfile::tempdir().unwrap();
378 let oid = init_repo(tmp.path()).to_string();
379 RefCreate::new(tmp.path(), "refs/heads/test-ref", &oid, "create")
380 .run(&ctx())
381 .await
382 .unwrap();
383 let result = RefLookup::new(tmp.path(), "refs/heads/test-ref")
384 .run(&ctx())
385 .await
386 .unwrap();
387 assert_eq!(result.name, "refs/heads/test-ref");
388 assert_eq!(result.target.as_deref(), Some(oid.as_str()));
389 assert!(!result.symbolic);
390 }
391
392 #[tokio::test]
393 async fn name_to_id() {
394 let tmp = tempfile::tempdir().unwrap();
395 let oid = init_repo(tmp.path()).to_string();
396 let result = RefNameToId::new(tmp.path(), "HEAD")
397 .run(&ctx())
398 .await
399 .unwrap();
400 assert_eq!(result.oid, oid);
401 }
402
403 #[tokio::test]
404 async fn rename_ref() {
405 let tmp = tempfile::tempdir().unwrap();
406 let oid = init_repo(tmp.path()).to_string();
407 RefCreate::new(tmp.path(), "refs/heads/old-ref", &oid, "c")
408 .run(&ctx())
409 .await
410 .unwrap();
411 let result = RefRename::new(
412 tmp.path(),
413 "refs/heads/old-ref",
414 "refs/heads/new-ref",
415 "rename",
416 false,
417 )
418 .run(&ctx())
419 .await
420 .unwrap();
421 assert_eq!(result.old_name, "refs/heads/old-ref");
422 assert_eq!(result.new_name, "refs/heads/new-ref");
423 assert!(
424 RefLookup::new(tmp.path(), "refs/heads/new-ref")
425 .run(&ctx())
426 .await
427 .is_ok()
428 );
429 }
430
431 #[tokio::test]
432 async fn delete_ref() {
433 let tmp = tempfile::tempdir().unwrap();
434 let oid = init_repo(tmp.path()).to_string();
435 RefCreate::new(tmp.path(), "refs/heads/to-delete", &oid, "c")
436 .run(&ctx())
437 .await
438 .unwrap();
439 let result = RefDelete::new(tmp.path(), "refs/heads/to-delete")
440 .run(&ctx())
441 .await
442 .unwrap();
443 assert!(result.deleted);
444 assert!(
445 RefLookup::new(tmp.path(), "refs/heads/to-delete")
446 .run(&ctx())
447 .await
448 .is_err()
449 );
450 }
451
452 #[tokio::test]
453 async fn lookup_missing_ref_fails() {
454 let tmp = tempfile::tempdir().unwrap();
455 init_repo(tmp.path());
456 assert!(
457 RefLookup::new(tmp.path(), "refs/heads/nope")
458 .run(&ctx())
459 .await
460 .is_err()
461 );
462 }
463
464 #[tokio::test]
465 async fn execute_serializes_correctly() {
466 let tmp = tempfile::tempdir().unwrap();
467 let oid = init_repo(tmp.path()).to_string();
468 let value = RefNameToId::new(tmp.path(), "HEAD")
469 .execute(&ctx())
470 .await
471 .unwrap();
472 assert_eq!(value["oid"], oid);
473 }
474}