Skip to main content

ironflow_ops_git/
worktree.rs

1//! Worktree 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 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
37/// Add a new worktree.
38///
39/// # Examples
40///
41/// ```no_run
42/// use ironflow_ops_git::worktree::WorktreeAdd;
43/// use ironflow_core::operation::Operation;
44///
45/// let op = WorktreeAdd::new("/path/to/repo", "wt-feature", "/path/to/worktree");
46/// assert_eq!(op.kind(), "git");
47/// ```
48pub struct WorktreeAdd {
49    repo_path: PathBuf,
50    name: String,
51    path: PathBuf,
52}
53
54impl WorktreeAdd {
55    /// Create a new worktree-add operation.
56    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    /// Execute and return a typed result.
69    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
101/// List all worktrees.
102///
103/// # Examples
104///
105/// ```no_run
106/// use ironflow_ops_git::worktree::WorktreeList;
107/// use ironflow_core::operation::Operation;
108///
109/// let op = WorktreeList::new("/path/to/repo");
110/// assert_eq!(op.kind(), "git");
111/// ```
112pub struct WorktreeList {
113    repo_path: PathBuf,
114}
115
116impl WorktreeList {
117    /// Create a new worktree-list operation.
118    pub fn new(repo_path: impl Into<PathBuf>) -> Self {
119        Self {
120            repo_path: repo_path.into(),
121        }
122    }
123
124    /// Execute and return a typed result.
125    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
157/// Validate a worktree.
158///
159/// # Examples
160///
161/// ```no_run
162/// use ironflow_ops_git::worktree::WorktreeValidate;
163/// use ironflow_core::operation::Operation;
164///
165/// let op = WorktreeValidate::new("/path/to/repo", "wt-feature");
166/// assert_eq!(op.kind(), "git");
167/// ```
168pub struct WorktreeValidate {
169    repo_path: PathBuf,
170    name: String,
171}
172
173impl WorktreeValidate {
174    /// Create a new worktree-validate operation.
175    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    /// Execute and return a typed result.
183    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
216/// Prune a worktree.
217///
218/// # Examples
219///
220/// ```no_run
221/// use ironflow_ops_git::worktree::WorktreePrune;
222/// use ironflow_core::operation::Operation;
223///
224/// let op = WorktreePrune::new("/path/to/repo", "wt-feature");
225/// assert_eq!(op.kind(), "git");
226/// ```
227pub struct WorktreePrune {
228    repo_path: PathBuf,
229    name: String,
230}
231
232impl WorktreePrune {
233    /// Create a new worktree-prune operation.
234    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    /// Execute and return a typed result.
242    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}