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    pub fn commit_ish(&mut self, c: impl Into<String>) -> &mut Self {
93        if let WorktreeAction::Add { commit_ish, .. } = &mut self.action {
94            *commit_ish = Some(c.into());
95        }
96        self
97    }
98
99    /// Create a new branch at the new worktree (requires [`add`](Self::add)).
100    pub fn new_branch(&mut self, b: impl Into<String>) -> &mut Self {
101        if let WorktreeAction::Add { new_branch, .. } = &mut self.action {
102            *new_branch = Some(b.into());
103        }
104        self
105    }
106
107    /// `--detach` (requires [`add`](Self::add)).
108    pub fn detach(&mut self) -> &mut Self {
109        if let WorktreeAction::Add { detach, .. } = &mut self.action {
110            *detach = true;
111        }
112        self
113    }
114
115    /// `--force` (requires [`add`](Self::add) or [`remove`](Self::remove)).
116    pub fn force(&mut self) -> &mut Self {
117        match &mut self.action {
118            WorktreeAction::Add { force, .. } | WorktreeAction::Remove { force, .. } => {
119                *force = true;
120            }
121            _ => {}
122        }
123        self
124    }
125
126    /// `worktree list`.
127    #[must_use]
128    pub fn list() -> Self {
129        Self {
130            executor: CommandExecutor::default(),
131            action: WorktreeAction::List { porcelain: false },
132        }
133    }
134
135    /// `worktree list --porcelain`.
136    #[must_use]
137    pub fn list_porcelain() -> Self {
138        Self {
139            executor: CommandExecutor::default(),
140            action: WorktreeAction::List { porcelain: true },
141        }
142    }
143
144    /// `worktree remove`.
145    pub fn remove(path: impl Into<PathBuf>) -> Self {
146        Self {
147            executor: CommandExecutor::default(),
148            action: WorktreeAction::Remove {
149                path: path.into(),
150                force: false,
151            },
152        }
153    }
154
155    /// `worktree prune`.
156    #[must_use]
157    pub fn prune() -> Self {
158        Self {
159            executor: CommandExecutor::default(),
160            action: WorktreeAction::Prune {
161                verbose: false,
162                dry_run: false,
163            },
164        }
165    }
166
167    /// `worktree move`.
168    pub fn move_tree(source: impl Into<PathBuf>, destination: impl Into<PathBuf>) -> Self {
169        Self {
170            executor: CommandExecutor::default(),
171            action: WorktreeAction::Move {
172                source: source.into(),
173                destination: destination.into(),
174            },
175        }
176    }
177
178    /// `worktree lock`.
179    pub fn lock(path: impl Into<PathBuf>) -> Self {
180        Self {
181            executor: CommandExecutor::default(),
182            action: WorktreeAction::Lock {
183                path: path.into(),
184                reason: None,
185            },
186        }
187    }
188
189    /// Attach a `--reason` (requires [`lock`](Self::lock)).
190    pub fn reason(&mut self, r: impl Into<String>) -> &mut Self {
191        if let WorktreeAction::Lock { reason, .. } = &mut self.action {
192            *reason = Some(r.into());
193        }
194        self
195    }
196
197    /// `worktree unlock`.
198    pub fn unlock(path: impl Into<PathBuf>) -> Self {
199        Self {
200            executor: CommandExecutor::default(),
201            action: WorktreeAction::Unlock { path: path.into() },
202        }
203    }
204}
205
206#[async_trait]
207impl GitCommand for WorktreeCommand {
208    type Output = CommandOutput;
209    fn get_executor(&self) -> &CommandExecutor {
210        &self.executor
211    }
212    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
213        &mut self.executor
214    }
215    fn build_command_args(&self) -> Vec<String> {
216        let mut args = vec!["worktree".to_string()];
217        match &self.action {
218            WorktreeAction::Add {
219                path,
220                commit_ish,
221                new_branch,
222                detach,
223                force,
224                track,
225            } => {
226                args.push("add".into());
227                if *force {
228                    args.push("--force".into());
229                }
230                if *detach {
231                    args.push("--detach".into());
232                }
233                if *track {
234                    args.push("--track".into());
235                }
236                if let Some(b) = new_branch {
237                    args.push("-b".into());
238                    args.push(b.clone());
239                }
240                args.push(path.display().to_string());
241                if let Some(c) = commit_ish {
242                    args.push(c.clone());
243                }
244            }
245            WorktreeAction::List { porcelain } => {
246                args.push("list".into());
247                if *porcelain {
248                    args.push("--porcelain".into());
249                }
250            }
251            WorktreeAction::Remove { path, force } => {
252                args.push("remove".into());
253                if *force {
254                    args.push("--force".into());
255                }
256                args.push(path.display().to_string());
257            }
258            WorktreeAction::Prune { verbose, dry_run } => {
259                args.push("prune".into());
260                if *verbose {
261                    args.push("-v".into());
262                }
263                if *dry_run {
264                    args.push("--dry-run".into());
265                }
266            }
267            WorktreeAction::Move {
268                source,
269                destination,
270            } => {
271                args.push("move".into());
272                args.push(source.display().to_string());
273                args.push(destination.display().to_string());
274            }
275            WorktreeAction::Lock { path, reason } => {
276                args.push("lock".into());
277                if let Some(r) = reason {
278                    args.push("--reason".into());
279                    args.push(r.clone());
280                }
281                args.push(path.display().to_string());
282            }
283            WorktreeAction::Unlock { path } => {
284                args.push("unlock".into());
285                args.push(path.display().to_string());
286            }
287        }
288        args
289    }
290    async fn execute(&self) -> Result<CommandOutput> {
291        self.execute_raw().await
292    }
293}