Skip to main content

grm/repo/worktree/
mod.rs

1//! This handles worktrees for repositories. Some considerations to take care
2//! of:
3//!
4//! * Which branch to check out / create
5//! * Which commit to check out
6//! * Whether to track a remote branch, and which
7//!
8//! There are a general rules. The main goal is to do the least surprising thing
9//! in each situation, and to never change existing setups (e.g. tracking,
10//! branch states) except when explicitly told to. In 99% of all cases, the
11//! workflow will be quite straightforward.
12//!
13//! * The name of the worktree (and therefore the path) is **always** the same
14//!   as the name of the branch.
15//! * Never modify existing local branches
16//! * Only modify tracking branches for existing local branches if explicitly
17//!   requested
18//! * By default, do not do remote operations. This means that we do no do any
19//!   tracking setup (but of course, the local branch can already have a
20//!   tracking branch set up, which will just be left alone)
21//! * Be quite lax with finding a remote tracking branch (as using an existing
22//!   branch is most likely preferred to creating a new branch)
23//!
24//! There are a few different options that can be given:
25//!
26//! * Explicit track (`--track`) and explicit no-track (`--no-track`)
27//! * A configuration may specify to enable tracking a remote branch by default
28//! * A configuration may specify a prefix for remote branches
29//!
30//! # How to handle the local branch?
31//!
32//! That one is easy: If a branch with the desired name already exists, all is
33//! well. If not, we create a new one.
34//!
35//! # Which commit should be checked out?
36//!
37//! The most imporant rule: If the local branch already existed, just leave it
38//! as it is. Only if a new branch is created do we need to answer the question
39//! which commit to set it to. Generally, we set the branch to whatever the
40//! "default" branch of the repository is (something like "main" or "master").
41//! But there are a few cases where we can use remote branches to make the
42//! result less surprising.
43//!
44//! First, if tracking is explicitly disabled, we still try to guess! But we
45//! *do* ignore `--track`, as this is how it's done everywhere else.
46//!
47//! As an example: If `origin/foobar` exists and we run `grm worktree add foobar
48//! --no-track`, we create a new worktree called `foobar` that's on the same
49//! state as `origin/foobar` (but we will not set up tracking, see below).
50//!
51//! If tracking is explicitly requested to a certain state, we use that remote
52//! branch. If it exists, easy. If not, no more guessing!
53//!
54//! Now, it's important to select the correct remote. In the easiest case, there
55//! is only one remote, so we just use that one. If there is more than one
56//! remote, we check whether there is a default remote configured via
57//! `track.default_remote`. If yes, we use that one. If not, we have to do the
58//! selection process below *for each of them*.  If only one of them returns
59//! some branch to track, we use that one. If more than one remote returns
60//! information, we only use it if it's identical for each. Otherwise we bail,
61//! as there is no point in guessing.
62//!
63//! The commit selection process looks like this:
64//!
65//! * If a prefix is specified in the configuration, we look for
66//!   `{remote}/{prefix}/{worktree_name}`
67//!
68//! * We look for `{remote}/{worktree_name}` (yes, this means that even when a
69//!   prefix is configured, we use a branch *without* a prefix if one with
70//!   prefix does not exist)
71//!
72//! Note that we may select different branches for different remotes when
73//! prefixes is used. If remote1 has a branch with a prefix and remote2 only has
74//! a branch *without* a prefix, we select them both when a prefix is used. This
75//! could lead to the following situation:
76//!
77//! * There is `origin/prefix/foobar` and `remote2/foobar`, with different
78//!   states
79//! * You set `track.default_prefix = "prefix"` (and no default remote!)
80//! * You run `grm worktree add prefix/foobar`
81//! * Instead of just picking `origin/prefix/foobar`, grm will complain because
82//!   it also selected `remote2/foobar`.
83//!
84//! This is just emergent behavior of the logic above. Fixing it would require
85//! additional logic for that edge case. I assume that it's just so rare to get
86//! that behavior that it's acceptable for now.
87//!
88//! Now we either have a commit, we aborted, or we do not have commit. In the
89//! last case, as stated above, we check out the "default" branch.
90//!
91//! # The remote tracking branch
92//!
93//! First, the only remote operations we do is branch creation! It's
94//! unfortunately not possible to defer remote branch creation until the first
95//! `git push`, which would be ideal. The remote tracking branch has to already
96//! exist, so we have to do the equivalent of `git push --set-upstream` during
97//! worktree creation.
98//!
99//! Whether (and which) remote branch to track works like this:
100//!
101//! * If `--no-track` is given, we never track a remote branch, except when
102//!   branch already has a tracking branch. So we'd be done already!
103//!
104//! * If `--track` is given, we always track this branch, regardless of anything
105//!   else. If the branch exists, cool, otherwise we create it.
106//!
107//! If neither is given, we only set up tracking if requested in the
108//! configuration file (`track.default = true`)
109//!
110//! The rest of the process is similar to the commit selection above. The only
111//! difference is the remote selection.  If there is only one, we use it, as
112//! before. Otherwise, we try to use `default_remote` from the configuration, if
113//! available.  If not, we do not set up a remote tracking branch. It works like
114//! this:
115//!
116//! * If a prefix is specified in the configuration, we use
117//!   `{remote}/{prefix}/{worktree_name}`
118//!
119//! * If no prefix is specified in the configuration, we use
120//!   `{remote}/{worktree_name}`
121//!
122//! Now that we have a remote, we use the same process as above:
123//!
124//! * If a prefix is specified in the configuration, we use for
125//!   `{remote}/{prefix}/{worktree_name}`
126//! * We use for `{remote}/{worktree_name}`
127//!
128//! ---
129//!
130//! All this means that in some weird situation, you may end up with the state
131//! of a remote branch while not actually tracking that branch. This can only
132//! happen in repositories with more than one remote. Imagine the following:
133//!
134//! The repository has two remotes (`remote1` and `remote2`) which have the
135//! exact same remote state. But there is no `default_remote` in the
136//! configuration (or no configuration at all). There is a remote branch
137//! `foobar`. As both `remote1/foobar` and `remote2/foobar` as the same, the new
138//! worktree will use that as the state of the new branch. But as `grm` cannot
139//! tell which remote branch to track, it will not set up remote tracking. This
140//! behavior may be a bit confusing, but first, there is no good way to resolve
141//! this, and second, the situation should be really rare (when having multiple
142//! remotes, you would generally have a `default_remote` configured).
143//!
144//! # Implementation
145//!
146//! To reduce the chance of bugs, the implementation uses the [typestate
147//! pattern](http://cliffle.com/blog/rust-typestate/). Here are the states we
148//! are moving through linearily:
149//!
150//! * Init
151//! * A local branch name is set
152//! * A local commit to set the new branch to is selected
153//! * A remote tracking branch is selected
154//! * The new branch is created with all the required settings
155//!
156//! Don't worry about the lifetime stuff: There is only one single lifetime, as
157//! everything (branches, commits) is derived from the single `repo::Repo`
158//! instance
159//!
160//! # Testing
161//!
162//! There are two types of input to the tests:
163//!
164//! 1) The parameters passed to `grm`, either via command line or via
165//!    configuration file
166//! 2) The circumstances in the repository and remotes
167//!
168//! ## Parameters
169//!
170//! * The name of the worktree
171//!   * Whether it contains slashes or not
172//!   * Whether it is invalid
173//! * `--track` and `--no-track`
174//! * Whether there is a configuration file and what it contains
175//!   * Whether `track.default` is enabled or disabled
176//!   * Whether `track.default_remote_prefix` is there or missing
177//!   * Whether `track.default_remote` is there or missing
178//!     * Whether that remote exists or not
179//!
180//! ## Situations
181//!
182//! ### The local branch
183//!
184//! * Whether the branch already exists
185//! * Whether the branch has a remote tracking branch and whether it differs
186//!   from the desired tracking branch (i.e. `--track` or config)
187//!
188//! ### Remotes
189//!
190//! * How many remotes there are, if any
191//! * If more than two remotes exist, whether their desired tracking branch
192//!   differs
193//!
194//! ### The remote tracking branch branch
195//!
196//! * Whether a remote branch with the same name as the worktree exists
197//! * Whether a remote branch with the same name as the worktree plus prefix
198//!   exists
199//!
200//! ## Outcomes
201//!
202//! We have to check the following afterwards:
203//!
204//! * Does the worktree exist in the correct location?
205//! * Does the local branch have the same name as the worktree?
206//! * Does the local branch have the correct commit?
207//! * Does the local branch track the correct remote branch?
208//! * Does that remote branch also exist?
209mod error;
210
211pub use error::{
212    CleanupWorktreeError, CleanupWorktreeWarning, CleanupWorktreeWarningReason, Error,
213    WorktreeConversionError, WorktreeRemoveError, WorktreeValidationError,
214    WorktreeValidationErrorReason,
215};
216
217use std::{fmt, iter, sync::mpsc};
218
219use camino::{Utf8Path as Path, Utf8PathBuf as PathBuf};
220
221use super::{Branch, BranchName, RemoteName, RepoHandle, Warning, config};
222use crate::{
223    path,
224    repo::{self, RepoChanges},
225};
226
227pub const GIT_MAIN_WORKTREE_DIRECTORY: &str = ".git-main-working-tree";
228
229pub struct Worktree {
230    name: WorktreeName,
231}
232
233impl Worktree {
234    /// A branch name must never start or end with a slash, and it cannot have two
235    /// consecutive slashes
236    fn new(name: &str) -> Result<Self, WorktreeValidationError> {
237        Ok(Self {
238            name: WorktreeName::new(name.to_owned())?,
239        })
240    }
241
242    pub fn name(&self) -> &WorktreeName {
243        &self.name
244    }
245
246    fn into_name(self) -> WorktreeName {
247        self.name
248    }
249
250    pub fn forward_branch(&self, rebase: bool, stash: bool) -> Result<Option<Warning>, Error> {
251        let repo = RepoHandle::open(Path::new(&self.name.as_str()))?;
252
253        let branch_name = BranchName::new(self.name.as_str().to_owned());
254
255        if let Some(remote_branch) = repo
256            .find_local_branch(&branch_name)?
257            .ok_or(Error::BranchNotFound(branch_name))?
258            .upstream()?
259        {
260            let status = repo.status(WorktreeSetup::NoWorktree)?;
261            let mut stashed_changes = false;
262
263            if !status.clean() {
264                if stash {
265                    repo.stash()?;
266                    stashed_changes = true;
267                } else {
268                    return Ok(Some(Warning(String::from("Worktree contains changes"))));
269                }
270            }
271
272            let unstash = || -> Result<(), Error> {
273                if stashed_changes {
274                    repo.stash_pop()?;
275                }
276                Ok(())
277            };
278
279            let remote_annotated_commit = repo
280                .0
281                .find_annotated_commit(remote_branch.commit()?.id().0)?;
282
283            if rebase {
284                let mut rebase = repo.0.rebase(
285                    None, // use HEAD
286                    Some(&remote_annotated_commit),
287                    None, // figure out the base yourself, libgit2!
288                    Some(&mut git2::RebaseOptions::new()),
289                )?;
290
291                while let Some(operation) = rebase.next() {
292                    let operation = operation?;
293
294                    // This is required to preserve the commiter of the rebased
295                    // commits, which is the expected behavior.
296                    let rebased_commit = repo.0.find_commit(operation.id())?;
297                    let committer = rebased_commit.committer();
298
299                    // This is effectively adding all files to the index explicitly.
300                    // Normal files are already staged, but changed submodules are not.
301                    let mut index = repo.0.index()?;
302                    index.add_all(iter::once("."), git2::IndexAddOption::CHECK_PATHSPEC, None)?;
303
304                    if let Err(error) = rebase.commit(None, &committer, None) {
305                        if error.code() == git2::ErrorCode::Applied {
306                            continue;
307                        }
308                        rebase.abort()?;
309                        unstash()?;
310                        return Err(error.into());
311                    }
312                }
313
314                rebase.finish(None)?;
315            } else {
316                let (analysis, _preference) = repo.0.merge_analysis(&[&remote_annotated_commit])?;
317
318                if analysis.is_up_to_date() {
319                    unstash()?;
320                    return Ok(None);
321                }
322                if !analysis.is_fast_forward() {
323                    unstash()?;
324                    return Ok(Some(Warning(String::from(
325                        "Worktree cannot be fast forwarded",
326                    ))));
327                }
328
329                repo.0.reset(
330                    remote_branch.commit()?.0.as_object(),
331                    git2::ResetType::Hard,
332                    Some(git2::build::CheckoutBuilder::new().safe()),
333                )?;
334            }
335            unstash()?;
336        } else {
337            return Ok(Some(Warning(String::from(
338                "No remote branch to rebase onto",
339            ))));
340        }
341
342        Ok(None)
343    }
344
345    pub fn rebase_onto_default(
346        &self,
347        config: &Option<WorktreeRootConfig>,
348        stash: bool,
349    ) -> Result<Option<Warning>, Error> {
350        let repo = RepoHandle::open(Path::new(&self.name.as_str()))?;
351
352        let guess_default_branch = || repo.default_branch()?.name();
353
354        let default_branch_name = match *config {
355            None => guess_default_branch()?,
356            Some(ref config) => match config.persistent_branches {
357                None => guess_default_branch()?,
358                Some(ref persistent_branches) => {
359                    if let Some(branch) = persistent_branches.first() {
360                        branch.clone()
361                    } else {
362                        guess_default_branch()?
363                    }
364                }
365            },
366        };
367
368        let status = repo.status(WorktreeSetup::NoWorktree)?;
369        let mut stashed_changes = false;
370
371        if !status.clean() {
372            if stash {
373                repo.stash()?;
374                stashed_changes = true;
375            } else {
376                return Ok(Some(Warning("Worktree contains changes".to_owned())));
377            }
378        }
379
380        let unstash = || -> Result<(), Error> {
381            if stashed_changes {
382                repo.stash_pop()?;
383            }
384            Ok(())
385        };
386
387        let base_branch = repo
388            .find_local_branch(&default_branch_name)?
389            .ok_or(Error::BranchNotFound(default_branch_name))?;
390        let base_annotated_commit = repo.0.find_annotated_commit(base_branch.commit()?.id().0)?;
391
392        let mut rebase = repo.0.rebase(
393            None, // use HEAD
394            Some(&base_annotated_commit),
395            None, // figure out the base yourself, libgit2!
396            Some(&mut git2::RebaseOptions::new()),
397        )?;
398
399        while let Some(operation) = rebase.next() {
400            let operation = operation?;
401
402            // This is required to preserve the commiter of the rebased
403            // commits, which is the expected behavior.
404            let rebased_commit = repo.0.find_commit(operation.id())?;
405            let committer = rebased_commit.committer();
406
407            // This is effectively adding all files to the index explicitly.
408            // Normal files are already staged, but changed submodules are not.
409            let mut index = repo.0.index()?;
410            index.add_all(iter::once("."), git2::IndexAddOption::CHECK_PATHSPEC, None)?;
411
412            if let Err(error) = rebase.commit(None, &committer, None) {
413                if error.code() == git2::ErrorCode::Applied {
414                    continue;
415                }
416                rebase.abort()?;
417                unstash()?;
418                return Err(error.into());
419            }
420        }
421
422        rebase.finish(None)?;
423        unstash()?;
424        Ok(None)
425    }
426}
427
428#[derive(Debug, PartialEq, Eq, Clone, Copy)]
429pub enum WorktreeSetup {
430    Worktree,
431    NoWorktree,
432}
433
434impl WorktreeSetup {
435    pub fn is_worktree(&self) -> bool {
436        *self == Self::Worktree
437    }
438
439    pub fn detect(path: &Path) -> Self {
440        if path.join(GIT_MAIN_WORKTREE_DIRECTORY).exists() {
441            Self::Worktree
442        } else {
443            Self::NoWorktree
444        }
445    }
446}
447
448impl From<bool> for WorktreeSetup {
449    fn from(value: bool) -> Self {
450        if value {
451            Self::Worktree
452        } else {
453            Self::NoWorktree
454        }
455    }
456}
457
458struct Init;
459
460enum LocalBranchInfo<'a> {
461    NoBranch,
462    Branch(repo::Branch<'a>),
463}
464
465struct WithLocalBranchName<'a> {
466    local_branch_name: BranchName,
467    local_branch: LocalBranchInfo<'a>,
468}
469
470struct WithLocalTargetSelected<'a> {
471    local_branch_name: BranchName,
472    local_branch: Option<repo::Branch<'a>>,
473    target_commit: Option<repo::Commit<'a>>,
474}
475
476struct RemoteTrackingBranch {
477    remote_name: RemoteName,
478    remote_branch_name: BranchName,
479    prefix: Option<String>,
480}
481
482struct WithRemoteTrackingBranch<'a> {
483    local_branch_name: BranchName,
484    local_branch: Option<repo::Branch<'a>>,
485    target_commit: Option<repo::Commit<'a>>,
486    remote_tracking_branch: Option<RemoteTrackingBranch>,
487}
488
489struct NewWorktree<'a, S: WorktreeState> {
490    repo: &'a WorktreeRepoHandle,
491    extra: S,
492}
493
494impl<'a> WithLocalBranchName<'a> {
495    fn new(name: &BranchName, worktree: &NewWorktree<'a, Init>) -> Result<Self, Error> {
496        Ok(Self {
497            local_branch_name: name.clone(),
498            local_branch: {
499                let branch = worktree.repo.as_repo().find_local_branch(name)?;
500                match branch {
501                    Some(branch) => LocalBranchInfo::Branch(branch),
502                    None => LocalBranchInfo::NoBranch,
503                }
504            },
505        })
506    }
507}
508
509trait WorktreeState {}
510
511impl WorktreeState for Init {}
512impl WorktreeState for WithLocalBranchName<'_> {}
513impl WorktreeState for WithLocalTargetSelected<'_> {}
514impl WorktreeState for WithRemoteTrackingBranch<'_> {}
515
516impl<'a> NewWorktree<'a, Init> {
517    fn new(repo: &'a WorktreeRepoHandle) -> Self {
518        Self {
519            repo,
520            extra: Init {},
521        }
522    }
523
524    fn set_local_branch_name(
525        self,
526        name: &BranchName,
527    ) -> Result<NewWorktree<'a, WithLocalBranchName<'a>>, Error> {
528        Ok(NewWorktree::<WithLocalBranchName> {
529            repo: self.repo,
530            extra: WithLocalBranchName::new(name, &self)?,
531        })
532    }
533}
534
535impl<'a, 'b> NewWorktree<'a, WithLocalBranchName<'b>>
536where
537    'a: 'b,
538{
539    fn local_branch_already_exists(&self) -> bool {
540        matches!(
541            self.extra.local_branch,
542            LocalBranchInfo::Branch(ref _branch)
543        )
544    }
545
546    fn select_commit(
547        self,
548        commit: Option<repo::Commit<'b>>,
549    ) -> NewWorktree<'a, WithLocalTargetSelected<'b>> {
550        NewWorktree::<'a, WithLocalTargetSelected> {
551            repo: self.repo,
552            extra: WithLocalTargetSelected::<'b> {
553                local_branch_name: self.extra.local_branch_name,
554                // As we just called `check_local_branch`, we can be sure that
555                // `self.extra.local_branch` is set to some `Some` value
556                local_branch: match self.extra.local_branch {
557                    LocalBranchInfo::NoBranch => None,
558                    LocalBranchInfo::Branch(branch) => Some(branch),
559                },
560                target_commit: commit,
561            },
562        }
563    }
564}
565
566impl<'a> NewWorktree<'a, WithLocalTargetSelected<'a>> {
567    fn set_remote_tracking_branch(
568        self,
569        branch: Option<RemoteTrackingBranch>,
570    ) -> NewWorktree<'a, WithRemoteTrackingBranch<'a>> {
571        NewWorktree::<WithRemoteTrackingBranch> {
572            repo: self.repo,
573            extra: WithRemoteTrackingBranch {
574                local_branch_name: self.extra.local_branch_name,
575                local_branch: self.extra.local_branch,
576                target_commit: self.extra.target_commit,
577                remote_tracking_branch: branch,
578            },
579        }
580    }
581}
582
583impl<'a> NewWorktree<'a, WithRemoteTrackingBranch<'a>> {
584    fn create(self, directory: &Path) -> Result<Option<Vec<Warning>>, Error> {
585        let mut warnings: Vec<Warning> = vec![];
586
587        let mut branch = if let Some(branch) = self.extra.local_branch {
588            branch
589        } else {
590            self.repo.as_repo().create_branch(
591                &self.extra.local_branch_name,
592                // TECHDEBT
593                // We must not call this with `Some()` without a valid target.
594                // I'm sure this can be improved, just not sure how.
595                &self
596                    .extra
597                    .target_commit
598                    .expect("target_commit must not be empty"),
599            )?
600        };
601
602        if let Some(remote_branch_config) = self.extra.remote_tracking_branch {
603            let remote_branch_with_prefix = if let Some(ref prefix) = remote_branch_config.prefix {
604                self.repo.as_repo().find_remote_branch(
605                    &remote_branch_config.remote_name,
606                    &BranchName::new(format!(
607                        "{prefix}/{}",
608                        remote_branch_config.remote_branch_name
609                    )),
610                )?
611            } else {
612                None
613            };
614
615            let remote_branch_without_prefix = self.repo.as_repo().find_remote_branch(
616                &remote_branch_config.remote_name,
617                &remote_branch_config.remote_branch_name,
618            )?;
619
620            let remote_branch = if let Some(ref _prefix) = remote_branch_config.prefix {
621                remote_branch_with_prefix
622            } else {
623                remote_branch_without_prefix
624            };
625
626            if let Some(remote_branch) = remote_branch {
627                if branch.commit()?.id().hex_string() != remote_branch.commit()?.id().hex_string() {
628                    warnings.push(Warning(format!("The local branch \"{}\" and the remote branch \"{}/{}\" differ. Make sure to push/pull afterwards!", &self.extra.local_branch_name, &remote_branch_config.remote_name, &remote_branch_config.remote_branch_name)));
629                }
630
631                branch.set_upstream(
632                    &remote_branch_config.remote_name,
633                    &remote_branch.basename()?,
634                )?;
635            } else {
636                let Some(mut remote) = self
637                    .repo
638                    .as_repo()
639                    .find_remote(&remote_branch_config.remote_name)?
640                else {
641                    return Err(Error::RemoteNotFound {
642                        name: remote_branch_config.remote_name,
643                    });
644                };
645
646                if !remote.is_pushable()? {
647                    return Err(Error::RemoteNotPushable {
648                        name: remote_branch_config.remote_name,
649                    });
650                }
651
652                if let Some(prefix) = remote_branch_config.prefix {
653                    remote.push(
654                        &self.extra.local_branch_name,
655                        &BranchName::new(format!(
656                            "{prefix}/{}",
657                            remote_branch_config.remote_branch_name
658                        )),
659                        self.repo.as_repo(),
660                    )?;
661
662                    branch.set_upstream(
663                        &remote_branch_config.remote_name,
664                        &BranchName::new(format!(
665                            "{prefix}/{}",
666                            remote_branch_config.remote_branch_name
667                        )),
668                    )?;
669                } else {
670                    remote.push(
671                        &self.extra.local_branch_name,
672                        &remote_branch_config.remote_branch_name,
673                        self.repo.as_repo(),
674                    )?;
675
676                    branch.set_upstream(
677                        &remote_branch_config.remote_name,
678                        &remote_branch_config.remote_branch_name,
679                    )?;
680                }
681            }
682        }
683
684        let branch_name = self.extra.local_branch_name.into_string();
685        // We have to create subdirectories first, otherwise adding the worktree
686        // will fail
687        if branch_name.contains('/') {
688            let path = Path::new(&branch_name);
689            if let Some(base) = path.parent() {
690                // This is a workaround of a bug in libgit2 (?)
691                //
692                // When *not* doing this, we will receive an error from the
693                // `Repository::worktree()` like this:
694                //
695                // > failed to make directory '/{repo}/.git-main-working-tree/worktrees/dir/test
696                //
697                // This is a discrepancy between the behavior of libgit2 and the
698                // git CLI when creating worktrees with slashes:
699                //
700                // The git CLI will create the worktree's configuration directory
701                // inside {git_dir}/worktrees/{last_path_component}. Look at this:
702                //
703                // ```
704                // $ git worktree add 1/2/3 -b 1/2/3
705                // $ ls .git/worktrees
706                // 3
707                // ```
708                //
709                // Interesting: When adding a worktree with a different name but the
710                // same final path component, git starts adding a counter suffix to
711                // the worktree directories:
712                //
713                // ```
714                // $ git worktree add 1/3/3 -b 1/3/3
715                // $ git worktree add 1/4/3 -b 1/4/3
716                // $ ls .git/worktrees
717                // 3
718                // 31
719                // 32
720                // ```
721                //
722                // I *guess* that the mapping back from the worktree directory under .git to the
723                // actual worktree directory is done via the `gitdir` file
724                // inside `.git/worktrees/{worktree}. This means that the actual
725                // directory would not matter. You can verify this by
726                // just renaming it:
727                //
728                // ```
729                // $ mv .git/worktrees/3 .git/worktrees/foobar
730                // $ git worktree list
731                // /tmp/       fcc8a2a7 [master]
732                // /tmp/1/2/3  fcc8a2a7 [1/2/3]
733                // /tmp/1/3/3  fcc8a2a7 [1/3/3]
734                // /tmp/1/4/3  fcc8a2a7 [1/4/3]
735                // ```
736                //
737                // => Still works
738                //
739                // Anyway, libgit2 does not do this: It tries to create the worktree
740                // directory inside .git with the exact name of the worktree, including
741                // any slashes. It should be this code:
742                //
743                // https://github.com/libgit2/libgit2/blob/f98dd5438f8d7bfd557b612fdf1605b1c3fb8eaf/src/libgit2/worktree.c#L346
744                //
745                // As a workaround, we can create the base directory manually for now.
746                //
747                // Tracking upstream issue: https://github.com/libgit2/libgit2/issues/6327
748                std::fs::create_dir_all(
749                    directory
750                        .join(GIT_MAIN_WORKTREE_DIRECTORY)
751                        .join("worktrees")
752                        .join(base),
753                )?;
754                std::fs::create_dir_all(base)?;
755            }
756        }
757
758        self.repo
759            .new_worktree(&branch_name, &directory.join(&branch_name), &branch)?;
760
761        Ok(if warnings.is_empty() {
762            None
763        } else {
764            Some(warnings)
765        })
766    }
767}
768
769#[derive(Debug, Clone, PartialEq, Eq)]
770pub struct WorktreeName(String);
771
772impl fmt::Display for WorktreeName {
773    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
774        write!(f, "{}", self.0)
775    }
776}
777
778impl WorktreeName {
779    pub fn new(name: String) -> Result<Self, WorktreeValidationError> {
780        if name.starts_with('/') || name.ends_with('/') {
781            return Err(WorktreeValidationError {
782                name,
783                reason: WorktreeValidationErrorReason::SlashAtStartOrEnd,
784            });
785        }
786
787        if name.contains("//") {
788            return Err(WorktreeValidationError {
789                name,
790                reason: WorktreeValidationErrorReason::ConsecutiveSlashes,
791            });
792        }
793
794        if name.contains(char::is_whitespace) {
795            return Err(WorktreeValidationError {
796                name,
797                reason: WorktreeValidationErrorReason::ContainsWhitespace,
798            });
799        }
800
801        Ok(Self(name))
802    }
803
804    pub fn as_str(&self) -> &str {
805        &self.0
806    }
807}
808
809pub enum TrackingSelection {
810    Explicit {
811        remote_name: RemoteName,
812        remote_branch_name: BranchName,
813    },
814    Automatic,
815    Disabled,
816}
817
818#[cfg(test)]
819mod tests {
820    use super::*;
821
822    #[test]
823    fn invalid_worktree_names() {
824        assert!(WorktreeName::new("/leadingslash".to_owned()).is_err());
825        assert!(WorktreeName::new("trailingslash/".to_owned()).is_err());
826        assert!(WorktreeName::new("//".to_owned()).is_err());
827        assert!(WorktreeName::new("test//test".to_owned()).is_err());
828        assert!(WorktreeName::new("test test".to_owned()).is_err());
829        assert!(WorktreeName::new("test\ttest".to_owned()).is_err());
830    }
831}
832
833pub struct WorktreeRootConfig {
834    pub persistent_branches: Option<Vec<BranchName>>,
835    pub track: Option<TrackingConfig>,
836}
837
838#[derive(Clone, Copy, PartialEq, Eq)]
839pub enum TrackingDefault {
840    Track,
841    NoTrack,
842}
843
844pub struct TrackingConfig {
845    pub default: TrackingDefault,
846    pub default_remote: RemoteName,
847    pub default_remote_prefix: Option<String>,
848}
849
850impl From<config::TrackingConfig> for TrackingConfig {
851    fn from(other: config::TrackingConfig) -> Self {
852        Self {
853            default: if other.default {
854                TrackingDefault::Track
855            } else {
856                TrackingDefault::NoTrack
857            },
858            default_remote: RemoteName::new(other.default_remote),
859            default_remote_prefix: other.default_remote_prefix,
860        }
861    }
862}
863
864impl From<config::WorktreeRootConfig> for WorktreeRootConfig {
865    fn from(other: config::WorktreeRootConfig) -> Self {
866        Self {
867            persistent_branches: other
868                .persistent_branches
869                .map(|branches| branches.into_iter().map(BranchName::new).collect()),
870            track: other.track.map(Into::into),
871        }
872    }
873}
874
875pub struct WorktreeRepoHandle(super::RepoHandle);
876
877impl WorktreeRepoHandle {
878    pub fn open(path: &Path) -> Result<Self, super::Error> {
879        Ok(Self(super::RepoHandle::open_with_worktree_setup(
880            path,
881            WorktreeSetup::Worktree,
882        )?))
883    }
884
885    pub fn as_repo(&self) -> &super::RepoHandle {
886        &self.0
887    }
888
889    fn base_directory(&self) -> Result<&Path, Error> {
890        let commondir = self.0.commondir()?;
891        commondir
892            .parent()
893            .ok_or_else(|| Error::InvalidBaseDirectory {
894                git_dir: commondir.to_owned(),
895            })
896    }
897
898    pub fn from_handle_unchecked(handle: super::RepoHandle) -> Self {
899        Self(handle)
900    }
901
902    pub fn into_handle(self) -> super::RepoHandle {
903        self.0
904    }
905
906    fn worktree_exists(&self, name: &WorktreeName) -> Result<bool, Error> {
907        match self.0.0.find_worktree(name.as_str()) {
908            Ok(_worktree) => Ok(true),
909            Err(err) if err.code() == git2::ErrorCode::NotFound => Ok(false),
910            Err(e) => Err(e.into()),
911        }
912    }
913
914    pub fn default_branch(&self) -> Result<Branch<'_>, Error> {
915        Ok(self.0.default_branch()?)
916    }
917
918    fn find_local_branch(&self, name: &BranchName) -> Result<Option<Branch<'_>>, Error> {
919        Ok(self.0.find_local_branch(name)?)
920    }
921
922    pub fn cleanup_worktrees(
923        &self,
924        directory: &Path,
925        deletion_notify_channel: &mpsc::SyncSender<WorktreeName>,
926    ) -> Result<Vec<CleanupWorktreeWarning>, CleanupWorktreeError> {
927        let mut warnings = Vec::new();
928
929        let worktrees = self.get_worktrees()?;
930
931        let config: Option<WorktreeRootConfig> = config::read_worktree_root_config(directory)
932            .map_err(|e| <config::Error as Into<Error>>::into(e))?
933            .map(Into::into);
934
935        let default_branch = match config {
936            None => self.default_branch()?,
937            Some(ref config) => match config.persistent_branches.as_ref() {
938                None => self.default_branch()?,
939                Some(persistent_branches) => {
940                    if let Some(branch) = persistent_branches.first() {
941                        self.find_local_branch(branch)?.ok_or_else(|| {
942                            CleanupWorktreeError::BranchNotFound {
943                                branch_name: branch.to_owned(),
944                            }
945                        })?
946                    } else {
947                        self.default_branch()?
948                    }
949                }
950            },
951        };
952
953        let default_branch_name = default_branch
954            .name()
955            .map_err(|err| CleanupWorktreeError::BranchName(err))?;
956
957        for worktree in worktrees
958            .into_iter()
959            .filter(|worktree| worktree.name().as_str() != default_branch_name.as_str())
960            .filter(|worktree| match config {
961                None => true,
962                Some(ref config) => match config.persistent_branches.as_ref() {
963                    None => true,
964                    Some(branches) => !branches
965                        .iter()
966                        .any(|branch| branch.as_str() == worktree.name().as_str()),
967                },
968            })
969        {
970            let repo_dir = &directory.join(worktree.name().as_str());
971            if repo_dir.exists() {
972                match self.remove_worktree(
973                    directory,
974                    worktree.name(),
975                    Path::new(worktree.name().as_str()),
976                    false,
977                    config.as_ref(),
978                    &default_branch,
979                ) {
980                    Ok(()) => {
981                        #[expect(
982                            clippy::missing_panics_doc,
983                            reason = "this is a clear bug, cannot be recovered anyway"
984                        )]
985                        deletion_notify_channel
986                            .send(worktree.into_name())
987                            .expect("receiving channel must be open until we are done");
988                    }
989                    Err(error) => match error {
990                        WorktreeRemoveError::Changes(ref changes) => {
991                            warnings.push(CleanupWorktreeWarning {
992                                worktree_name: worktree.name().to_owned(),
993                                reason: CleanupWorktreeWarningReason::UncommittedChanges(*changes),
994                            });
995                        }
996                        WorktreeRemoveError::NotMerged { branch_name } => {
997                            warnings.push(CleanupWorktreeWarning {
998                                worktree_name: worktree.name().to_owned(),
999                                reason: CleanupWorktreeWarningReason::NotMerged { branch_name },
1000                            });
1001                        }
1002                        _ => return Err(CleanupWorktreeError::RemoveError(error)),
1003                    },
1004                }
1005            } else {
1006                warnings.push(CleanupWorktreeWarning {
1007                    worktree_name: worktree.name().to_owned(),
1008                    reason: CleanupWorktreeWarningReason::NoDirectory,
1009                });
1010            }
1011        }
1012        Ok(warnings)
1013    }
1014
1015    pub fn find_unmanaged_worktrees(&self, directory: &Path) -> Result<Vec<PathBuf>, Error> {
1016        let worktrees = self.get_worktrees()?;
1017
1018        let mut unmanaged_worktrees = Vec::new();
1019        for entry in directory.read_dir_utf8()? {
1020            let entry = entry?;
1021            #[expect(clippy::missing_panics_doc, reason = "see expect() message")]
1022            let dirname = entry
1023                .path()
1024                .strip_prefix(directory)
1025                // that unwrap() is safe as each entry is
1026                // guaranteed to be a subentry of &directory
1027                .expect("each entry is guaranteed to have the prefix");
1028
1029            let config: Option<WorktreeRootConfig> =
1030                config::read_worktree_root_config(directory)?.map(Into::into);
1031
1032            let guess_default_branch = || {
1033                self.0
1034                    .default_branch()
1035                    .map_err(|error| format!("Failed getting default branch: {error}"))?
1036                    .name()
1037                    .map_err(|error| format!("Failed getting default branch name: {error}"))
1038            };
1039
1040            let default_branch_name = match config {
1041                None => guess_default_branch().ok(),
1042                Some(ref config) => match config.persistent_branches.as_ref() {
1043                    None => guess_default_branch().ok(),
1044                    Some(persistent_branches) => {
1045                        if let Some(branch) = persistent_branches.first() {
1046                            Some(branch.clone())
1047                        } else {
1048                            guess_default_branch().ok()
1049                        }
1050                    }
1051                },
1052            };
1053
1054            if dirname == GIT_MAIN_WORKTREE_DIRECTORY {
1055                continue;
1056            }
1057
1058            if dirname == config::WORKTREE_CONFIG_FILE_NAME {
1059                continue;
1060            }
1061            if let Some(default_branch_name) = default_branch_name {
1062                if dirname == default_branch_name.as_str() {
1063                    continue;
1064                }
1065            }
1066            if !&worktrees
1067                .iter()
1068                .any(|worktree| worktree.name().as_str() == dirname)
1069            {
1070                unmanaged_worktrees.push(PathBuf::from(dirname));
1071            }
1072        }
1073        Ok(unmanaged_worktrees)
1074    }
1075
1076    pub fn get_worktrees(&self) -> Result<Vec<Worktree>, Error> {
1077        Ok(self
1078            .0
1079            .0
1080            .worktrees()?
1081            .iter()
1082            .map(|remote| {
1083                remote
1084                    .map_err(|_err| Error::WorktreeNameNotUtf8)
1085                    .and_then(|name| name.ok_or(Error::WorktreeNameEmpty))
1086            })
1087            .collect::<Result<Vec<_>, Error>>()?
1088            .into_iter()
1089            .map(Worktree::new)
1090            .collect::<Result<Vec<_>, WorktreeValidationError>>()?)
1091    }
1092
1093    pub fn remove_worktree(
1094        &self,
1095        base_dir: &Path,
1096        worktree_name: &WorktreeName,
1097        worktree_dir: &Path,
1098        force: bool,
1099        worktree_config: Option<&WorktreeRootConfig>,
1100        default_branch: &Branch,
1101    ) -> Result<(), WorktreeRemoveError> {
1102        //! We remove the worktree only under the following conditions (unless `force` is given):
1103        //!
1104        //! * It has no changes
1105        //! * If it has a remote tracking branch, it does not differ
1106        //! * It is merged into the default branch
1107        //! * It is merged into any "persistent branch"
1108        //!
1109        //! To clarify: Even if it is merged into local persistent branches, it will still not be
1110        //! deleted when the remote branch differs
1111
1112        let fullpath = base_dir.join(worktree_dir);
1113
1114        if !fullpath.exists() {
1115            return Err(WorktreeRemoveError::DoesNotExist(fullpath));
1116        }
1117        let worktree_repo = RepoHandle::open(&fullpath)?;
1118
1119        let local_branch = worktree_repo.head_branch()?;
1120
1121        let branch_name = local_branch.name()?;
1122
1123        if branch_name.as_str() != worktree_name.as_str() {
1124            return Err(WorktreeRemoveError::BranchNameMismatch {
1125                worktree_name: worktree_name.clone(),
1126                branch_name,
1127            });
1128        }
1129
1130        let branch = worktree_repo
1131            .find_local_branch(&branch_name)?
1132            .ok_or_else(|| WorktreeRemoveError::BranchNotFound(branch_name.clone()))?;
1133
1134        if !force {
1135            let status = worktree_repo.status(WorktreeSetup::NoWorktree)?;
1136
1137            if let Some(changes) = status.changes {
1138                return Err(WorktreeRemoveError::Changes(changes));
1139            }
1140
1141            let is_merged_into_default_branch = {
1142                let (ahead_of_default_branch, _behind) =
1143                    worktree_repo.graph_ahead_behind(&branch, default_branch)?;
1144
1145                ahead_of_default_branch == 0
1146            };
1147
1148            let mut is_merged_into_persistent_branch = false;
1149            let mut has_persistent_branches = false;
1150            if let Some(config) = worktree_config {
1151                if let Some(branches) = config.persistent_branches.as_ref() {
1152                    has_persistent_branches = true;
1153                    for persistent_branch in branches {
1154                        let persistent_branch = worktree_repo
1155                            .find_local_branch(persistent_branch)?
1156                            .ok_or_else(|| {
1157                                WorktreeRemoveError::BranchNotFound(branch_name.clone())
1158                            })?;
1159
1160                        let (ahead, _behind) =
1161                            worktree_repo.graph_ahead_behind(&branch, &persistent_branch)?;
1162
1163                        if ahead == 0 {
1164                            is_merged_into_persistent_branch = true;
1165                        }
1166                    }
1167                }
1168            }
1169
1170            let merged_into_default_or_persistent_branches = is_merged_into_default_branch
1171                || (has_persistent_branches && is_merged_into_persistent_branch);
1172
1173            if !merged_into_default_or_persistent_branches {
1174                return Err(WorktreeRemoveError::NotMerged { branch_name });
1175            }
1176
1177            if let Some(remote_branch) = branch.upstream()? {
1178                let (ahead, behind) = worktree_repo.graph_ahead_behind(&branch, &remote_branch)?;
1179
1180                if (ahead, behind) != (0, 0) {
1181                    return Err(WorktreeRemoveError::NotInSyncWithRemote { branch_name });
1182                }
1183            }
1184        }
1185
1186        // worktree_dir is a relative path, starting from base_dir. We walk it
1187        // upwards (from subdirectory to parent directories) and remove each
1188        // component, in case it is empty. Only the leaf directory can be
1189        // removed unconditionally (as it contains the worktree itself).
1190        if let Err(e) = std::fs::remove_dir_all(&fullpath) {
1191            return Err(WorktreeRemoveError::RemoveError {
1192                path: fullpath,
1193                error: e,
1194            });
1195        }
1196
1197        if let Some(current_dir) = worktree_dir.parent() {
1198            for current_dir in current_dir.ancestors() {
1199                let current_dir = base_dir.join(current_dir);
1200                if current_dir
1201                    .read_dir()
1202                    .map_err(|error| WorktreeRemoveError::ReadDirectoryError {
1203                        path: current_dir.clone(),
1204                        error,
1205                    })?
1206                    .next()
1207                    .is_none()
1208                {
1209                    if let Err(e) = std::fs::remove_dir(&current_dir) {
1210                        return Err(WorktreeRemoveError::RemoveError {
1211                            path: current_dir,
1212                            error: e,
1213                        });
1214                    }
1215                } else {
1216                    break;
1217                }
1218            }
1219        }
1220
1221        self.0.prune_worktree(worktree_name)?;
1222        branch.delete()?;
1223
1224        Ok(())
1225    }
1226
1227    fn new_worktree(
1228        &self,
1229        name: &str,
1230        directory: &Path,
1231        target_branch: &Branch,
1232    ) -> Result<(), Error> {
1233        self.0.0.worktree(
1234            name,
1235            directory.as_std_path(),
1236            Some(git2::WorktreeAddOptions::new().reference(Some(target_branch.as_reference()))),
1237        )?;
1238        Ok(())
1239    }
1240
1241    pub fn add_worktree(
1242        &self,
1243        name: &WorktreeName,
1244        tracking_selection: TrackingSelection,
1245    ) -> Result<Vec<Warning>, Error> {
1246        let mut warnings: Vec<Warning> = vec![];
1247
1248        let repo_directory = self.base_directory()?;
1249
1250        let remotes = self.as_repo().remotes()?;
1251
1252        let config: Option<WorktreeRootConfig> =
1253            config::read_worktree_root_config(repo_directory)?.map(Into::into);
1254
1255        if self.worktree_exists(name)? {
1256            return Err(Error::WorktreeAlreadyExists { name: name.clone() });
1257        }
1258
1259        let track_config = config.and_then(|config| config.track);
1260        let prefix = track_config
1261            .as_ref()
1262            .and_then(|track| track.default_remote_prefix.as_ref());
1263
1264        let default_tracking = track_config
1265            .as_ref()
1266            .map_or(TrackingDefault::NoTrack, |track| track.default);
1267
1268        let default_remote = track_config
1269            .as_ref()
1270            .map(|track| track.default_remote.clone());
1271
1272        // Note that we have to define all variables that borrow from `repo`
1273        // *first*, otherwise we'll receive "borrowed value does not live long
1274        // enough" errors. This is due to the `repo` reference inside `Worktree` that is
1275        // passed through each state type.
1276        //
1277        // The `commit` variable will be dropped at the end of the scope, together with
1278        // all worktree variables. It will be done in the opposite direction of
1279        // delcaration (FILO).
1280        //
1281        // So if we define `commit` *after* the respective worktrees, it will be dropped
1282        // first while still being borrowed by `Worktree`.
1283        let default_branch_head = self.as_repo().default_branch()?.commit_owned()?;
1284
1285        let worktree = NewWorktree::<Init>::new(self)
1286            .set_local_branch_name(&BranchName::new(name.as_str().to_owned()))?;
1287
1288        let get_remote_head = |remote_name: &RemoteName,
1289                               remote_branch_name: &BranchName|
1290         -> Result<Option<repo::Commit>, Error> {
1291            Ok(self
1292                .as_repo()
1293                .find_remote_branch(remote_name, remote_branch_name)?
1294                .map(|branch| branch.commit_owned())
1295                .transpose()?)
1296        };
1297
1298        let worktree = if worktree.local_branch_already_exists() {
1299            worktree.select_commit(None)
1300        } else {
1301            if let TrackingSelection::Explicit {
1302                ref remote_name,
1303                ref remote_branch_name,
1304            } = tracking_selection
1305            {
1306                worktree.select_commit(Some(
1307                    self.as_repo()
1308                        .find_remote_branch(remote_name, remote_branch_name)?
1309                        .map_or_else(
1310                            || Ok(default_branch_head),
1311                            |remote_branch| remote_branch.commit_owned(),
1312                        )?,
1313                ))
1314            } else {
1315                match remotes.len() {
1316                    0 => worktree.select_commit(Some(default_branch_head)),
1317                    1 => {
1318                        #[expect(clippy::indexing_slicing, reason = "checked for len() explicitly")]
1319                        let remote_name = &remotes[0];
1320                        let commit: Option<repo::Commit> = ({
1321                            if let Some(prefix) = prefix {
1322                                get_remote_head(
1323                                    remote_name,
1324                                    &BranchName::new(format!("{prefix}/{name}")),
1325                                )?
1326                            } else {
1327                                None
1328                            }
1329                        })
1330                        .or(get_remote_head(
1331                            remote_name,
1332                            &BranchName::new(name.as_str().to_owned()),
1333                        )?)
1334                        .or_else(|| Some(default_branch_head));
1335
1336                        worktree.select_commit(commit)
1337                    }
1338                    _ => {
1339                        let commit = if let Some(ref default_remote) = default_remote {
1340                            if let Some(prefix) = prefix {
1341                                self.as_repo()
1342                                    .find_remote_branch(default_remote, &BranchName::new(format!("{prefix}/{name}")))?.map(|remote_branch| remote_branch.commit_owned()).transpose()?
1343                            } else {
1344                                None
1345                            }
1346                            .or({
1347                                self.as_repo().find_remote_branch(default_remote, &BranchName::new(name.as_str().to_owned()))?.map(|remote_branch|remote_branch.commit_owned() ).transpose()?
1348                            })
1349                        } else {
1350                            None
1351                        }.or({
1352                            let mut commits = vec![];
1353                            for remote_name in &remotes {
1354                                let remote_head: Option<repo::Commit> = ({
1355                                    if let Some(prefix) = prefix {
1356                                        self.as_repo().find_remote_branch(
1357                                            remote_name,
1358                                            &BranchName::new(format!("{prefix}/{name}")),
1359                                        )?.map(|remote_branch| remote_branch.commit_owned()).transpose()?
1360                                    } else {
1361                                        None
1362                                    }
1363                                })
1364                                .or({
1365                                    self.as_repo().find_remote_branch(remote_name, &BranchName::new(name.as_str().to_owned()))?.map(|remote_branch|remote_branch.commit_owned()).transpose()?
1366                                })
1367                                .or(None);
1368                                commits.push(remote_head);
1369                            }
1370
1371                            let mut commits = commits
1372                                .into_iter()
1373                                .flatten()
1374                                // have to collect first because the `flatten()` return
1375                                // typedoes not implement `windows()`
1376                                .collect::<Vec<repo::Commit>>();
1377                            // `flatten()` takes care of `None` values here. If all
1378                            // remotes return None for the branch, we do *not* abort, we
1379                            // continue!
1380                            if commits.is_empty() {
1381                                Some(default_branch_head)
1382                            } else if commits.len() == 1 {
1383                                Some(commits.swap_remove(0))
1384                            } else if commits.windows(2).any(
1385                                #[expect(
1386                                    clippy::missing_asserts_for_indexing,
1387                                    clippy::indexing_slicing,
1388                                    reason = "windows function always returns two elements"
1389                                )]
1390                                |window| {
1391                                    let c1 = &window[0];
1392                                    let c2 = &window[1];
1393                                    (*c1).id().hex_string() != (*c2).id().hex_string()
1394                                }) {
1395                                warnings.push(
1396                                    // TODO this should also include the branch
1397                                    // name. BUT: the branch name may be different
1398                                    // between the remotes. Let's just leave it
1399                                    // until I get around to fix that inconsistency
1400                                    // (see module-level doc about), which might be
1401                                    // never, as it's such a rare edge case.
1402                                    Warning("Branch exists on multiple remotes, but they deviate. Selecting default branch instead".to_owned())
1403                                );
1404                                Some(default_branch_head)
1405                            } else {
1406                                Some(commits.swap_remove(0))
1407                            }
1408                        });
1409                        worktree.select_commit(commit)
1410                    }
1411                }
1412            }
1413        };
1414
1415        let worktree = worktree.set_remote_tracking_branch(match tracking_selection {
1416            TrackingSelection::Disabled => None,
1417            TrackingSelection::Explicit {
1418                remote_name,
1419                remote_branch_name,
1420            } => {
1421                Some(RemoteTrackingBranch {
1422                    remote_name,
1423                    remote_branch_name,
1424                    prefix: None, // Always disable prefixing when explicitly given --track
1425                })
1426            }
1427            TrackingSelection::Automatic => {
1428                if default_tracking == TrackingDefault::NoTrack {
1429                    None
1430                } else {
1431                    match remotes.len() {
1432                        0 => None,
1433                        1 =>
1434                        {
1435                            #[expect(
1436                                clippy::indexing_slicing,
1437                                reason = "checked for len() explicitly"
1438                            )]
1439                            Some(RemoteTrackingBranch {
1440                                remote_name: remotes[0].clone(),
1441                                remote_branch_name: BranchName::new(name.as_str().to_owned()),
1442                                prefix: prefix.cloned(),
1443                            })
1444                        }
1445                        _ => default_remote.map(|default_remote| RemoteTrackingBranch {
1446                            remote_name: default_remote,
1447                            remote_branch_name: BranchName::new(name.as_str().to_owned()),
1448                            prefix: prefix.cloned(),
1449                        }),
1450                    }
1451                }
1452            }
1453        });
1454
1455        worktree.create(repo_directory)?;
1456
1457        Ok(warnings)
1458    }
1459}