1use 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#[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
44pub struct ConfigGet {
56 repo_path: PathBuf,
57 key: String,
58}
59
60impl ConfigGet {
61 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 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
100pub struct ConfigSet {
112 repo_path: PathBuf,
113 key: String,
114 value: String,
115}
116
117impl ConfigSet {
118 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 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
165pub struct ConfigDelete {
177 repo_path: PathBuf,
178 key: String,
179}
180
181impl ConfigDelete {
182 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 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
221pub struct ConfigList {
233 repo_path: PathBuf,
234}
235
236impl ConfigList {
237 pub fn new(repo_path: impl Into<PathBuf>) -> Self {
239 Self {
240 repo_path: repo_path.into(),
241 }
242 }
243
244 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}