Skip to main content

git_spawn/command/
worktree.rs

1//! `git worktree` — manage multiple working trees attached to the same repository.
2
3use crate::command::{CommandExecutor, CommandOutput, GitCommand};
4use crate::error::Result;
5use async_trait::async_trait;
6use std::path::PathBuf;
7
8/// Actions supported by `git worktree`.
9#[derive(Debug, Clone)]
10pub enum WorktreeAction {
11    /// `git worktree add [-b <branch>] [--detach] [--force] <path> [<commit-ish>]`.
12    Add {
13        /// Path for the new worktree.
14        path: PathBuf,
15        /// Commit-ish to check out.
16        commit_ish: Option<String>,
17        /// `-b <branch>` create a new branch.
18        new_branch: Option<String>,
19        /// `--detach`.
20        detach: bool,
21        /// `--force`.
22        force: bool,
23        /// `--track`.
24        track: bool,
25    },
26    /// `git worktree list [--porcelain]`.
27    List {
28        /// Emit porcelain format.
29        porcelain: bool,
30    },
31    /// `git worktree remove <path>`.
32    Remove {
33        /// Worktree path.
34        path: PathBuf,
35        /// `--force`.
36        force: bool,
37    },
38    /// `git worktree prune`.
39    Prune {
40        /// `-v` verbose.
41        verbose: bool,
42        /// `--dry-run`.
43        dry_run: bool,
44    },
45    /// `git worktree move <source> <destination>`.
46    Move {
47        /// Current worktree path.
48        source: PathBuf,
49        /// New path.
50        destination: PathBuf,
51    },
52    /// `git worktree lock <path> [--reason <s>]`.
53    Lock {
54        /// Worktree path.
55        path: PathBuf,
56        /// Optional reason.
57        reason: Option<String>,
58    },
59    /// `git worktree unlock <path>`.
60    Unlock {
61        /// Worktree path.
62        path: PathBuf,
63    },
64}
65
66/// Builder for `git worktree`.
67#[derive(Debug, Clone)]
68pub struct WorktreeCommand {
69    /// Shared executor.
70    pub executor: CommandExecutor,
71    /// Action.
72    pub action: WorktreeAction,
73}
74
75impl WorktreeCommand {
76    /// `worktree add`.
77    pub fn add(path: impl Into<PathBuf>) -> Self {
78        Self {
79            executor: CommandExecutor::default(),
80            action: WorktreeAction::Add {
81                path: path.into(),
82                commit_ish: None,
83                new_branch: None,
84                detach: false,
85                force: false,
86                track: false,
87            },
88        }
89    }
90
91    /// Check out `commit_ish` in the new worktree (requires [`add`](Self::add)).
92    #[must_use]
93    pub fn commit_ish(mut self, c: impl Into<String>) -> Self {
94        if let WorktreeAction::Add { commit_ish, .. } = &mut self.action {
95            *commit_ish = Some(c.into());
96        }
97        self
98    }
99
100    /// Create a new branch at the new worktree (requires [`add`](Self::add)).
101    #[must_use]
102    pub fn new_branch(mut self, b: impl Into<String>) -> Self {
103        if let WorktreeAction::Add { new_branch, .. } = &mut self.action {
104            *new_branch = Some(b.into());
105        }
106        self
107    }
108
109    /// `--detach` (requires [`add`](Self::add)).
110    #[must_use]
111    pub fn detach(mut self) -> Self {
112        if let WorktreeAction::Add { detach, .. } = &mut self.action {
113            *detach = true;
114        }
115        self
116    }
117
118    /// `--force` (requires [`add`](Self::add) or [`remove`](Self::remove)).
119    #[must_use]
120    pub fn force(mut self) -> Self {
121        match &mut self.action {
122            WorktreeAction::Add { force, .. } | WorktreeAction::Remove { force, .. } => {
123                *force = true;
124            }
125            _ => {}
126        }
127        self
128    }
129
130    /// `worktree list`.
131    #[must_use]
132    pub fn list() -> Self {
133        Self {
134            executor: CommandExecutor::default(),
135            action: WorktreeAction::List { porcelain: false },
136        }
137    }
138
139    /// `worktree list --porcelain`.
140    #[must_use]
141    pub fn list_porcelain() -> Self {
142        Self {
143            executor: CommandExecutor::default(),
144            action: WorktreeAction::List { porcelain: true },
145        }
146    }
147
148    /// `worktree remove`.
149    pub fn remove(path: impl Into<PathBuf>) -> Self {
150        Self {
151            executor: CommandExecutor::default(),
152            action: WorktreeAction::Remove {
153                path: path.into(),
154                force: false,
155            },
156        }
157    }
158
159    /// `worktree prune`.
160    #[must_use]
161    pub fn prune() -> Self {
162        Self {
163            executor: CommandExecutor::default(),
164            action: WorktreeAction::Prune {
165                verbose: false,
166                dry_run: false,
167            },
168        }
169    }
170
171    /// `worktree move`.
172    pub fn move_tree(source: impl Into<PathBuf>, destination: impl Into<PathBuf>) -> Self {
173        Self {
174            executor: CommandExecutor::default(),
175            action: WorktreeAction::Move {
176                source: source.into(),
177                destination: destination.into(),
178            },
179        }
180    }
181
182    /// `worktree lock`.
183    pub fn lock(path: impl Into<PathBuf>) -> Self {
184        Self {
185            executor: CommandExecutor::default(),
186            action: WorktreeAction::Lock {
187                path: path.into(),
188                reason: None,
189            },
190        }
191    }
192
193    /// Attach a `--reason` (requires [`lock`](Self::lock)).
194    #[must_use]
195    pub fn reason(mut self, r: impl Into<String>) -> Self {
196        if let WorktreeAction::Lock { reason, .. } = &mut self.action {
197            *reason = Some(r.into());
198        }
199        self
200    }
201
202    /// `worktree unlock`.
203    pub fn unlock(path: impl Into<PathBuf>) -> Self {
204        Self {
205            executor: CommandExecutor::default(),
206            action: WorktreeAction::Unlock { path: path.into() },
207        }
208    }
209}
210
211#[async_trait]
212impl GitCommand for WorktreeCommand {
213    type Output = CommandOutput;
214    fn get_executor(&self) -> &CommandExecutor {
215        &self.executor
216    }
217    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
218        &mut self.executor
219    }
220    fn build_command_args(&self) -> Vec<String> {
221        let mut args = vec!["worktree".to_string()];
222        match &self.action {
223            WorktreeAction::Add {
224                path,
225                commit_ish,
226                new_branch,
227                detach,
228                force,
229                track,
230            } => {
231                args.push("add".into());
232                if *force {
233                    args.push("--force".into());
234                }
235                if *detach {
236                    args.push("--detach".into());
237                }
238                if *track {
239                    args.push("--track".into());
240                }
241                if let Some(b) = new_branch {
242                    args.push("-b".into());
243                    args.push(b.clone());
244                }
245                args.push(path.display().to_string());
246                if let Some(c) = commit_ish {
247                    args.push(c.clone());
248                }
249            }
250            WorktreeAction::List { porcelain } => {
251                args.push("list".into());
252                if *porcelain {
253                    args.push("--porcelain".into());
254                }
255            }
256            WorktreeAction::Remove { path, force } => {
257                args.push("remove".into());
258                if *force {
259                    args.push("--force".into());
260                }
261                args.push(path.display().to_string());
262            }
263            WorktreeAction::Prune { verbose, dry_run } => {
264                args.push("prune".into());
265                if *verbose {
266                    args.push("-v".into());
267                }
268                if *dry_run {
269                    args.push("--dry-run".into());
270                }
271            }
272            WorktreeAction::Move {
273                source,
274                destination,
275            } => {
276                args.push("move".into());
277                args.push(source.display().to_string());
278                args.push(destination.display().to_string());
279            }
280            WorktreeAction::Lock { path, reason } => {
281                args.push("lock".into());
282                if let Some(r) = reason {
283                    args.push("--reason".into());
284                    args.push(r.clone());
285                }
286                args.push(path.display().to_string());
287            }
288            WorktreeAction::Unlock { path } => {
289                args.push("unlock".into());
290                args.push(path.display().to_string());
291            }
292        }
293        args
294    }
295    async fn execute(&self) -> Result<CommandOutput> {
296        self.execute_raw().await
297    }
298}