Skip to main content

ironflow_ops_git/
config.rs

1//! Git config operations.
2
3use std::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 ConfigGetOutput {
16    pub key: String,
17    pub value: String,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct ConfigSetOutput {
22    pub key: String,
23    pub value: String,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct ConfigDeleteOutput {
28    pub key: String,
29    pub deleted: bool,
30}
31
32/// A single config entry.
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct ConfigEntry {
35    pub name: String,
36    pub value: String,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct ConfigListOutput {
41    pub entries: Vec<ConfigEntry>,
42}
43
44/// Get a config value.
45///
46/// # Examples
47///
48/// ```no_run
49/// use ironflow_ops_git::config::ConfigGet;
50/// use ironflow_core::operation::Operation;
51///
52/// let op = ConfigGet::new("/path/to/repo", "user.name");
53/// assert_eq!(op.kind(), "git");
54/// ```
55pub struct ConfigGet {
56    repo_path: PathBuf,
57    key: String,
58}
59
60impl ConfigGet {
61    /// Create a new config-get operation.
62    pub fn new(repo_path: impl Into<PathBuf>, key: impl Into<String>) -> Self {
63        Self {
64            repo_path: repo_path.into(),
65            key: key.into(),
66        }
67    }
68
69    /// Execute and return a typed result.
70    pub async fn run(&self, _ctx: &OperationContext) -> Result<ConfigGetOutput, OperationError> {
71        let repo_path = self.repo_path.clone();
72        let key = self.key.clone();
73        blocking(move || {
74            let repo = Repository::open(&repo_path)?;
75            let config = repo.config()?;
76            let value = config.get_string(&key)?;
77            Ok(ConfigGetOutput { key, value })
78        })
79        .await
80    }
81}
82
83#[async_trait]
84impl Operation for ConfigGet {
85    fn kind(&self) -> &str {
86        "git"
87    }
88    async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
89        to_value(&self.run(ctx).await?)
90    }
91    fn input(&self) -> Option<Value> {
92        Some(serde_json::json!({ "repo_path": self.repo_path, "key": self.key }))
93    }
94}
95
96impl TypedOperation for ConfigGet {
97    type Output = ConfigGetOutput;
98}
99
100/// Set a config value.
101///
102/// # Examples
103///
104/// ```no_run
105/// use ironflow_ops_git::config::ConfigSet;
106/// use ironflow_core::operation::Operation;
107///
108/// let op = ConfigSet::new("/path/to/repo", "user.name", "Alice");
109/// assert_eq!(op.kind(), "git");
110/// ```
111pub struct ConfigSet {
112    repo_path: PathBuf,
113    key: String,
114    value: String,
115}
116
117impl ConfigSet {
118    /// Create a new config-set operation.
119    pub fn new(
120        repo_path: impl Into<PathBuf>,
121        key: impl Into<String>,
122        value: impl Into<String>,
123    ) -> Self {
124        Self {
125            repo_path: repo_path.into(),
126            key: key.into(),
127            value: value.into(),
128        }
129    }
130
131    /// Execute and return a typed result.
132    pub async fn run(&self, _ctx: &OperationContext) -> Result<ConfigSetOutput, OperationError> {
133        let repo_path = self.repo_path.clone();
134        let key = self.key.clone();
135        let value = self.value.clone();
136        blocking(move || {
137            let repo = Repository::open(&repo_path)?;
138            let mut config = repo.config()?;
139            config.set_str(&key, &value)?;
140            Ok(ConfigSetOutput { key, value })
141        })
142        .await
143    }
144}
145
146#[async_trait]
147impl Operation for ConfigSet {
148    fn kind(&self) -> &str {
149        "git"
150    }
151    async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
152        to_value(&self.run(ctx).await?)
153    }
154    fn input(&self) -> Option<Value> {
155        Some(
156            serde_json::json!({ "repo_path": self.repo_path, "key": self.key, "value": self.value }),
157        )
158    }
159}
160
161impl TypedOperation for ConfigSet {
162    type Output = ConfigSetOutput;
163}
164
165/// Delete a config entry.
166///
167/// # Examples
168///
169/// ```no_run
170/// use ironflow_ops_git::config::ConfigDelete;
171/// use ironflow_core::operation::Operation;
172///
173/// let op = ConfigDelete::new("/path/to/repo", "user.name");
174/// assert_eq!(op.kind(), "git");
175/// ```
176pub struct ConfigDelete {
177    repo_path: PathBuf,
178    key: String,
179}
180
181impl ConfigDelete {
182    /// Create a new config-delete operation.
183    pub fn new(repo_path: impl Into<PathBuf>, key: impl Into<String>) -> Self {
184        Self {
185            repo_path: repo_path.into(),
186            key: key.into(),
187        }
188    }
189
190    /// Execute and return a typed result.
191    pub async fn run(&self, _ctx: &OperationContext) -> Result<ConfigDeleteOutput, OperationError> {
192        let repo_path = self.repo_path.clone();
193        let key = self.key.clone();
194        blocking(move || {
195            let repo = Repository::open(&repo_path)?;
196            let mut config = repo.config()?;
197            config.remove(&key)?;
198            Ok(ConfigDeleteOutput { key, deleted: true })
199        })
200        .await
201    }
202}
203
204#[async_trait]
205impl Operation for ConfigDelete {
206    fn kind(&self) -> &str {
207        "git"
208    }
209    async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
210        to_value(&self.run(ctx).await?)
211    }
212    fn input(&self) -> Option<Value> {
213        Some(serde_json::json!({ "repo_path": self.repo_path, "key": self.key }))
214    }
215}
216
217impl TypedOperation for ConfigDelete {
218    type Output = ConfigDeleteOutput;
219}
220
221/// List all config entries.
222///
223/// # Examples
224///
225/// ```no_run
226/// use ironflow_ops_git::config::ConfigList;
227/// use ironflow_core::operation::Operation;
228///
229/// let op = ConfigList::new("/path/to/repo");
230/// assert_eq!(op.kind(), "git");
231/// ```
232pub struct ConfigList {
233    repo_path: PathBuf,
234}
235
236impl ConfigList {
237    /// Create a new config-list operation.
238    pub fn new(repo_path: impl Into<PathBuf>) -> Self {
239        Self {
240            repo_path: repo_path.into(),
241        }
242    }
243
244    /// Execute and return a typed result.
245    pub async fn run(&self, _ctx: &OperationContext) -> Result<ConfigListOutput, OperationError> {
246        let repo_path = self.repo_path.clone();
247        blocking(move || {
248            let repo = Repository::open(&repo_path)?;
249            let mut config = repo.config()?;
250            let snapshot = config.snapshot()?;
251            let mut entries = Vec::new();
252            let mut iter = snapshot.entries(None)?;
253            while let Some(entry) = iter.next() {
254                let entry = entry?;
255                if let (Some(name), Some(value)) = (entry.name(), entry.value()) {
256                    entries.push(ConfigEntry {
257                        name: name.to_string(),
258                        value: value.to_string(),
259                    });
260                }
261            }
262            Ok(ConfigListOutput { entries })
263        })
264        .await
265    }
266}
267
268#[async_trait]
269impl Operation for ConfigList {
270    fn kind(&self) -> &str {
271        "git"
272    }
273    async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
274        to_value(&self.run(ctx).await?)
275    }
276    fn input(&self) -> Option<Value> {
277        Some(serde_json::json!({ "repo_path": self.repo_path }))
278    }
279}
280
281impl TypedOperation for ConfigList {
282    type Output = ConfigListOutput;
283}
284
285#[cfg(test)]
286mod tests {
287    use git2::Repository;
288    use ironflow_core::operation::Operation;
289
290    use super::*;
291    use crate::test_helpers::ctx;
292
293    #[tokio::test]
294    async fn set_and_get() {
295        let tmp = tempfile::tempdir().unwrap();
296        Repository::init(tmp.path()).unwrap();
297        ConfigSet::new(tmp.path(), "user.name", "Alice")
298            .run(&ctx())
299            .await
300            .unwrap();
301        let result = ConfigGet::new(tmp.path(), "user.name")
302            .run(&ctx())
303            .await
304            .unwrap();
305        assert_eq!(result.key, "user.name");
306        assert_eq!(result.value, "Alice");
307    }
308
309    #[tokio::test]
310    async fn get_missing_key_fails() {
311        let tmp = tempfile::tempdir().unwrap();
312        Repository::init(tmp.path()).unwrap();
313        assert!(
314            ConfigGet::new(tmp.path(), "no.such.key")
315                .run(&ctx())
316                .await
317                .is_err()
318        );
319    }
320
321    #[tokio::test]
322    async fn delete_removes_key() {
323        let tmp = tempfile::tempdir().unwrap();
324        Repository::init(tmp.path()).unwrap();
325        ConfigSet::new(tmp.path(), "test.deletekey", "val")
326            .run(&ctx())
327            .await
328            .unwrap();
329        let result = ConfigDelete::new(tmp.path(), "test.deletekey")
330            .run(&ctx())
331            .await
332            .unwrap();
333        assert!(result.deleted);
334        assert!(
335            ConfigGet::new(tmp.path(), "test.deletekey")
336                .run(&ctx())
337                .await
338                .is_err()
339        );
340    }
341
342    #[tokio::test]
343    async fn list_includes_set_key() {
344        let tmp = tempfile::tempdir().unwrap();
345        Repository::init(tmp.path()).unwrap();
346        ConfigSet::new(tmp.path(), "test.key", "val")
347            .run(&ctx())
348            .await
349            .unwrap();
350        let result = ConfigList::new(tmp.path()).run(&ctx()).await.unwrap();
351        assert!(
352            result
353                .entries
354                .iter()
355                .any(|e| e.name == "test.key" && e.value == "val")
356        );
357    }
358
359    #[tokio::test]
360    async fn execute_serializes_correctly() {
361        let tmp = tempfile::tempdir().unwrap();
362        Repository::init(tmp.path()).unwrap();
363        ConfigSet::new(tmp.path(), "user.name", "Bob")
364            .run(&ctx())
365            .await
366            .unwrap();
367        let value = ConfigGet::new(tmp.path(), "user.name")
368            .execute(&ctx())
369            .await
370            .unwrap();
371        assert_eq!(value["value"], "Bob");
372    }
373}