1use std::path::PathBuf;
4
5use async_trait::async_trait;
6use git2::{ObjectType, Oid, 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
14fn object_type_label(kind: Option<ObjectType>) -> &'static str {
15 match kind {
16 Some(ObjectType::Commit) => "commit",
17 Some(ObjectType::Tree) => "tree",
18 Some(ObjectType::Blob) => "blob",
19 Some(ObjectType::Tag) => "tag",
20 _ => "unknown",
21 }
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct BlobCreateOutput {
26 pub oid: String,
27 pub size: usize,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct TreeEntryOutput {
33 pub name: String,
34 pub oid: String,
35 pub kind: String,
36 pub filemode: i32,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct TreeLookupOutput {
41 pub oid: String,
42 pub entries: Vec<TreeEntryOutput>,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct FindObjectOutput {
47 pub oid: String,
48 #[serde(rename = "type")]
49 pub object_type: String,
50}
51
52pub struct BlobCreate {
64 repo_path: PathBuf,
65 content: Vec<u8>,
66}
67
68impl BlobCreate {
69 pub fn new(repo_path: impl Into<PathBuf>, content: Vec<u8>) -> Self {
71 Self {
72 repo_path: repo_path.into(),
73 content,
74 }
75 }
76
77 pub async fn run(&self, _ctx: &OperationContext) -> Result<BlobCreateOutput, OperationError> {
79 let repo_path = self.repo_path.clone();
80 let content = self.content.clone();
81 blocking(move || {
82 let repo = Repository::open(&repo_path)?;
83 let oid = repo.blob(&content)?;
84 Ok(BlobCreateOutput {
85 oid: oid.to_string(),
86 size: content.len(),
87 })
88 })
89 .await
90 }
91}
92
93#[async_trait]
94impl Operation for BlobCreate {
95 fn kind(&self) -> &str {
96 "git"
97 }
98 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
99 to_value(&self.run(ctx).await?)
100 }
101 fn input(&self) -> Option<Value> {
102 Some(serde_json::json!({ "repo_path": self.repo_path, "size": self.content.len() }))
103 }
104}
105
106impl TypedOperation for BlobCreate {
107 type Output = BlobCreateOutput;
108}
109
110pub struct TreeLookup {
122 repo_path: PathBuf,
123 oid: String,
124}
125
126impl TreeLookup {
127 pub fn new(repo_path: impl Into<PathBuf>, oid: impl Into<String>) -> Self {
129 Self {
130 repo_path: repo_path.into(),
131 oid: oid.into(),
132 }
133 }
134
135 pub async fn run(&self, _ctx: &OperationContext) -> Result<TreeLookupOutput, OperationError> {
137 let repo_path = self.repo_path.clone();
138 let oid_str = self.oid.clone();
139 blocking(move || {
140 let repo = Repository::open(&repo_path)?;
141 let oid = Oid::from_str(&oid_str)?;
142 let tree = repo.find_tree(oid)?;
143 let entries = tree
144 .iter()
145 .map(|entry| TreeEntryOutput {
146 name: entry.name().unwrap_or("").to_string(),
147 oid: entry.id().to_string(),
148 kind: object_type_label(entry.kind()).to_string(),
149 filemode: entry.filemode(),
150 })
151 .collect();
152 Ok(TreeLookupOutput {
153 oid: oid_str,
154 entries,
155 })
156 })
157 .await
158 }
159}
160
161#[async_trait]
162impl Operation for TreeLookup {
163 fn kind(&self) -> &str {
164 "git"
165 }
166 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
167 to_value(&self.run(ctx).await?)
168 }
169 fn input(&self) -> Option<Value> {
170 Some(serde_json::json!({ "repo_path": self.repo_path, "oid": self.oid }))
171 }
172}
173
174impl TypedOperation for TreeLookup {
175 type Output = TreeLookupOutput;
176}
177
178pub struct FindObject {
190 repo_path: PathBuf,
191 oid: String,
192}
193
194impl FindObject {
195 pub fn new(repo_path: impl Into<PathBuf>, oid: impl Into<String>) -> Self {
197 Self {
198 repo_path: repo_path.into(),
199 oid: oid.into(),
200 }
201 }
202
203 pub async fn run(&self, _ctx: &OperationContext) -> Result<FindObjectOutput, OperationError> {
205 let repo_path = self.repo_path.clone();
206 let oid_str = self.oid.clone();
207 blocking(move || {
208 let repo = Repository::open(&repo_path)?;
209 let oid = Oid::from_str(&oid_str)?;
210 let obj = repo.find_object(oid, None)?;
211 Ok(FindObjectOutput {
212 oid: oid_str,
213 object_type: object_type_label(obj.kind()).to_string(),
214 })
215 })
216 .await
217 }
218}
219
220#[async_trait]
221impl Operation for FindObject {
222 fn kind(&self) -> &str {
223 "git"
224 }
225 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
226 to_value(&self.run(ctx).await?)
227 }
228 fn input(&self) -> Option<Value> {
229 Some(serde_json::json!({ "repo_path": self.repo_path, "oid": self.oid }))
230 }
231}
232
233impl TypedOperation for FindObject {
234 type Output = FindObjectOutput;
235}
236
237#[cfg(test)]
238mod tests {
239 use git2::Repository;
240 use ironflow_core::operation::Operation;
241
242 use super::*;
243 use crate::test_helpers::{ctx, init_repo};
244
245 #[tokio::test]
246 async fn blob_create_returns_oid_and_size() {
247 let tmp = tempfile::tempdir().unwrap();
248 init_repo(tmp.path());
249 let result = BlobCreate::new(tmp.path(), b"hello".to_vec())
250 .run(&ctx())
251 .await
252 .unwrap();
253 assert!(!result.oid.is_empty());
254 assert_eq!(result.size, 5);
255 }
256
257 #[tokio::test]
258 async fn blob_create_empty() {
259 let tmp = tempfile::tempdir().unwrap();
260 init_repo(tmp.path());
261 let result = BlobCreate::new(tmp.path(), vec![])
262 .run(&ctx())
263 .await
264 .unwrap();
265 assert_eq!(result.size, 0);
266 }
267
268 #[tokio::test]
269 async fn tree_lookup_lists_entries() {
270 let tmp = tempfile::tempdir().unwrap();
271 init_repo(tmp.path());
272 let repo = Repository::open(tmp.path()).unwrap();
273 let head = repo.head().unwrap().peel_to_commit().unwrap();
274 let tree_oid = head.tree().unwrap().id().to_string();
275 let result = TreeLookup::new(tmp.path(), &tree_oid)
276 .run(&ctx())
277 .await
278 .unwrap();
279 assert_eq!(result.oid, tree_oid);
280 assert!(result.entries.iter().any(|e| e.name == "file.txt"));
281 }
282
283 #[tokio::test]
284 async fn find_object_returns_type() {
285 let tmp = tempfile::tempdir().unwrap();
286 init_repo(tmp.path());
287 let repo = Repository::open(tmp.path()).unwrap();
288 let commit_oid = repo
289 .head()
290 .unwrap()
291 .peel_to_commit()
292 .unwrap()
293 .id()
294 .to_string();
295 let result = FindObject::new(tmp.path(), &commit_oid)
296 .run(&ctx())
297 .await
298 .unwrap();
299 assert_eq!(result.object_type, "commit");
300 }
301
302 #[tokio::test]
303 async fn find_object_invalid_oid_fails() {
304 let tmp = tempfile::tempdir().unwrap();
305 init_repo(tmp.path());
306 assert!(
307 FindObject::new(tmp.path(), "0000000000000000000000000000000000000000")
308 .run(&ctx())
309 .await
310 .is_err()
311 );
312 }
313
314 #[tokio::test]
315 async fn execute_serializes_correctly() {
316 let tmp = tempfile::tempdir().unwrap();
317 init_repo(tmp.path());
318 let value = BlobCreate::new(tmp.path(), b"data".to_vec())
319 .execute(&ctx())
320 .await
321 .unwrap();
322 assert_eq!(value["size"], 4);
323 assert!(value["oid"].is_string());
324 }
325}