Skip to main content

ironflow_ops_git/
submodule.rs

1//! Submodule operations.
2
3use std::path::{Path, PathBuf};
4
5use async_trait::async_trait;
6use git2::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 SubmoduleAddOutput {
16    pub url: String,
17    pub path: String,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct SubmoduleInitOutput {
22    pub name: String,
23    pub initialized: bool,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct SubmoduleUpdateOutput {
28    pub name: String,
29    pub updated: bool,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct SubmoduleLookupOutput {
34    pub name: String,
35    pub url: String,
36    pub path: String,
37    pub head_id: Option<String>,
38}
39
40/// A single submodule entry.
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct SubmoduleEntry {
43    pub name: String,
44    pub url: String,
45    pub path: String,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct SubmoduleListOutput {
50    pub submodules: Vec<SubmoduleEntry>,
51}
52
53/// Add a submodule.
54///
55/// # Examples
56///
57/// ```no_run
58/// use ironflow_ops_git::submodule::SubmoduleAdd;
59/// use ironflow_core::operation::Operation;
60///
61/// let op = SubmoduleAdd::new("/path/to/repo", "https://example.com/sub.git", "vendor/sub");
62/// assert_eq!(op.kind(), "git");
63/// ```
64pub struct SubmoduleAdd {
65    repo_path: PathBuf,
66    url: String,
67    path: String,
68}
69
70impl SubmoduleAdd {
71    /// Create a new submodule-add operation.
72    pub fn new(
73        repo_path: impl Into<PathBuf>,
74        url: impl Into<String>,
75        path: impl Into<String>,
76    ) -> Self {
77        Self {
78            repo_path: repo_path.into(),
79            url: url.into(),
80            path: path.into(),
81        }
82    }
83
84    /// Execute and return a typed result.
85    pub async fn run(&self, _ctx: &OperationContext) -> Result<SubmoduleAddOutput, OperationError> {
86        let repo_path = self.repo_path.clone();
87        let url = self.url.clone();
88        let path = self.path.clone();
89        blocking(move || {
90            let repo = Repository::open(&repo_path)?;
91            repo.submodule(&url, Path::new(&path), true)?;
92            Ok(SubmoduleAddOutput { url, path })
93        })
94        .await
95    }
96}
97
98#[async_trait]
99impl Operation for SubmoduleAdd {
100    fn kind(&self) -> &str {
101        "git"
102    }
103    async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
104        to_value(&self.run(ctx).await?)
105    }
106    fn input(&self) -> Option<Value> {
107        Some(serde_json::json!({ "repo_path": self.repo_path, "url": self.url, "path": self.path }))
108    }
109}
110
111impl TypedOperation for SubmoduleAdd {
112    type Output = SubmoduleAddOutput;
113}
114
115/// Initialize a submodule.
116///
117/// # Examples
118///
119/// ```no_run
120/// use ironflow_ops_git::submodule::SubmoduleInit;
121/// use ironflow_core::operation::Operation;
122///
123/// let op = SubmoduleInit::new("/path/to/repo", "vendor/sub");
124/// assert_eq!(op.kind(), "git");
125/// ```
126pub struct SubmoduleInit {
127    repo_path: PathBuf,
128    name: String,
129}
130
131impl SubmoduleInit {
132    /// Create a new submodule-init operation.
133    pub fn new(repo_path: impl Into<PathBuf>, name: impl Into<String>) -> Self {
134        Self {
135            repo_path: repo_path.into(),
136            name: name.into(),
137        }
138    }
139
140    /// Execute and return a typed result.
141    pub async fn run(
142        &self,
143        _ctx: &OperationContext,
144    ) -> Result<SubmoduleInitOutput, OperationError> {
145        let repo_path = self.repo_path.clone();
146        let name = self.name.clone();
147        blocking(move || {
148            let repo = Repository::open(&repo_path)?;
149            let mut sub = repo.find_submodule(&name)?;
150            sub.init(false)?;
151            Ok(SubmoduleInitOutput {
152                name,
153                initialized: true,
154            })
155        })
156        .await
157    }
158}
159
160#[async_trait]
161impl Operation for SubmoduleInit {
162    fn kind(&self) -> &str {
163        "git"
164    }
165    async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
166        to_value(&self.run(ctx).await?)
167    }
168    fn input(&self) -> Option<Value> {
169        Some(serde_json::json!({ "repo_path": self.repo_path, "name": self.name }))
170    }
171}
172
173impl TypedOperation for SubmoduleInit {
174    type Output = SubmoduleInitOutput;
175}
176
177/// Update a submodule (clone or fetch + checkout).
178///
179/// # Examples
180///
181/// ```no_run
182/// use ironflow_ops_git::submodule::SubmoduleUpdate;
183/// use ironflow_core::operation::Operation;
184///
185/// let op = SubmoduleUpdate::new("/path/to/repo", "vendor/sub");
186/// assert_eq!(op.kind(), "git");
187/// ```
188pub struct SubmoduleUpdate {
189    repo_path: PathBuf,
190    name: String,
191}
192
193impl SubmoduleUpdate {
194    /// Create a new submodule-update operation.
195    pub fn new(repo_path: impl Into<PathBuf>, name: impl Into<String>) -> Self {
196        Self {
197            repo_path: repo_path.into(),
198            name: name.into(),
199        }
200    }
201
202    /// Execute and return a typed result.
203    pub async fn run(
204        &self,
205        _ctx: &OperationContext,
206    ) -> Result<SubmoduleUpdateOutput, OperationError> {
207        let repo_path = self.repo_path.clone();
208        let name = self.name.clone();
209        blocking(move || {
210            let repo = Repository::open(&repo_path)?;
211            let mut sub = repo.find_submodule(&name)?;
212            sub.update(true, None)?;
213            Ok(SubmoduleUpdateOutput {
214                name,
215                updated: true,
216            })
217        })
218        .await
219    }
220}
221
222#[async_trait]
223impl Operation for SubmoduleUpdate {
224    fn kind(&self) -> &str {
225        "git"
226    }
227    async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
228        to_value(&self.run(ctx).await?)
229    }
230    fn input(&self) -> Option<Value> {
231        Some(serde_json::json!({ "repo_path": self.repo_path, "name": self.name }))
232    }
233}
234
235impl TypedOperation for SubmoduleUpdate {
236    type Output = SubmoduleUpdateOutput;
237}
238
239/// Look up a submodule by name.
240///
241/// # Examples
242///
243/// ```no_run
244/// use ironflow_ops_git::submodule::SubmoduleLookup;
245/// use ironflow_core::operation::Operation;
246///
247/// let op = SubmoduleLookup::new("/path/to/repo", "vendor/sub");
248/// assert_eq!(op.kind(), "git");
249/// ```
250pub struct SubmoduleLookup {
251    repo_path: PathBuf,
252    name: String,
253}
254
255impl SubmoduleLookup {
256    /// Create a new submodule-lookup operation.
257    pub fn new(repo_path: impl Into<PathBuf>, name: impl Into<String>) -> Self {
258        Self {
259            repo_path: repo_path.into(),
260            name: name.into(),
261        }
262    }
263
264    /// Execute and return a typed result.
265    pub async fn run(
266        &self,
267        _ctx: &OperationContext,
268    ) -> Result<SubmoduleLookupOutput, OperationError> {
269        let repo_path = self.repo_path.clone();
270        let name = self.name.clone();
271        blocking(move || {
272            let repo = Repository::open(&repo_path)?;
273            let sub = repo.find_submodule(&name)?;
274            Ok(SubmoduleLookupOutput {
275                name: sub.name().unwrap_or("").to_string(),
276                url: sub.url().unwrap_or("").to_string(),
277                path: sub.path().to_string_lossy().into_owned(),
278                head_id: sub.head_id().map(|o| o.to_string()),
279            })
280        })
281        .await
282    }
283}
284
285#[async_trait]
286impl Operation for SubmoduleLookup {
287    fn kind(&self) -> &str {
288        "git"
289    }
290    async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
291        to_value(&self.run(ctx).await?)
292    }
293    fn input(&self) -> Option<Value> {
294        Some(serde_json::json!({ "repo_path": self.repo_path, "name": self.name }))
295    }
296}
297
298impl TypedOperation for SubmoduleLookup {
299    type Output = SubmoduleLookupOutput;
300}
301
302/// List all submodules.
303///
304/// # Examples
305///
306/// ```no_run
307/// use ironflow_ops_git::submodule::SubmoduleList;
308/// use ironflow_core::operation::Operation;
309///
310/// let op = SubmoduleList::new("/path/to/repo");
311/// assert_eq!(op.kind(), "git");
312/// ```
313pub struct SubmoduleList {
314    repo_path: PathBuf,
315}
316
317impl SubmoduleList {
318    /// Create a new submodule-list operation.
319    pub fn new(repo_path: impl Into<PathBuf>) -> Self {
320        Self {
321            repo_path: repo_path.into(),
322        }
323    }
324
325    /// Execute and return a typed result.
326    pub async fn run(
327        &self,
328        _ctx: &OperationContext,
329    ) -> Result<SubmoduleListOutput, OperationError> {
330        let repo_path = self.repo_path.clone();
331        blocking(move || {
332            let repo = Repository::open(&repo_path)?;
333            let subs = repo.submodules()?;
334            let list = subs
335                .iter()
336                .map(|s| SubmoduleEntry {
337                    name: s.name().unwrap_or("").to_string(),
338                    url: s.url().unwrap_or("").to_string(),
339                    path: s.path().to_string_lossy().into_owned(),
340                })
341                .collect();
342            Ok(SubmoduleListOutput { submodules: list })
343        })
344        .await
345    }
346}
347
348#[async_trait]
349impl Operation for SubmoduleList {
350    fn kind(&self) -> &str {
351        "git"
352    }
353    async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
354        to_value(&self.run(ctx).await?)
355    }
356    fn input(&self) -> Option<Value> {
357        Some(serde_json::json!({ "repo_path": self.repo_path }))
358    }
359}
360
361impl TypedOperation for SubmoduleList {
362    type Output = SubmoduleListOutput;
363}