1use std::path::{Path, PathBuf};
4
5use async_trait::async_trait;
6use git2::{Repository, RepositoryState};
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 repo_state_label(state: RepositoryState) -> &'static str {
15 match state {
16 RepositoryState::Clean => "clean",
17 RepositoryState::Merge => "merge",
18 RepositoryState::Revert | RepositoryState::RevertSequence => "revert",
19 RepositoryState::CherryPickSequence | RepositoryState::CherryPick => "cherrypick",
20 RepositoryState::Bisect => "bisect",
21 RepositoryState::Rebase
22 | RepositoryState::RebaseInteractive
23 | RepositoryState::RebaseMerge => "rebase",
24 RepositoryState::ApplyMailbox | RepositoryState::ApplyMailboxOrRebase => "apply-mailbox",
25 }
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct RepoInitOutput {
31 pub path: PathBuf,
33 pub bare: bool,
35}
36
37pub struct RepoInit {
52 path: PathBuf,
53 bare: bool,
54}
55
56impl RepoInit {
57 pub fn new(path: impl Into<PathBuf>, bare: bool) -> Self {
59 Self {
60 path: path.into(),
61 bare,
62 }
63 }
64
65 pub async fn run(&self, _ctx: &OperationContext) -> Result<RepoInitOutput, OperationError> {
67 let path = self.path.clone();
68 let bare = self.bare;
69 blocking(move || {
70 if bare {
71 Repository::init_bare(&path)?;
72 } else {
73 Repository::init(&path)?;
74 }
75 Ok(RepoInitOutput { path, bare })
76 })
77 .await
78 }
79}
80
81#[async_trait]
82impl Operation for RepoInit {
83 fn kind(&self) -> &str {
84 "git"
85 }
86
87 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
88 to_value(&self.run(ctx).await?)
89 }
90
91 fn input(&self) -> Option<Value> {
92 Some(serde_json::json!({ "path": self.path, "bare": self.bare }))
93 }
94}
95
96impl TypedOperation for RepoInit {
97 type Output = RepoInitOutput;
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct RepoOpenOutput {
103 pub path: PathBuf,
105 pub bare: bool,
107}
108
109pub struct RepoOpen {
123 path: PathBuf,
124}
125
126impl RepoOpen {
127 pub fn new(path: impl Into<PathBuf>) -> Self {
129 Self { path: path.into() }
130 }
131
132 pub async fn run(&self, _ctx: &OperationContext) -> Result<RepoOpenOutput, OperationError> {
134 let path = self.path.clone();
135 blocking(move || {
136 let repo = Repository::open(&path)?;
137 let is_bare = repo.is_bare();
138 let workdir = repo.workdir().map(Path::to_path_buf);
139 Ok(RepoOpenOutput {
140 path: workdir.unwrap_or(path),
141 bare: is_bare,
142 })
143 })
144 .await
145 }
146}
147
148#[async_trait]
149impl Operation for RepoOpen {
150 fn kind(&self) -> &str {
151 "git"
152 }
153
154 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
155 to_value(&self.run(ctx).await?)
156 }
157
158 fn input(&self) -> Option<Value> {
159 Some(serde_json::json!({ "path": self.path }))
160 }
161}
162
163impl TypedOperation for RepoOpen {
164 type Output = RepoOpenOutput;
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct RepoCloneOutput {
170 pub url: String,
172 pub path: PathBuf,
174}
175
176pub struct RepoClone {
188 url: String,
189 path: PathBuf,
190}
191
192impl RepoClone {
193 pub fn new(url: impl Into<String>, path: impl Into<PathBuf>) -> Self {
195 Self {
196 url: url.into(),
197 path: path.into(),
198 }
199 }
200
201 pub async fn run(&self, _ctx: &OperationContext) -> Result<RepoCloneOutput, OperationError> {
203 let url = self.url.clone();
204 let path = self.path.clone();
205 blocking(move || {
206 Repository::clone(&url, &path)?;
207 Ok(RepoCloneOutput { url, path })
208 })
209 .await
210 }
211}
212
213#[async_trait]
214impl Operation for RepoClone {
215 fn kind(&self) -> &str {
216 "git"
217 }
218
219 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
220 to_value(&self.run(ctx).await?)
221 }
222
223 fn input(&self) -> Option<Value> {
224 Some(serde_json::json!({ "url": self.url, "path": self.path }))
225 }
226}
227
228impl TypedOperation for RepoClone {
229 type Output = RepoCloneOutput;
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct RepoDiscoverOutput {
235 pub path: Option<PathBuf>,
237 pub bare: bool,
239}
240
241pub struct RepoDiscover {
255 start_path: PathBuf,
256}
257
258impl RepoDiscover {
259 pub fn new(start_path: impl Into<PathBuf>) -> Self {
261 Self {
262 start_path: start_path.into(),
263 }
264 }
265
266 pub async fn run(&self, _ctx: &OperationContext) -> Result<RepoDiscoverOutput, OperationError> {
268 let start = self.start_path.clone();
269 blocking(move || {
270 let repo = Repository::discover(&start)?;
271 let workdir = repo.workdir().map(Path::to_path_buf);
272 let is_bare = repo.is_bare();
273 Ok(RepoDiscoverOutput {
274 path: workdir,
275 bare: is_bare,
276 })
277 })
278 .await
279 }
280}
281
282#[async_trait]
283impl Operation for RepoDiscover {
284 fn kind(&self) -> &str {
285 "git"
286 }
287
288 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
289 to_value(&self.run(ctx).await?)
290 }
291
292 fn input(&self) -> Option<Value> {
293 Some(serde_json::json!({ "start_path": self.start_path }))
294 }
295}
296
297impl TypedOperation for RepoDiscover {
298 type Output = RepoDiscoverOutput;
299}
300
301#[derive(Debug, Clone, Serialize, Deserialize)]
303pub struct RepoStateOutput {
304 pub state: String,
306}
307
308pub struct RepoState {
322 repo_path: PathBuf,
323}
324
325impl RepoState {
326 pub fn new(repo_path: impl Into<PathBuf>) -> Self {
328 Self {
329 repo_path: repo_path.into(),
330 }
331 }
332
333 pub async fn run(&self, _ctx: &OperationContext) -> Result<RepoStateOutput, OperationError> {
335 let path = self.repo_path.clone();
336 blocking(move || {
337 let repo = Repository::open(&path)?;
338 let state = repo_state_label(repo.state()).to_string();
339 Ok(RepoStateOutput { state })
340 })
341 .await
342 }
343}
344
345#[async_trait]
346impl Operation for RepoState {
347 fn kind(&self) -> &str {
348 "git"
349 }
350
351 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
352 to_value(&self.run(ctx).await?)
353 }
354
355 fn input(&self) -> Option<Value> {
356 Some(serde_json::json!({ "repo_path": self.repo_path }))
357 }
358}
359
360impl TypedOperation for RepoState {
361 type Output = RepoStateOutput;
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367 use crate::test_helpers::ctx;
368
369 #[tokio::test]
370 async fn init_creates_repo() {
371 let tmp = tempfile::tempdir().unwrap();
372 let target = tmp.path().join("new-repo");
373 let op = RepoInit::new(&target, false);
374 let result = op.run(&ctx()).await.unwrap();
375 assert!(!result.bare);
376 assert!(target.join(".git").exists());
377 }
378
379 #[tokio::test]
380 async fn init_creates_bare_repo() {
381 let tmp = tempfile::tempdir().unwrap();
382 let target = tmp.path().join("bare-repo");
383 let op = RepoInit::new(&target, true);
384 let result = op.run(&ctx()).await.unwrap();
385 assert!(result.bare);
386 assert!(target.join("HEAD").exists());
387 }
388
389 #[tokio::test]
390 async fn clone_local() {
391 let tmp = tempfile::tempdir().unwrap();
392 let origin = tmp.path().join("origin");
393 Repository::init(&origin).unwrap();
394
395 let target = tmp.path().join("clone");
396 let url = origin.to_str().unwrap();
397 let op = RepoClone::new(url, &target);
398 let result = op.run(&ctx()).await.unwrap();
399 assert_eq!(result.path, target);
400 assert!(target.join(".git").exists());
401 }
402
403 #[tokio::test]
404 async fn discover_finds_repo() {
405 let tmp = tempfile::tempdir().unwrap();
406 Repository::init(tmp.path()).unwrap();
407 let subdir = tmp.path().join("a").join("b");
408 std::fs::create_dir_all(&subdir).unwrap();
409
410 let op = RepoDiscover::new(&subdir);
411 let result = op.run(&ctx()).await.unwrap();
412 assert!(!result.bare);
413 }
414
415 #[tokio::test]
416 async fn state_on_clean_repo() {
417 let tmp = tempfile::tempdir().unwrap();
418 Repository::init(tmp.path()).unwrap();
419
420 let op = RepoState::new(tmp.path());
421 let result = op.run(&ctx()).await.unwrap();
422 assert_eq!(result.state, "clean");
423 }
424}