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}