1use std::path::{Path, PathBuf};
4
5use async_trait::async_trait;
6use git2::{IndexAddOption, 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 IndexPathOutput {
16 pub path: String,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct IndexPathspecsOutput {
21 pub pathspecs: Vec<String>,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct IndexUpdateAllOutput {
26 pub updated: bool,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct IndexWriteTreeOutput {
31 pub tree_oid: String,
32}
33
34pub struct IndexAdd {
46 repo_path: PathBuf,
47 path: String,
48}
49
50impl IndexAdd {
51 pub fn new(repo_path: impl Into<PathBuf>, path: impl Into<String>) -> Self {
53 Self {
54 repo_path: repo_path.into(),
55 path: path.into(),
56 }
57 }
58
59 pub async fn run(&self, _ctx: &OperationContext) -> Result<IndexPathOutput, OperationError> {
61 let repo_path = self.repo_path.clone();
62 let file_path = self.path.clone();
63 blocking(move || {
64 let repo = Repository::open(&repo_path)?;
65 let mut index = repo.index()?;
66 index.add_path(Path::new(&file_path))?;
67 index.write()?;
68 Ok(IndexPathOutput { path: file_path })
69 })
70 .await
71 }
72}
73
74#[async_trait]
75impl Operation for IndexAdd {
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, "path": self.path }))
84 }
85}
86
87impl TypedOperation for IndexAdd {
88 type Output = IndexPathOutput;
89}
90
91pub struct IndexAddAll {
105 repo_path: PathBuf,
106 pathspecs: Vec<String>,
107}
108
109impl IndexAddAll {
110 pub fn new(repo_path: impl Into<PathBuf>, pathspecs: Vec<impl Into<String>>) -> Self {
112 Self {
113 repo_path: repo_path.into(),
114 pathspecs: pathspecs.into_iter().map(Into::into).collect(),
115 }
116 }
117
118 pub async fn run(
120 &self,
121 _ctx: &OperationContext,
122 ) -> Result<IndexPathspecsOutput, OperationError> {
123 let repo_path = self.repo_path.clone();
124 let pathspecs = self.pathspecs.clone();
125 blocking(move || {
126 let repo = Repository::open(&repo_path)?;
127 let mut index = repo.index()?;
128 index.add_all(&pathspecs, IndexAddOption::DEFAULT, None)?;
129 index.write()?;
130 Ok(IndexPathspecsOutput { pathspecs })
131 })
132 .await
133 }
134}
135
136#[async_trait]
137impl Operation for IndexAddAll {
138 fn kind(&self) -> &str {
139 "git"
140 }
141 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
142 to_value(&self.run(ctx).await?)
143 }
144 fn input(&self) -> Option<Value> {
145 Some(serde_json::json!({ "repo_path": self.repo_path, "pathspecs": self.pathspecs }))
146 }
147}
148
149impl TypedOperation for IndexAddAll {
150 type Output = IndexPathspecsOutput;
151}
152
153pub struct IndexRemove {
165 repo_path: PathBuf,
166 path: String,
167}
168
169impl IndexRemove {
170 pub fn new(repo_path: impl Into<PathBuf>, path: impl Into<String>) -> Self {
172 Self {
173 repo_path: repo_path.into(),
174 path: path.into(),
175 }
176 }
177
178 pub async fn run(&self, _ctx: &OperationContext) -> Result<IndexPathOutput, OperationError> {
180 let repo_path = self.repo_path.clone();
181 let file_path = self.path.clone();
182 blocking(move || {
183 let repo = Repository::open(&repo_path)?;
184 let mut index = repo.index()?;
185 index.remove_path(Path::new(&file_path))?;
186 index.write()?;
187 Ok(IndexPathOutput { path: file_path })
188 })
189 .await
190 }
191}
192
193#[async_trait]
194impl Operation for IndexRemove {
195 fn kind(&self) -> &str {
196 "git"
197 }
198 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
199 to_value(&self.run(ctx).await?)
200 }
201 fn input(&self) -> Option<Value> {
202 Some(serde_json::json!({ "repo_path": self.repo_path, "path": self.path }))
203 }
204}
205
206impl TypedOperation for IndexRemove {
207 type Output = IndexPathOutput;
208}
209
210pub struct IndexRemoveAll {
222 repo_path: PathBuf,
223 pathspecs: Vec<String>,
224}
225
226impl IndexRemoveAll {
227 pub fn new(repo_path: impl Into<PathBuf>, pathspecs: Vec<impl Into<String>>) -> Self {
229 Self {
230 repo_path: repo_path.into(),
231 pathspecs: pathspecs.into_iter().map(Into::into).collect(),
232 }
233 }
234
235 pub async fn run(
237 &self,
238 _ctx: &OperationContext,
239 ) -> Result<IndexPathspecsOutput, OperationError> {
240 let repo_path = self.repo_path.clone();
241 let pathspecs = self.pathspecs.clone();
242 blocking(move || {
243 let repo = Repository::open(&repo_path)?;
244 let mut index = repo.index()?;
245 index.remove_all(&pathspecs, None)?;
246 index.write()?;
247 Ok(IndexPathspecsOutput { pathspecs })
248 })
249 .await
250 }
251}
252
253#[async_trait]
254impl Operation for IndexRemoveAll {
255 fn kind(&self) -> &str {
256 "git"
257 }
258 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
259 to_value(&self.run(ctx).await?)
260 }
261 fn input(&self) -> Option<Value> {
262 Some(serde_json::json!({ "repo_path": self.repo_path, "pathspecs": self.pathspecs }))
263 }
264}
265
266impl TypedOperation for IndexRemoveAll {
267 type Output = IndexPathspecsOutput;
268}
269
270pub struct IndexUpdateAll {
285 repo_path: PathBuf,
286}
287
288impl IndexUpdateAll {
289 pub fn new(repo_path: impl Into<PathBuf>) -> Self {
291 Self {
292 repo_path: repo_path.into(),
293 }
294 }
295
296 pub async fn run(
298 &self,
299 _ctx: &OperationContext,
300 ) -> Result<IndexUpdateAllOutput, OperationError> {
301 let repo_path = self.repo_path.clone();
302 blocking(move || {
303 let repo = Repository::open(&repo_path)?;
304 let mut index = repo.index()?;
305 index.update_all(["*"], None)?;
306 index.write()?;
307 Ok(IndexUpdateAllOutput { updated: true })
308 })
309 .await
310 }
311}
312
313#[async_trait]
314impl Operation for IndexUpdateAll {
315 fn kind(&self) -> &str {
316 "git"
317 }
318 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
319 to_value(&self.run(ctx).await?)
320 }
321 fn input(&self) -> Option<Value> {
322 Some(serde_json::json!({ "repo_path": self.repo_path }))
323 }
324}
325
326impl TypedOperation for IndexUpdateAll {
327 type Output = IndexUpdateAllOutput;
328}
329
330pub struct IndexWriteTree {
345 repo_path: PathBuf,
346}
347
348impl IndexWriteTree {
349 pub fn new(repo_path: impl Into<PathBuf>) -> Self {
351 Self {
352 repo_path: repo_path.into(),
353 }
354 }
355
356 pub async fn run(
358 &self,
359 _ctx: &OperationContext,
360 ) -> Result<IndexWriteTreeOutput, OperationError> {
361 let repo_path = self.repo_path.clone();
362 blocking(move || {
363 let repo = Repository::open(&repo_path)?;
364 let mut index = repo.index()?;
365 let oid = index.write_tree()?;
366 Ok(IndexWriteTreeOutput {
367 tree_oid: oid.to_string(),
368 })
369 })
370 .await
371 }
372}
373
374#[async_trait]
375impl Operation for IndexWriteTree {
376 fn kind(&self) -> &str {
377 "git"
378 }
379 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
380 to_value(&self.run(ctx).await?)
381 }
382 fn input(&self) -> Option<Value> {
383 Some(serde_json::json!({ "repo_path": self.repo_path }))
384 }
385}
386
387impl TypedOperation for IndexWriteTree {
388 type Output = IndexWriteTreeOutput;
389}
390
391#[cfg(test)]
392mod tests {
393 use std::fs;
394
395 use ironflow_core::operation::Operation;
396
397 use super::*;
398 use crate::test_helpers::{ctx, init_repo};
399
400 #[tokio::test]
401 async fn add_and_remove() {
402 let tmp = tempfile::tempdir().unwrap();
403 init_repo(tmp.path());
404 fs::write(tmp.path().join("new.txt"), "n").unwrap();
405 let result = IndexAdd::new(tmp.path(), "new.txt")
406 .run(&ctx())
407 .await
408 .unwrap();
409 assert_eq!(result.path, "new.txt");
410 let result = IndexRemove::new(tmp.path(), "new.txt")
411 .run(&ctx())
412 .await
413 .unwrap();
414 assert_eq!(result.path, "new.txt");
415 }
416
417 #[tokio::test]
418 async fn add_all_with_glob() {
419 let tmp = tempfile::tempdir().unwrap();
420 init_repo(tmp.path());
421 fs::write(tmp.path().join("a.rs"), "a").unwrap();
422 fs::write(tmp.path().join("b.rs"), "b").unwrap();
423 let result = IndexAddAll::new(tmp.path(), vec!["*.rs"])
424 .run(&ctx())
425 .await
426 .unwrap();
427 assert_eq!(result.pathspecs, vec!["*.rs"]);
428 }
429
430 #[tokio::test]
431 async fn update_all() {
432 let tmp = tempfile::tempdir().unwrap();
433 init_repo(tmp.path());
434 fs::write(tmp.path().join("file.txt"), "modified").unwrap();
435 let result = IndexUpdateAll::new(tmp.path()).run(&ctx()).await.unwrap();
436 assert!(result.updated);
437 }
438
439 #[tokio::test]
440 async fn write_tree_returns_oid() {
441 let tmp = tempfile::tempdir().unwrap();
442 init_repo(tmp.path());
443 let result = IndexWriteTree::new(tmp.path()).run(&ctx()).await.unwrap();
444 assert!(!result.tree_oid.is_empty());
445 }
446
447 #[tokio::test]
448 async fn add_nonexistent_file_fails() {
449 let tmp = tempfile::tempdir().unwrap();
450 init_repo(tmp.path());
451 assert!(
452 IndexAdd::new(tmp.path(), "nope.txt")
453 .run(&ctx())
454 .await
455 .is_err()
456 );
457 }
458
459 #[tokio::test]
460 async fn execute_serializes_correctly() {
461 let tmp = tempfile::tempdir().unwrap();
462 init_repo(tmp.path());
463 let value = IndexWriteTree::new(tmp.path())
464 .execute(&ctx())
465 .await
466 .unwrap();
467 assert!(value["tree_oid"].is_string());
468 }
469}