git_prole/git/worktree/
mod.rs1use std::borrow::Cow;
2use std::ffi::OsStr;
3use std::fmt::Debug;
4use std::process::Command;
5
6use camino::Utf8Path;
7use camino::Utf8PathBuf;
8use command_error::CommandExt;
9use command_error::OutputContext;
10use miette::miette;
11use miette::Context;
12use rustc_hash::FxHashMap;
13use tap::Tap;
14use tracing::instrument;
15use utf8_command::Utf8Output;
16
17use crate::config::BranchReplacement;
18use crate::final_component;
19use crate::AppGit;
20
21use super::BranchRef;
22use super::GitLike;
23use super::LocalBranchRef;
24
25mod resolve_unique_names;
26
27mod parse;
28
29pub use parse::Worktree;
30pub use parse::WorktreeHead;
31pub use parse::Worktrees;
32pub use resolve_unique_names::RenamedWorktree;
33pub use resolve_unique_names::ResolveUniqueNameOpts;
34
35#[repr(transparent)]
37pub struct GitWorktree<'a, G>(&'a G);
38
39impl<G> Debug for GitWorktree<'_, G>
40where
41 G: GitLike,
42{
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 f.debug_tuple("GitWorktree")
45 .field(&self.0.get_current_dir().as_ref())
46 .finish()
47 }
48}
49
50impl<'a, G> GitWorktree<'a, G>
51where
52 G: GitLike,
53{
54 pub fn new(git: &'a G) -> Self {
55 Self(git)
56 }
57
58 #[instrument(level = "trace")]
63 pub fn main(&self) -> miette::Result<Worktree> {
64 Ok(self.list()?.into_main())
66 }
67
68 #[instrument(level = "trace")]
73 pub fn container(&self) -> miette::Result<Utf8PathBuf> {
74 let mut path = self.main()?.path;
76 if !path.pop() {
77 Err(miette!("Main worktree path has no parent: {path}"))
78 } else {
79 Ok(path)
80 }
81 }
82
83 #[instrument(level = "trace")]
85 pub fn list(&self) -> miette::Result<Worktrees> {
86 Ok(self
87 .0
88 .command()
89 .args(["worktree", "list", "--porcelain", "-z"])
90 .output_checked_as(|context: OutputContext<Utf8Output>| {
91 if !context.status().success() {
92 Err(context.error())
93 } else {
94 let output = &context.output().stdout;
95 match Worktrees::parse(self.0.as_git(), output) {
96 Ok(worktrees) => Ok(worktrees),
97 Err(err) => {
98 let err = miette!("{err}");
99 Err(context.error_msg(err))
100 }
101 }
102 }
103 })?)
104 }
105
106 #[instrument(level = "trace")]
110 pub fn is_inside(&self) -> miette::Result<bool> {
111 Ok(self
112 .0
113 .as_git()
114 .rev_parse_command()
115 .arg("--is-inside-work-tree")
116 .output_checked_as(|context: OutputContext<Utf8Output>| {
117 if !context.status().success() {
118 Err(context.error())
119 } else {
120 let stdout = context.output().stdout.trim();
121 match stdout {
122 "true" => Ok(true),
123 "false" => Ok(false),
124 _ => Err(context.error_msg("Expected 'true' or 'false'")),
125 }
126 }
127 })?)
128 }
129
130 #[instrument(level = "trace")]
132 pub fn root(&self) -> miette::Result<Utf8PathBuf> {
133 Ok(self
134 .0
135 .as_git()
136 .rev_parse_command()
137 .arg("--show-toplevel")
138 .output_checked_utf8()
139 .wrap_err("Failed to get worktree root")?
140 .stdout
141 .trim()
142 .into())
143 }
144
145 #[instrument(level = "trace")]
146 pub fn add(&self, path: &Utf8Path, options: &AddWorktreeOpts<'_>) -> miette::Result<()> {
147 self.add_command(path, options).status_checked()?;
148 Ok(())
149 }
150
151 #[instrument(level = "trace")]
152 pub fn add_command(&self, path: &Utf8Path, options: &AddWorktreeOpts<'_>) -> Command {
153 let mut command = self.0.command();
154 command.args(["worktree", "add"]);
155
156 if options.detach {
157 command.arg("--detach");
158 }
159
160 if let Some(branch) = options.create_branch {
161 command.arg(if options.force_branch { "-B" } else { "-b" });
162 command.arg(branch.branch_name());
163 }
164
165 if !options.checkout {
166 command.arg("--no-checkout");
167 }
168
169 if options.guess_remote {
170 command.arg("--guess-remote");
171 }
172
173 if options.track {
174 command.arg("--track");
175 }
176
177 command.arg(path.as_str());
178
179 if let Some(start_point) = options.start_point {
180 command.arg(start_point);
181 }
182
183 command
184 }
185
186 #[instrument(level = "trace")]
187 pub fn rename(&self, from: &Utf8Path, to: &Utf8Path) -> miette::Result<()> {
188 self.0
189 .command()
190 .current_dir(from)
191 .args(["worktree", "move", from.as_str(), to.as_str()])
192 .status_checked()?;
193 Ok(())
194 }
195
196 #[instrument(level = "trace")]
197 pub fn repair(
198 &self,
199 paths: impl IntoIterator<Item = impl AsRef<OsStr>> + Debug,
200 ) -> miette::Result<()> {
201 self.0
202 .command()
203 .args(["worktree", "repair"])
204 .args(paths)
205 .output_checked_utf8()?;
206 Ok(())
207 }
208}
209
210#[derive(Clone, Copy, Debug)]
212pub struct AddWorktreeOpts<'a> {
213 pub force_branch: bool,
216 pub create_branch: Option<&'a LocalBranchRef>,
218 pub checkout: bool,
221 pub guess_remote: bool,
224 pub track: bool,
227 pub start_point: Option<&'a str>,
229 pub detach: bool,
232}
233
234impl<'a> Default for AddWorktreeOpts<'a> {
235 fn default() -> Self {
236 Self {
237 force_branch: false,
238 create_branch: None,
239 checkout: true,
240 guess_remote: false,
241 track: false,
242 start_point: None,
243 detach: false,
244 }
245 }
246}
247
248impl<'a, C> GitWorktree<'a, AppGit<'a, C>>
249where
250 C: AsRef<Utf8Path>,
251{
252 pub fn dirname_for<'b>(&self, branch: &'b str) -> Cow<'b, str> {
258 let branch_replacements = self.0.config.file.add.branch_replacements();
259 if branch_replacements.is_empty() {
260 Cow::Borrowed(final_component(branch))
261 } else {
262 let mut dirname = branch.to_owned();
263 for BranchReplacement {
264 find,
265 replace,
266 count,
267 } in branch_replacements
268 {
269 dirname = match count {
270 Some(count) => find.replacen(&dirname, *count, replace),
271 None => find.replace_all(&dirname, replace),
272 }
273 .into_owned();
274 }
275
276 if dirname.contains(std::path::MAIN_SEPARATOR_STR) {
277 let final_component = final_component(&dirname);
278 tracing::warn!(
279 %branch,
280 after_replacements=%dirname,
281 using=%final_component,
282 "Applying `add.branch_replacements` substitutions resulted in a directory name which includes a `{}`",
283 std::path::MAIN_SEPARATOR_STR,
284 );
285 final_component.to_owned().into()
286 } else {
287 dirname.into()
288 }
289 }
290 }
291
292 #[instrument(level = "trace")]
296 pub fn path_for(&self, branch: &str) -> miette::Result<Utf8PathBuf> {
297 Ok(self
298 .container()?
299 .tap_mut(|p| p.push(&*self.dirname_for(branch))))
300 }
301
302 #[instrument(level = "trace")]
304 pub fn resolve_unique_names(
305 &self,
306 opts: ResolveUniqueNameOpts<'_>,
307 ) -> miette::Result<FxHashMap<Utf8PathBuf, RenamedWorktree>> {
308 resolve_unique_names::resolve_unique_worktree_names(self.0, opts)
309 }
310
311 #[instrument(level = "trace")]
313 pub fn preferred_branch(
314 &self,
315 preferred_branch: Option<&BranchRef>,
316 worktrees: Option<&Worktrees>,
317 ) -> miette::Result<Option<Worktree>> {
318 let worktrees = match worktrees {
319 Some(worktrees) => worktrees,
320 None => &self.list()?,
321 };
322 let preferred_branch = match preferred_branch {
323 Some(preferred_branch) => preferred_branch,
324 None => &match self.0.branch().preferred()? {
325 Some(preferred_branch) => preferred_branch,
326 None => {
327 return Ok(None);
328 }
329 },
330 };
331
332 Ok(worktrees.for_branch(&preferred_branch.as_local()).cloned())
334 }
335
336 #[instrument(level = "trace")]
344 pub fn find_some(&self) -> miette::Result<Utf8PathBuf> {
345 if self.is_inside()? {
346 tracing::debug!("Inside worktree");
347 return self.root();
349 }
350 let worktrees = self.list()?;
351
352 if let Some(worktree) = self.preferred_branch(None, Some(&worktrees))? {
353 tracing::debug!(%worktree, "Found worktree for preferred branch");
354 return Ok(worktree.path);
356 }
357
358 tracing::debug!("No worktree for preferred branch");
359
360 if worktrees.main().head.is_bare() && worktrees.len() > 1 {
361 let worktree = worktrees
365 .into_iter()
366 .find(|(_path, worktree)| !worktree.head.is_bare())
367 .expect("Only one worktree can be bare")
368 .0;
369
370 tracing::debug!(%worktree, "Found non-bare worktree");
371 return Ok(worktree);
372 }
373
374 tracing::debug!("Non-bare main worktree or no non-bare worktrees");
385 Ok(worktrees.main_path().to_owned())
386 }
387}