1use std::path::{Path, PathBuf};
7
8use async_trait::async_trait;
9use git2::{FetchOptions, Repository, SubmoduleUpdateOptions};
10use ironflow_core::error::OperationError;
11use ironflow_core::operation::{Operation, OperationContext, TypedOperation};
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14
15use crate::helpers::{
16 GitAuth, auth_builders, blocking, blocking_authenticated, credentials_callbacks, redact_url,
17 to_value,
18};
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct SubmoduleAddOutput {
22 pub url: String,
23 pub path: String,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct SubmoduleInitOutput {
28 pub name: String,
29 pub initialized: bool,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct SubmoduleUpdateOutput {
34 pub name: String,
35 pub updated: bool,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct SubmoduleLookupOutput {
40 pub name: String,
41 pub url: String,
42 pub path: String,
43 pub head_id: Option<String>,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct SubmoduleEntry {
49 pub name: String,
50 pub url: String,
51 pub path: String,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct SubmoduleListOutput {
56 pub submodules: Vec<SubmoduleEntry>,
57}
58
59pub struct SubmoduleAdd {
71 repo_path: PathBuf,
72 url: String,
73 path: String,
74}
75
76impl SubmoduleAdd {
77 pub fn new(
79 repo_path: impl Into<PathBuf>,
80 url: impl Into<String>,
81 path: impl Into<String>,
82 ) -> Self {
83 Self {
84 repo_path: repo_path.into(),
85 url: url.into(),
86 path: path.into(),
87 }
88 }
89
90 pub async fn run(&self, _ctx: &OperationContext) -> Result<SubmoduleAddOutput, OperationError> {
92 let repo_path = self.repo_path.clone();
93 let url = self.url.clone();
94 let path = self.path.clone();
95 blocking(move || {
96 let repo = Repository::open(&repo_path)?;
97 repo.submodule(&url, Path::new(&path), true)?;
98 Ok(SubmoduleAddOutput {
99 url: redact_url(&url),
100 path,
101 })
102 })
103 .await
104 }
105}
106
107#[async_trait]
108impl Operation for SubmoduleAdd {
109 fn kind(&self) -> &str {
110 "git"
111 }
112 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
113 to_value(&self.run(ctx).await?)
114 }
115 fn input(&self) -> Option<Value> {
116 Some(
117 serde_json::json!({ "repo_path": self.repo_path, "url": redact_url(&self.url), "path": self.path }),
118 )
119 }
120}
121
122impl TypedOperation for SubmoduleAdd {
123 type Output = SubmoduleAddOutput;
124}
125
126pub struct SubmoduleInit {
138 repo_path: PathBuf,
139 name: String,
140}
141
142impl SubmoduleInit {
143 pub fn new(repo_path: impl Into<PathBuf>, name: impl Into<String>) -> Self {
145 Self {
146 repo_path: repo_path.into(),
147 name: name.into(),
148 }
149 }
150
151 pub async fn run(
153 &self,
154 _ctx: &OperationContext,
155 ) -> Result<SubmoduleInitOutput, OperationError> {
156 let repo_path = self.repo_path.clone();
157 let name = self.name.clone();
158 blocking(move || {
159 let repo = Repository::open(&repo_path)?;
160 let mut sub = repo.find_submodule(&name)?;
161 sub.init(false)?;
162 Ok(SubmoduleInitOutput {
163 name,
164 initialized: true,
165 })
166 })
167 .await
168 }
169}
170
171#[async_trait]
172impl Operation for SubmoduleInit {
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(serde_json::json!({ "repo_path": self.repo_path, "name": self.name }))
181 }
182}
183
184impl TypedOperation for SubmoduleInit {
185 type Output = SubmoduleInitOutput;
186}
187
188pub struct SubmoduleUpdate {
200 repo_path: PathBuf,
201 name: String,
202 auth: GitAuth,
203}
204
205impl SubmoduleUpdate {
206 pub fn new(repo_path: impl Into<PathBuf>, name: impl Into<String>) -> Self {
208 Self {
209 repo_path: repo_path.into(),
210 name: name.into(),
211 auth: GitAuth::default(),
212 }
213 }
214
215 pub async fn run(
227 &self,
228 ctx: &OperationContext,
229 ) -> Result<SubmoduleUpdateOutput, OperationError> {
230 let repo_path = self.repo_path.clone();
231 let name = self.name.clone();
232 blocking_authenticated(ctx, &self.auth, move |creds| {
233 let repo = Repository::open(&repo_path)?;
234 let mut sub = repo.find_submodule(&name)?;
235 let mut fetch_opts = FetchOptions::new();
236 fetch_opts.remote_callbacks(credentials_callbacks(creds));
237 let mut update_opts = SubmoduleUpdateOptions::new();
238 update_opts.fetch(fetch_opts);
239 sub.update(true, Some(&mut update_opts))?;
240 Ok(SubmoduleUpdateOutput {
241 name,
242 updated: true,
243 })
244 })
245 .await
246 }
247}
248
249#[async_trait]
250impl Operation for SubmoduleUpdate {
251 fn kind(&self) -> &str {
252 "git"
253 }
254 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
255 to_value(&self.run(ctx).await?)
256 }
257 fn input(&self) -> Option<Value> {
258 Some(serde_json::json!({ "repo_path": self.repo_path, "name": self.name }))
259 }
260}
261
262impl TypedOperation for SubmoduleUpdate {
263 type Output = SubmoduleUpdateOutput;
264}
265
266auth_builders!(
267 SubmoduleUpdate,
268 "submodule",
269 "\"/path/to/repo\", \"vendor/sub\""
270);
271
272pub struct SubmoduleLookup {
284 repo_path: PathBuf,
285 name: String,
286}
287
288impl SubmoduleLookup {
289 pub fn new(repo_path: impl Into<PathBuf>, name: impl Into<String>) -> Self {
291 Self {
292 repo_path: repo_path.into(),
293 name: name.into(),
294 }
295 }
296
297 pub async fn run(
299 &self,
300 _ctx: &OperationContext,
301 ) -> Result<SubmoduleLookupOutput, OperationError> {
302 let repo_path = self.repo_path.clone();
303 let name = self.name.clone();
304 blocking(move || {
305 let repo = Repository::open(&repo_path)?;
306 let sub = repo.find_submodule(&name)?;
307 Ok(SubmoduleLookupOutput {
308 name: sub.name().unwrap_or("").to_string(),
309 url: redact_url(sub.url().unwrap_or("")),
310 path: sub.path().to_string_lossy().into_owned(),
311 head_id: sub.head_id().map(|o| o.to_string()),
312 })
313 })
314 .await
315 }
316}
317
318#[async_trait]
319impl Operation for SubmoduleLookup {
320 fn kind(&self) -> &str {
321 "git"
322 }
323 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
324 to_value(&self.run(ctx).await?)
325 }
326 fn input(&self) -> Option<Value> {
327 Some(serde_json::json!({ "repo_path": self.repo_path, "name": self.name }))
328 }
329}
330
331impl TypedOperation for SubmoduleLookup {
332 type Output = SubmoduleLookupOutput;
333}
334
335pub struct SubmoduleList {
347 repo_path: PathBuf,
348}
349
350impl SubmoduleList {
351 pub fn new(repo_path: impl Into<PathBuf>) -> Self {
353 Self {
354 repo_path: repo_path.into(),
355 }
356 }
357
358 pub async fn run(
360 &self,
361 _ctx: &OperationContext,
362 ) -> Result<SubmoduleListOutput, OperationError> {
363 let repo_path = self.repo_path.clone();
364 blocking(move || {
365 let repo = Repository::open(&repo_path)?;
366 let subs = repo.submodules()?;
367 let list = subs
368 .iter()
369 .map(|s| SubmoduleEntry {
370 name: s.name().unwrap_or("").to_string(),
371 url: redact_url(s.url().unwrap_or("")),
372 path: s.path().to_string_lossy().into_owned(),
373 })
374 .collect();
375 Ok(SubmoduleListOutput { submodules: list })
376 })
377 .await
378 }
379}
380
381#[async_trait]
382impl Operation for SubmoduleList {
383 fn kind(&self) -> &str {
384 "git"
385 }
386 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
387 to_value(&self.run(ctx).await?)
388 }
389 fn input(&self) -> Option<Value> {
390 Some(serde_json::json!({ "repo_path": self.repo_path }))
391 }
392}
393
394impl TypedOperation for SubmoduleList {
395 type Output = SubmoduleListOutput;
396}