git_repos/
worktree.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?
209use std::{cell::RefCell, path::Path};
210
211// use super::output::*;
212use super::repo;
213
214pub const GIT_MAIN_WORKTREE_DIRECTORY: &str = ".git-main-working-tree";
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn invalid_worktree_names() {
222        assert!(add_worktree(Path::new("/tmp/"), "/leadingslash", None, false).is_err());
223        assert!(add_worktree(Path::new("/tmp/"), "trailingslash/", None, false).is_err());
224        assert!(add_worktree(Path::new("/tmp/"), "//", None, false).is_err());
225        assert!(add_worktree(Path::new("/tmp/"), "test//test", None, false).is_err());
226        assert!(add_worktree(Path::new("/tmp/"), "test test", None, false).is_err());
227        assert!(add_worktree(Path::new("/tmp/"), "test\ttest", None, false).is_err());
228    }
229}
230
231struct Init;
232
233struct WithLocalBranchName<'a> {
234    local_branch_name: String,
235    /// Outer option: Is there a computed value?
236    /// Inner option: Is there actually a branch?
237    ///
238    /// None => No computed value yet
239    /// Some(None) => No branch
240    /// Some(Some(_)) => Branch
241    local_branch: RefCell<Option<Option<repo::Branch<'a>>>>,
242}
243
244struct WithLocalTargetSelected<'a> {
245    local_branch_name: String,
246    local_branch: Option<repo::Branch<'a>>,
247    target_commit: Option<Box<repo::Commit<'a>>>,
248}
249
250struct WithRemoteTrackingBranch<'a> {
251    local_branch_name: String,
252    local_branch: Option<repo::Branch<'a>>,
253    target_commit: Option<Box<repo::Commit<'a>>>,
254    remote_tracking_branch: Option<(String, String)>,
255    prefix: Option<String>,
256}
257
258struct Worktree<'a, S: WorktreeState> {
259    repo: &'a repo::RepoHandle,
260    extra: S,
261}
262
263impl<'a> WithLocalBranchName<'a> {
264    fn new(name: String) -> Self {
265        Self { local_branch_name: name, local_branch: RefCell::new(None) }
266    }
267}
268
269trait WorktreeState {}
270
271impl WorktreeState for Init {}
272impl<'a> WorktreeState for WithLocalBranchName<'a> {}
273impl<'a> WorktreeState for WithLocalTargetSelected<'a> {}
274impl<'a> WorktreeState for WithRemoteTrackingBranch<'a> {}
275
276impl<'a> Worktree<'a, Init> {
277    fn new(repo: &'a repo::RepoHandle) -> Self {
278        Self { repo, extra: Init {} }
279    }
280
281    fn set_local_branch_name(self, name: &str) -> Worktree<'a, WithLocalBranchName<'a>> {
282        Worktree::<WithLocalBranchName> {
283            repo: self.repo,
284            extra: WithLocalBranchName::new(name.to_string()),
285        }
286    }
287}
288
289impl<'a, 'b> Worktree<'a, WithLocalBranchName<'b>>
290where
291    'a: 'b,
292{
293    fn check_local_branch(&self) {
294        let mut branchref = self.extra.local_branch.borrow_mut();
295        if branchref.is_none() {
296            let branch = self.repo.find_local_branch(&self.extra.local_branch_name);
297            *branchref = Some(if let Ok(branch) = branch { Some(branch) } else { None });
298        }
299    }
300
301    fn local_branch_already_exists(&self) -> bool {
302        if let Some(branch) = &*self.extra.local_branch.borrow() {
303            return branch.is_some();
304        }
305        self.check_local_branch();
306        // As we just called `check_local_branch`, we can be sure that
307        // `self.extra.local_branch` is set to some `Some` value
308        (*self.extra.local_branch.borrow()).as_ref().unwrap().is_some()
309    }
310
311    fn select_commit(
312        self,
313        commit: Option<Box<repo::Commit<'b>>>,
314    ) -> Worktree<'a, WithLocalTargetSelected<'b>> {
315        self.check_local_branch();
316
317        Worktree::<'a, WithLocalTargetSelected> {
318            repo: self.repo,
319            extra: WithLocalTargetSelected::<'b> {
320                local_branch_name: self.extra.local_branch_name,
321                // As we just called `check_local_branch`, we can be sure that
322                // `self.extra.local_branch` is set to some `Some` value
323                local_branch: self.extra.local_branch.into_inner().unwrap(),
324                target_commit: commit,
325            },
326        }
327    }
328}
329
330impl<'a> Worktree<'a, WithLocalTargetSelected<'a>> {
331    fn set_remote_tracking_branch(
332        self,
333        branch: Option<(&str, &str)>,
334        prefix: Option<&str>,
335    ) -> Worktree<'a, WithRemoteTrackingBranch<'a>> {
336        Worktree::<WithRemoteTrackingBranch> {
337            repo: self.repo,
338            extra: WithRemoteTrackingBranch {
339                local_branch_name: self.extra.local_branch_name,
340                local_branch: self.extra.local_branch,
341                target_commit: self.extra.target_commit,
342                remote_tracking_branch: branch.map(|(s1, s2)| (s1.to_string(), s2.to_string())),
343                prefix: prefix.map(|prefix| prefix.to_string()),
344            },
345        }
346    }
347}
348
349impl<'a> Worktree<'a, WithRemoteTrackingBranch<'a>> {
350    fn create(self, directory: &Path) -> Result<Option<Vec<String>>, String> {
351        let mut warnings: Vec<String> = vec![];
352
353        let mut branch = if let Some(branch) = self.extra.local_branch {
354            branch
355        } else {
356            self.repo.create_branch(
357                &self.extra.local_branch_name,
358                // TECHDEBT
359                // We must not call this with `Some()` without a valid target.
360                // I'm sure this can be improved, just not sure how.
361                &self.extra.target_commit.unwrap(),
362            )?
363        };
364
365        if let Some((remote_name, remote_branch_name)) = self.extra.remote_tracking_branch {
366            let remote_branch_with_prefix = if let Some(ref prefix) = self.extra.prefix {
367                if let Ok(remote_branch) = self
368                    .repo
369                    .find_remote_branch(&remote_name, &format!("{prefix}/{remote_branch_name}"))
370                {
371                    Some(remote_branch)
372                } else {
373                    None
374                }
375            } else {
376                None
377            };
378
379            let remote_branch_without_prefix = if let Ok(remote_branch) =
380                self.repo.find_remote_branch(&remote_name, &remote_branch_name)
381            {
382                Some(remote_branch)
383            } else {
384                None
385            };
386
387            let remote_branch = if let Some(ref _prefix) = self.extra.prefix {
388                remote_branch_with_prefix
389            } else {
390                remote_branch_without_prefix
391            };
392
393            match remote_branch {
394                Some(remote_branch) => {
395                    if branch.commit()?.id().hex_string()
396                        != remote_branch.commit()?.id().hex_string()
397                    {
398                        warnings.push(format!("The local branch \"{}\" and the remote branch \"{}/{}\" differ. Make sure to push/pull afterwards!", &self.extra.local_branch_name, &remote_name, &remote_branch_name));
399                    }
400
401                    branch.set_upstream(&remote_name, &remote_branch.basename()?)?;
402                },
403                None => {
404                    let mut remote = match self.repo.find_remote(&remote_name)? {
405                        Some(remote) => remote,
406                        None => return Err(format!("Remote \"{remote_name}\" not found")),
407                    };
408
409                    if !remote.is_pushable()? {
410                        return Err(format!("Cannot push to non-pushable remote \"{remote_name}\""));
411                    }
412
413                    if let Some(prefix) = self.extra.prefix {
414                        remote.push(
415                            &self.extra.local_branch_name,
416                            &format!("{}/{}", prefix, remote_branch_name),
417                            self.repo,
418                        )?;
419
420                        branch.set_upstream(
421                            &remote_name,
422                            &format!("{}/{}", prefix, remote_branch_name),
423                        )?;
424                    } else {
425                        remote.push(
426                            &self.extra.local_branch_name,
427                            &remote_branch_name,
428                            self.repo,
429                        )?;
430
431                        branch.set_upstream(&remote_name, &remote_branch_name)?;
432                    }
433                },
434            }
435        }
436
437        // We have to create subdirectories first, otherwise adding the worktree
438        // will fail
439        if self.extra.local_branch_name.contains('/') {
440            let path = Path::new(&self.extra.local_branch_name);
441            if let Some(base) = path.parent() {
442                // This is a workaround of a bug in libgit2 (?)
443                //
444                // When *not* doing this, we will receive an error from the `Repository::worktree()`
445                // like this:
446                //
447                // > failed to make directory '/{repo}/.git-main-working-tree/worktrees/dir/test
448                //
449                // This is a discrepancy between the behavior of libgit2 and the
450                // git CLI when creating worktrees with slashes:
451                //
452                // The git CLI will create the worktree's configuration directory
453                // inside {git_dir}/worktrees/{last_path_component}. Look at this:
454                //
455                // ```
456                // $ git worktree add 1/2/3 -b 1/2/3
457                // $ ls .git/worktrees
458                // 3
459                // ```
460                //
461                // Interesting: When adding a worktree with a different name but the
462                // same final path component, git starts adding a counter suffix to
463                // the worktree directories:
464                //
465                // ```
466                // $ git worktree add 1/3/3 -b 1/3/3
467                // $ git worktree add 1/4/3 -b 1/4/3
468                // $ ls .git/worktrees
469                // 3
470                // 31
471                // 32
472                // ```
473                //
474                // I *guess* that the mapping back from the worktree directory under .git to the actual
475                // worktree directory is done via the `gitdir` file inside `.git/worktrees/{worktree}.
476                // This means that the actual directory would not matter. You can verify this by
477                // just renaming it:
478                //
479                // ```
480                // $ mv .git/worktrees/3 .git/worktrees/foobar
481                // $ git worktree list
482                // /tmp/       fcc8a2a7 [master]
483                // /tmp/1/2/3  fcc8a2a7 [1/2/3]
484                // /tmp/1/3/3  fcc8a2a7 [1/3/3]
485                // /tmp/1/4/3  fcc8a2a7 [1/4/3]
486                // ```
487                //
488                // => Still works
489                //
490                // Anyway, libgit2 does not do this: It tries to create the worktree
491                // directory inside .git with the exact name of the worktree, including
492                // any slashes. It should be this code:
493                //
494                // https://github.com/libgit2/libgit2/blob/f98dd5438f8d7bfd557b612fdf1605b1c3fb8eaf/src/libgit2/worktree.c#L346
495                //
496                // As a workaround, we can create the base directory manually for now.
497                //
498                // Tracking upstream issue: https://github.com/libgit2/libgit2/issues/6327
499                std::fs::create_dir_all(
500                    directory.join(GIT_MAIN_WORKTREE_DIRECTORY).join("worktrees").join(base),
501                )
502                .map_err(|error| error.to_string())?;
503                std::fs::create_dir_all(base).map_err(|error| error.to_string())?;
504            }
505        }
506
507        self.repo.new_worktree(
508            &self.extra.local_branch_name,
509            &directory.join(&self.extra.local_branch_name),
510            &branch,
511        )?;
512
513        Ok(if warnings.is_empty() { None } else { Some(warnings) })
514    }
515}
516
517/// A branch name must never start or end with a slash, and it cannot have two
518/// consecutive slashes
519fn validate_worktree_name(name: &str) -> Result<(), String> {
520    if name.starts_with('/') || name.ends_with('/') {
521        return Err(format!("Invalid worktree name: {}. It cannot start or end with a slash", name));
522    }
523
524    if name.contains("//") {
525        return Err(format!(
526            "Invalid worktree name: {}. It cannot contain two consecutive slashes",
527            name
528        ));
529    }
530
531    if name.contains(char::is_whitespace) {
532        return Err(format!("Invalid worktree name: {}. It cannot contain whitespace", name));
533    }
534
535    Ok(())
536}
537
538// TECHDEBT
539//
540// Instead of opening the repo & reading configuration inside the function, it
541// should be done by the caller and given as a parameter
542pub fn add_worktree(
543    directory: &Path,
544    name: &str,
545    track: Option<(&str, &str)>,
546    no_track: bool,
547) -> Result<Option<Vec<String>>, String> {
548    let mut warnings: Vec<String> = vec![];
549
550    validate_worktree_name(name)?;
551
552    let repo = repo::RepoHandle::open(directory, true).map_err(|error| {
553        match error.kind {
554            repo::RepoErrorKind::NotFound => {
555                String::from("Current directory does not contain a worktree setup")
556            },
557            _ => format!("Error opening repo: {}", error),
558        }
559    })?;
560
561    let remotes = &repo.remotes()?;
562
563    let config = repo::read_worktree_root_config(directory)?;
564
565    if repo.find_worktree(name).is_ok() {
566        return Err(format!("Worktree {} already exists", &name));
567    }
568
569    let track_config = config.and_then(|config| config.track);
570    let prefix = track_config.as_ref().and_then(|track| track.default_remote_prefix.as_ref());
571    let enable_tracking = track_config.as_ref().map_or(false, |track| track.default);
572    let default_remote = track_config.as_ref().map(|track| track.default_remote.clone());
573
574    // Note that we have to define all variables that borrow from `repo`
575    // *first*, otherwise we'll receive "borrowed value does not live long
576    // enough" errors. This is due to the `repo` reference inside `Worktree` that is
577    // passed through each state type.
578    //
579    // The `commit` variable will be dropped at the end of the scope, together with all
580    // worktree variables. It will be done in the opposite direction of delcaration (FILO).
581    //
582    // So if we define `commit` *after* the respective worktrees, it will be dropped first while
583    // still being borrowed by `Worktree`.
584    let default_branch_head = repo.default_branch()?.commit_owned()?;
585
586    let worktree = Worktree::<Init>::new(&repo).set_local_branch_name(name);
587
588    let get_remote_head = |remote_name: &str,
589                           remote_branch_name: &str|
590     -> Result<Option<Box<repo::Commit>>, String> {
591        if let Ok(remote_branch) = repo.find_remote_branch(remote_name, remote_branch_name) {
592            Ok(Some(Box::new(remote_branch.commit_owned()?)))
593        } else {
594            Ok(None)
595        }
596    };
597
598    let worktree = if worktree.local_branch_already_exists() {
599        worktree.select_commit(None)
600    } else if let Some((remote_name, remote_branch_name)) = if no_track { None } else { track } {
601        if let Ok(remote_branch) = repo.find_remote_branch(remote_name, remote_branch_name) {
602            worktree.select_commit(Some(Box::new(remote_branch.commit_owned()?)))
603        } else {
604            worktree.select_commit(Some(Box::new(default_branch_head)))
605        }
606    } else {
607        match remotes.len() {
608            0 => worktree.select_commit(Some(Box::new(default_branch_head))),
609            1 => {
610                let remote_name = &remotes[0];
611                let commit: Option<Box<repo::Commit>> = ({
612                    if let Some(prefix) = prefix {
613                        get_remote_head(remote_name, &format!("{prefix}/{name}"))?
614                    } else {
615                        None
616                    }
617                })
618                .or(get_remote_head(remote_name, name)?)
619                .or_else(|| Some(Box::new(default_branch_head)));
620
621                worktree.select_commit(commit)
622            },
623            _ => {
624                let commit = if let Some(ref default_remote) = default_remote {
625                    if let Some(ref prefix) = prefix {
626                        if let Ok(remote_branch) = repo
627                            .find_remote_branch(default_remote, &format!("{prefix}/{name}"))
628                        {
629                            Some(Box::new(remote_branch.commit_owned()?))
630                        } else {
631                            None
632                        }
633                    } else {
634                        None
635                    }
636                    .or({
637                        if let Ok(remote_branch) =
638                            repo.find_remote_branch(default_remote, name)
639                        {
640                            Some(Box::new(remote_branch.commit_owned()?))
641                        } else {
642                            None
643                        }
644                    })
645                } else {
646                    None
647                }.or({
648                    let mut commits = vec![];
649                    for remote_name in remotes.iter() {
650                        let remote_head: Option<Box<repo::Commit>> = ({
651                            if let Some(ref prefix) = prefix {
652                                if let Ok(remote_branch) = repo.find_remote_branch(
653                                    remote_name,
654                                    &format!("{prefix}/{name}"),
655                                ) {
656                                    Some(Box::new(remote_branch.commit_owned()?))
657                                } else {
658                                    None
659                                }
660                            } else {
661                                None
662                            }
663                        })
664                        .or({
665                            if let Ok(remote_branch) =
666                                repo.find_remote_branch(remote_name, name)
667                            {
668                                Some(Box::new(remote_branch.commit_owned()?))
669                            } else {
670                                None
671                            }
672                        })
673                        .or(None);
674                        commits.push(remote_head);
675                    }
676
677                    let mut commits = commits
678                        .into_iter()
679                        .flatten()
680                        // have to collect first because the `flatten()` return
681                        // typedoes not implement `windows()`
682                        .collect::<Vec<Box<repo::Commit>>>();
683                    // `flatten()` takes care of `None` values here. If all
684                    // remotes return None for the branch, we do *not* abort, we
685                    // continue!
686                    if commits.is_empty() {
687                        Some(Box::new(default_branch_head))
688                    } else if commits.len() == 1 {
689                        Some(commits.swap_remove(0))
690                    } else if commits.windows(2).any(|window| {
691                        let c1 = &window[0];
692                        let c2 = &window[1];
693                        (*c1).id().hex_string() != (*c2).id().hex_string()
694                    }) {
695                        warnings.push(
696                            // TODO this should also include the branch
697                            // name. BUT: the branch name may be different
698                            // between the remotes. Let's just leave it
699                            // until I get around to fix that inconsistency
700                            // (see module-level doc about), which might be
701                            // never, as it's such a rare edge case.
702                            "Branch exists on multiple remotes, but they deviate. Selecting default branch instead".to_string()
703                        );
704                        Some(Box::new(default_branch_head))
705                    } else {
706                        Some(commits.swap_remove(0))
707                    }
708                });
709                worktree.select_commit(commit)
710            },
711        }
712    };
713
714    let worktree = if no_track {
715        worktree.set_remote_tracking_branch(None, prefix.map(|s| s.as_str()))
716    } else if let Some((remote_name, remote_branch_name)) = track {
717        worktree.set_remote_tracking_branch(
718            Some((remote_name, remote_branch_name)),
719            None, // Always disable prefixing when explicitly given --track
720        )
721    } else if !enable_tracking {
722        worktree.set_remote_tracking_branch(None, prefix.map(|s| s.as_str()))
723    } else {
724        match remotes.len() {
725            0 => worktree.set_remote_tracking_branch(None, prefix.map(|s| s.as_str())),
726            1 => {
727                worktree.set_remote_tracking_branch(
728                    Some((&remotes[0], name)),
729                    prefix.map(|s| s.as_str()),
730                )
731            },
732            _ => {
733                if let Some(default_remote) = default_remote {
734                    worktree.set_remote_tracking_branch(
735                        Some((&default_remote, name)),
736                        prefix.map(|s| s.as_str()),
737                    )
738                } else {
739                    worktree.set_remote_tracking_branch(None, prefix.map(|s| s.as_str()))
740                }
741            },
742        }
743    };
744
745    worktree.create(directory)?;
746
747    Ok(if warnings.is_empty() { None } else { Some(warnings) })
748}