ironflow_ops_git/
worktree.rs1use 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 WorktreeAddOutput {
16 pub name: String,
17 pub path: PathBuf,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct WorktreeListOutput {
22 pub worktrees: Vec<String>,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct WorktreeValidateOutput {
27 pub name: String,
28 pub valid: bool,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct WorktreePruneOutput {
33 pub name: String,
34 pub pruned: bool,
35}
36
37pub struct WorktreeAdd {
49 repo_path: PathBuf,
50 name: String,
51 path: PathBuf,
52}
53
54impl WorktreeAdd {
55 pub fn new(
57 repo_path: impl Into<PathBuf>,
58 name: impl Into<String>,
59 path: impl Into<PathBuf>,
60 ) -> Self {
61 Self {
62 repo_path: repo_path.into(),
63 name: name.into(),
64 path: path.into(),
65 }
66 }
67
68 pub async fn run(&self, _ctx: &OperationContext) -> Result<WorktreeAddOutput, OperationError> {
70 let repo_path = self.repo_path.clone();
71 let name = self.name.clone();
72 let path = self.path.clone();
73 blocking(move || {
74 let repo = Repository::open(&repo_path)?;
75 repo.worktree(&name, &path, None)?;
76 Ok(WorktreeAddOutput { name, path })
77 })
78 .await
79 }
80}
81
82#[async_trait]
83impl Operation for WorktreeAdd {
84 fn kind(&self) -> &str {
85 "git"
86 }
87 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
88 to_value(&self.run(ctx).await?)
89 }
90 fn input(&self) -> Option<Value> {
91 Some(
92 serde_json::json!({ "repo_path": self.repo_path, "name": self.name, "path": self.path }),
93 )
94 }
95}
96
97impl TypedOperation for WorktreeAdd {
98 type Output = WorktreeAddOutput;
99}
100
101pub struct WorktreeList {
113 repo_path: PathBuf,
114}
115
116impl WorktreeList {
117 pub fn new(repo_path: impl Into<PathBuf>) -> Self {
119 Self {
120 repo_path: repo_path.into(),
121 }
122 }
123
124 pub async fn run(&self, _ctx: &OperationContext) -> Result<WorktreeListOutput, OperationError> {
126 let repo_path = self.repo_path.clone();
127 blocking(move || {
128 let repo = Repository::open(&repo_path)?;
129 let worktrees = repo.worktrees()?;
130 let list = worktrees
131 .iter()
132 .filter_map(|w| w.map(String::from))
133 .collect();
134 Ok(WorktreeListOutput { worktrees: list })
135 })
136 .await
137 }
138}
139
140#[async_trait]
141impl Operation for WorktreeList {
142 fn kind(&self) -> &str {
143 "git"
144 }
145 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
146 to_value(&self.run(ctx).await?)
147 }
148 fn input(&self) -> Option<Value> {
149 Some(serde_json::json!({ "repo_path": self.repo_path }))
150 }
151}
152
153impl TypedOperation for WorktreeList {
154 type Output = WorktreeListOutput;
155}
156
157pub struct WorktreeValidate {
169 repo_path: PathBuf,
170 name: String,
171}
172
173impl WorktreeValidate {
174 pub fn new(repo_path: impl Into<PathBuf>, name: impl Into<String>) -> Self {
176 Self {
177 repo_path: repo_path.into(),
178 name: name.into(),
179 }
180 }
181
182 pub async fn run(
184 &self,
185 _ctx: &OperationContext,
186 ) -> Result<WorktreeValidateOutput, OperationError> {
187 let repo_path = self.repo_path.clone();
188 let name = self.name.clone();
189 blocking(move || {
190 let repo = Repository::open(&repo_path)?;
191 let wt = repo.find_worktree(&name)?;
192 let valid = wt.validate().is_ok();
193 Ok(WorktreeValidateOutput { name, valid })
194 })
195 .await
196 }
197}
198
199#[async_trait]
200impl Operation for WorktreeValidate {
201 fn kind(&self) -> &str {
202 "git"
203 }
204 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
205 to_value(&self.run(ctx).await?)
206 }
207 fn input(&self) -> Option<Value> {
208 Some(serde_json::json!({ "repo_path": self.repo_path, "name": self.name }))
209 }
210}
211
212impl TypedOperation for WorktreeValidate {
213 type Output = WorktreeValidateOutput;
214}
215
216pub struct WorktreePrune {
228 repo_path: PathBuf,
229 name: String,
230}
231
232impl WorktreePrune {
233 pub fn new(repo_path: impl Into<PathBuf>, name: impl Into<String>) -> Self {
235 Self {
236 repo_path: repo_path.into(),
237 name: name.into(),
238 }
239 }
240
241 pub async fn run(
243 &self,
244 _ctx: &OperationContext,
245 ) -> Result<WorktreePruneOutput, OperationError> {
246 let repo_path = self.repo_path.clone();
247 let name = self.name.clone();
248 blocking(move || {
249 let repo = Repository::open(&repo_path)?;
250 let wt = repo.find_worktree(&name)?;
251 wt.prune(None)?;
252 Ok(WorktreePruneOutput { name, pruned: true })
253 })
254 .await
255 }
256}
257
258#[async_trait]
259impl Operation for WorktreePrune {
260 fn kind(&self) -> &str {
261 "git"
262 }
263 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
264 to_value(&self.run(ctx).await?)
265 }
266 fn input(&self) -> Option<Value> {
267 Some(serde_json::json!({ "repo_path": self.repo_path, "name": self.name }))
268 }
269}
270
271impl TypedOperation for WorktreePrune {
272 type Output = WorktreePruneOutput;
273}
274
275#[cfg(test)]
276mod tests {
277 use std::fs;
278
279 use ironflow_core::operation::Operation;
280
281 use super::*;
282 use crate::test_helpers::{ctx, init_repo};
283
284 #[tokio::test]
285 async fn add_and_list() {
286 let tmp = tempfile::tempdir().unwrap();
287 init_repo(tmp.path());
288 let wt_path = tmp.path().join("wt-test");
289 let result = WorktreeAdd::new(tmp.path(), "wt-test", &wt_path)
290 .run(&ctx())
291 .await
292 .unwrap();
293 assert_eq!(result.name, "wt-test");
294 let list = WorktreeList::new(tmp.path()).run(&ctx()).await.unwrap();
295 assert!(list.worktrees.contains(&"wt-test".to_string()));
296 }
297
298 #[tokio::test]
299 async fn validate_existing_worktree() {
300 let tmp = tempfile::tempdir().unwrap();
301 init_repo(tmp.path());
302 let wt_path = tmp.path().join("wt-val");
303 WorktreeAdd::new(tmp.path(), "wt-val", &wt_path)
304 .run(&ctx())
305 .await
306 .unwrap();
307 let result = WorktreeValidate::new(tmp.path(), "wt-val")
308 .run(&ctx())
309 .await
310 .unwrap();
311 assert!(result.valid);
312 }
313
314 #[tokio::test]
315 async fn prune_worktree() {
316 let tmp = tempfile::tempdir().unwrap();
317 init_repo(tmp.path());
318 let wt_path = tmp.path().join("wt-prune");
319 WorktreeAdd::new(tmp.path(), "wt-prune", &wt_path)
320 .run(&ctx())
321 .await
322 .unwrap();
323 fs::remove_dir_all(&wt_path).unwrap();
324 let result = WorktreePrune::new(tmp.path(), "wt-prune")
325 .run(&ctx())
326 .await
327 .unwrap();
328 assert!(result.pruned);
329 }
330
331 #[tokio::test]
332 async fn list_empty_worktrees() {
333 let tmp = tempfile::tempdir().unwrap();
334 init_repo(tmp.path());
335 let result = WorktreeList::new(tmp.path()).run(&ctx()).await.unwrap();
336 assert!(result.worktrees.is_empty());
337 }
338
339 #[tokio::test]
340 async fn execute_serializes_correctly() {
341 let tmp = tempfile::tempdir().unwrap();
342 init_repo(tmp.path());
343 let value = WorktreeList::new(tmp.path()).execute(&ctx()).await.unwrap();
344 assert!(value["worktrees"].is_array());
345 }
346}