grm/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::{fmt, path::Path};
210
211use thiserror::Error;
212
213use super::{BranchName, RemoteName, Warning, config, repo};
214
215pub const GIT_MAIN_WORKTREE_DIRECTORY: &str = ".git-main-working-tree";
216
217struct Init;
218
219enum LocalBranchInfo<'a> {
220 NoBranch,
221 Branch(repo::Branch<'a>),
222}
223
224struct WithLocalBranchName<'a> {
225 local_branch_name: BranchName,
226 local_branch: LocalBranchInfo<'a>,
227}
228
229struct WithLocalTargetSelected<'a> {
230 local_branch_name: BranchName,
231 local_branch: Option<repo::Branch<'a>>,
232 target_commit: Option<Box<repo::Commit<'a>>>,
233}
234
235struct WithRemoteTrackingBranch<'a> {
236 local_branch_name: BranchName,
237 local_branch: Option<repo::Branch<'a>>,
238 target_commit: Option<Box<repo::Commit<'a>>>,
239 remote_tracking_branch: Option<(RemoteName, BranchName)>,
240 prefix: Option<String>,
241}
242
243struct Worktree<'a, S: WorktreeState> {
244 repo: &'a repo::RepoHandle,
245 extra: S,
246}
247
248impl<'a> WithLocalBranchName<'a> {
249 fn new(name: &BranchName, worktree: &Worktree<'a, Init>) -> Result<Self, Error> {
250 Ok(Self {
251 local_branch_name: name.clone(),
252 local_branch: {
253 let branch = worktree.repo.find_local_branch(name)?;
254 match branch {
255 Some(branch) => LocalBranchInfo::Branch(branch),
256 None => LocalBranchInfo::NoBranch,
257 }
258 },
259 })
260 }
261}
262
263trait WorktreeState {}
264
265impl WorktreeState for Init {}
266impl WorktreeState for WithLocalBranchName<'_> {}
267impl WorktreeState for WithLocalTargetSelected<'_> {}
268impl WorktreeState for WithRemoteTrackingBranch<'_> {}
269
270impl<'a> Worktree<'a, Init> {
271 fn new(repo: &'a repo::RepoHandle) -> Self {
272 Self {
273 repo,
274 extra: Init {},
275 }
276 }
277
278 fn set_local_branch_name(
279 self,
280 name: &BranchName,
281 ) -> Result<Worktree<'a, WithLocalBranchName<'a>>, Error> {
282 Ok(Worktree::<WithLocalBranchName> {
283 repo: self.repo,
284 extra: WithLocalBranchName::new(name, &self)?,
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(branch.ok());
298 // }
299 // }
300
301 fn local_branch_already_exists(&self) -> bool {
302 matches!(
303 self.extra.local_branch,
304 LocalBranchInfo::Branch(ref _branch)
305 )
306 }
307
308 fn select_commit(
309 self,
310 commit: Option<Box<repo::Commit<'b>>>,
311 ) -> Worktree<'a, WithLocalTargetSelected<'b>> {
312 Worktree::<'a, WithLocalTargetSelected> {
313 repo: self.repo,
314 extra: WithLocalTargetSelected::<'b> {
315 local_branch_name: self.extra.local_branch_name,
316 // As we just called `check_local_branch`, we can be sure that
317 // `self.extra.local_branch` is set to some `Some` value
318 local_branch: match self.extra.local_branch {
319 LocalBranchInfo::NoBranch => None,
320 LocalBranchInfo::Branch(branch) => Some(branch),
321 },
322 target_commit: commit,
323 },
324 }
325 }
326}
327
328impl<'a> Worktree<'a, WithLocalTargetSelected<'a>> {
329 fn set_remote_tracking_branch(
330 self,
331 branch: Option<(&RemoteName, &BranchName)>,
332 prefix: Option<&str>,
333 ) -> Worktree<'a, WithRemoteTrackingBranch<'a>> {
334 Worktree::<WithRemoteTrackingBranch> {
335 repo: self.repo,
336 extra: WithRemoteTrackingBranch {
337 local_branch_name: self.extra.local_branch_name,
338 local_branch: self.extra.local_branch,
339 target_commit: self.extra.target_commit,
340 remote_tracking_branch: branch.map(|(s1, s2)| (s1.clone(), s2.clone())),
341 prefix: prefix.map(ToOwned::to_owned),
342 },
343 }
344 }
345}
346
347impl<'a> Worktree<'a, WithRemoteTrackingBranch<'a>> {
348 fn create(self, directory: &Path) -> Result<Option<Vec<Warning>>, Error> {
349 let mut warnings: Vec<Warning> = vec![];
350
351 let mut branch = if let Some(branch) = self.extra.local_branch {
352 branch
353 } else {
354 self.repo.create_branch(
355 &self.extra.local_branch_name,
356 // TECHDEBT
357 // We must not call this with `Some()` without a valid target.
358 // I'm sure this can be improved, just not sure how.
359 &self
360 .extra
361 .target_commit
362 .expect("target_commit must not be empty"),
363 )?
364 };
365
366 if let Some((remote_name, remote_branch_name)) = self.extra.remote_tracking_branch {
367 let remote_branch_with_prefix = if let Some(ref prefix) = self.extra.prefix {
368 self.repo
369 .find_remote_branch(
370 &remote_name,
371 &BranchName::new(format!("{prefix}/{remote_branch_name}")),
372 )
373 .ok()
374 } else {
375 None
376 };
377
378 let remote_branch_without_prefix = self
379 .repo
380 .find_remote_branch(&remote_name, &remote_branch_name)
381 .ok();
382
383 let remote_branch = if let Some(ref _prefix) = self.extra.prefix {
384 remote_branch_with_prefix
385 } else {
386 remote_branch_without_prefix
387 };
388
389 if let Some(remote_branch) = remote_branch {
390 if branch.commit()?.id().hex_string() != remote_branch.commit()?.id().hex_string() {
391 warnings.push(Warning(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)));
392 }
393
394 branch.set_upstream(&remote_name, &remote_branch.basename()?)?;
395 } else {
396 let Some(mut remote) = self.repo.find_remote(&remote_name)? else {
397 return Err(Error::RemoteNotFound { name: remote_name });
398 };
399
400 if !remote.is_pushable()? {
401 return Err(Error::RemoteNotPushable { name: remote_name });
402 }
403
404 if let Some(prefix) = self.extra.prefix {
405 remote.push(
406 &self.extra.local_branch_name,
407 &BranchName::new(format!("{prefix}/{remote_branch_name}")),
408 self.repo,
409 )?;
410
411 branch.set_upstream(
412 &remote_name,
413 &BranchName::new(format!("{prefix}/{remote_branch_name}")),
414 )?;
415 } else {
416 remote.push(
417 &self.extra.local_branch_name,
418 &remote_branch_name,
419 self.repo,
420 )?;
421
422 branch.set_upstream(&remote_name, &remote_branch_name)?;
423 }
424 }
425 }
426
427 let branch_name = self.extra.local_branch_name.into_string();
428 // We have to create subdirectories first, otherwise adding the worktree
429 // will fail
430 if branch_name.contains('/') {
431 let path = Path::new(&branch_name);
432 if let Some(base) = path.parent() {
433 // This is a workaround of a bug in libgit2 (?)
434 //
435 // When *not* doing this, we will receive an error from the
436 // `Repository::worktree()` like this:
437 //
438 // > failed to make directory '/{repo}/.git-main-working-tree/worktrees/dir/test
439 //
440 // This is a discrepancy between the behavior of libgit2 and the
441 // git CLI when creating worktrees with slashes:
442 //
443 // The git CLI will create the worktree's configuration directory
444 // inside {git_dir}/worktrees/{last_path_component}. Look at this:
445 //
446 // ```
447 // $ git worktree add 1/2/3 -b 1/2/3
448 // $ ls .git/worktrees
449 // 3
450 // ```
451 //
452 // Interesting: When adding a worktree with a different name but the
453 // same final path component, git starts adding a counter suffix to
454 // the worktree directories:
455 //
456 // ```
457 // $ git worktree add 1/3/3 -b 1/3/3
458 // $ git worktree add 1/4/3 -b 1/4/3
459 // $ ls .git/worktrees
460 // 3
461 // 31
462 // 32
463 // ```
464 //
465 // I *guess* that the mapping back from the worktree directory under .git to the
466 // actual worktree directory is done via the `gitdir` file
467 // inside `.git/worktrees/{worktree}. This means that the actual
468 // directory would not matter. You can verify this by
469 // just renaming it:
470 //
471 // ```
472 // $ mv .git/worktrees/3 .git/worktrees/foobar
473 // $ git worktree list
474 // /tmp/ fcc8a2a7 [master]
475 // /tmp/1/2/3 fcc8a2a7 [1/2/3]
476 // /tmp/1/3/3 fcc8a2a7 [1/3/3]
477 // /tmp/1/4/3 fcc8a2a7 [1/4/3]
478 // ```
479 //
480 // => Still works
481 //
482 // Anyway, libgit2 does not do this: It tries to create the worktree
483 // directory inside .git with the exact name of the worktree, including
484 // any slashes. It should be this code:
485 //
486 // https://github.com/libgit2/libgit2/blob/f98dd5438f8d7bfd557b612fdf1605b1c3fb8eaf/src/libgit2/worktree.c#L346
487 //
488 // As a workaround, we can create the base directory manually for now.
489 //
490 // Tracking upstream issue: https://github.com/libgit2/libgit2/issues/6327
491 std::fs::create_dir_all(
492 directory
493 .join(GIT_MAIN_WORKTREE_DIRECTORY)
494 .join("worktrees")
495 .join(base),
496 )?;
497 std::fs::create_dir_all(base)?;
498 }
499 }
500
501 self.repo
502 .new_worktree(&branch_name, &directory.join(&branch_name), &branch)?;
503
504 Ok(if warnings.is_empty() {
505 None
506 } else {
507 Some(warnings)
508 })
509 }
510}
511
512#[derive(Debug, Clone, PartialEq, Eq)]
513pub struct WorktreeName(String);
514
515impl fmt::Display for WorktreeName {
516 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
517 write!(f, "{}", self.0)
518 }
519}
520
521impl WorktreeName {
522 pub fn new(from: String) -> Self {
523 Self(from)
524 }
525
526 pub fn as_str(&self) -> &str {
527 &self.0
528 }
529
530 pub fn into_string(self) -> String {
531 self.0
532 }
533}
534
535#[derive(Debug, Error)]
536enum WorktreeValidationErrorReason {
537 #[error("cannot start or end with a slash")]
538 SlashAtStartOrEnd,
539 #[error("cannot contain two consecutive slashes")]
540 ConsecutiveSlashes,
541 #[error("cannot contain whitespace")]
542 ContainsWhitespace,
543}
544
545#[derive(Debug, Error)]
546#[error("invalid worktree name \"{}\": {}", .name, .reason)]
547pub struct WorktreeValidationError {
548 name: WorktreeName,
549 reason: WorktreeValidationErrorReason,
550}
551
552/// A branch name must never start or end with a slash, and it cannot have two
553/// consecutive slashes
554fn validate_worktree_name(name: &WorktreeName) -> Result<(), WorktreeValidationError> {
555 let name = name.as_str();
556
557 if name.starts_with('/') || name.ends_with('/') {
558 return Err(WorktreeValidationError {
559 name: WorktreeName::new(name.to_owned()),
560 reason: WorktreeValidationErrorReason::SlashAtStartOrEnd,
561 });
562 }
563
564 if name.contains("//") {
565 return Err(WorktreeValidationError {
566 name: WorktreeName::new(name.to_owned()),
567 reason: WorktreeValidationErrorReason::ConsecutiveSlashes,
568 });
569 }
570
571 if name.contains(char::is_whitespace) {
572 return Err(WorktreeValidationError {
573 name: WorktreeName::new(name.to_owned()),
574 reason: WorktreeValidationErrorReason::ContainsWhitespace,
575 });
576 }
577
578 Ok(())
579}
580
581#[derive(Debug, Error)]
582pub enum Error {
583 #[error(transparent)]
584 Repo(#[from] repo::Error),
585 #[error(transparent)]
586 Config(#[from] config::Error),
587 #[error(transparent)]
588 InvalidWorktreeName(#[from] WorktreeValidationError),
589 #[error("Remote \"{name}\" not found", name = .name)]
590 RemoteNotFound { name: RemoteName },
591 #[error("Cannot push to non-pushable remote \"{name}\"", name = .name)]
592 RemoteNotPushable { name: RemoteName },
593 #[error(transparent)]
594 Io(#[from] std::io::Error),
595 #[error("Current directory does not contain a worktree setup")]
596 NotAWorktreeSetup,
597 #[error("Worktree {} already exists", .name)]
598 WorktreeAlreadyExists { name: WorktreeName },
599}
600
601// TECHDEBT
602//
603// Instead of opening the repo & reading configuration inside the function, it
604// should be done by the caller and given as a parameter
605pub fn add_worktree(
606 directory: &Path,
607 name: &WorktreeName,
608 track: Option<(RemoteName, BranchName)>,
609 no_track: bool,
610) -> Result<Option<Vec<Warning>>, Error> {
611 let mut warnings: Vec<Warning> = vec![];
612
613 validate_worktree_name(name)?;
614
615 let repo = repo::RepoHandle::open(directory, true).map_err(|error| match error {
616 repo::Error::NotFound => Error::NotAWorktreeSetup,
617 _ => error.into(),
618 })?;
619
620 let remotes = &repo.remotes()?;
621
622 let config: Option<repo::WorktreeRootConfig> =
623 config::read_worktree_root_config(directory)?.map(Into::into);
624
625 if repo.find_worktree(name).is_ok() {
626 return Err(Error::WorktreeAlreadyExists { name: name.clone() });
627 }
628
629 let track_config = config.and_then(|config| config.track);
630 let prefix = track_config
631 .as_ref()
632 .and_then(|track| track.default_remote_prefix.as_ref());
633 let enable_tracking = track_config.as_ref().is_some_and(|track| track.default);
634 let default_remote = track_config
635 .as_ref()
636 .map(|track| track.default_remote.clone());
637
638 // Note that we have to define all variables that borrow from `repo`
639 // *first*, otherwise we'll receive "borrowed value does not live long
640 // enough" errors. This is due to the `repo` reference inside `Worktree` that is
641 // passed through each state type.
642 //
643 // The `commit` variable will be dropped at the end of the scope, together with
644 // all worktree variables. It will be done in the opposite direction of
645 // delcaration (FILO).
646 //
647 // So if we define `commit` *after* the respective worktrees, it will be dropped
648 // first while still being borrowed by `Worktree`.
649 let default_branch_head = repo.default_branch()?.commit_owned()?;
650
651 let worktree = Worktree::<Init>::new(&repo)
652 .set_local_branch_name(&BranchName::new(name.as_str().to_owned()))?;
653
654 let get_remote_head = |remote_name: &RemoteName,
655 remote_branch_name: &BranchName|
656 -> Result<Option<Box<repo::Commit>>, Error> {
657 if let Ok(remote_branch) = repo.find_remote_branch(remote_name, remote_branch_name) {
658 Ok(Some(Box::new(remote_branch.commit_owned()?)))
659 } else {
660 Ok(None)
661 }
662 };
663
664 let worktree = if worktree.local_branch_already_exists() {
665 worktree.select_commit(None)
666 } else {
667 #[expect(
668 clippy::pattern_type_mismatch,
669 reason = "i cannot get this to work properly, but it's fine as it is"
670 )]
671 if let Some((remote_name, remote_branch_name)) =
672 if no_track { None } else { track.as_ref() }
673 {
674 if let Ok(remote_branch) = repo.find_remote_branch(remote_name, remote_branch_name) {
675 worktree.select_commit(Some(Box::new(remote_branch.commit_owned()?)))
676 } else {
677 worktree.select_commit(Some(Box::new(default_branch_head)))
678 }
679 } else {
680 match remotes.len() {
681 0 => worktree.select_commit(Some(Box::new(default_branch_head))),
682 1 => {
683 #[expect(clippy::indexing_slicing, reason = "checked for len() explicitly")]
684 let remote_name = &remotes[0];
685 let commit: Option<Box<repo::Commit>> = ({
686 if let Some(prefix) = prefix {
687 get_remote_head(
688 remote_name,
689 &BranchName::new(format!("{prefix}/{name}")),
690 )?
691 } else {
692 None
693 }
694 })
695 .or(get_remote_head(
696 remote_name,
697 &BranchName::new(name.as_str().to_owned()),
698 )?)
699 .or_else(|| Some(Box::new(default_branch_head)));
700
701 worktree.select_commit(commit)
702 }
703 _ => {
704 let commit = if let Some(ref default_remote) = default_remote {
705 if let Some(prefix) = prefix {
706 if let Ok(remote_branch) = repo
707 .find_remote_branch(default_remote, &BranchName::new(format!("{prefix}/{name}")))
708 {
709 Some(Box::new(remote_branch.commit_owned()?))
710 } else {
711 None
712 }
713 } else {
714 None
715 }
716 .or({
717 if let Ok(remote_branch) =
718 repo.find_remote_branch(default_remote, &BranchName::new(name.as_str().to_owned()))
719 {
720 Some(Box::new(remote_branch.commit_owned()?))
721 } else {
722 None
723 }
724 })
725 } else {
726 None
727 }.or({
728 let mut commits = vec![];
729 for remote_name in remotes {
730 let remote_head: Option<Box<repo::Commit>> = ({
731 if let Some(prefix) = prefix {
732 if let Ok(remote_branch) = repo.find_remote_branch(
733 remote_name,
734 &BranchName::new(format!("{prefix}/{name}")),
735 ) {
736 Some(Box::new(remote_branch.commit_owned()?))
737 } else {
738 None
739 }
740 } else {
741 None
742 }
743 })
744 .or({
745 if let Ok(remote_branch) =
746 repo.find_remote_branch(remote_name, &BranchName::new(name.as_str().to_owned()))
747 {
748 Some(Box::new(remote_branch.commit_owned()?))
749 } else {
750 None
751 }
752 })
753 .or(None);
754 commits.push(remote_head);
755 }
756
757 let mut commits = commits
758 .into_iter()
759 .flatten()
760 // have to collect first because the `flatten()` return
761 // typedoes not implement `windows()`
762 .collect::<Vec<Box<repo::Commit>>>();
763 // `flatten()` takes care of `None` values here. If all
764 // remotes return None for the branch, we do *not* abort, we
765 // continue!
766 if commits.is_empty() {
767 Some(Box::new(default_branch_head))
768 } else if commits.len() == 1 {
769 Some(commits.swap_remove(0))
770 } else if commits.windows(2).any(
771 #[expect(
772 clippy::missing_asserts_for_indexing,
773 clippy::indexing_slicing,
774 reason = "windows function always returns two elements"
775 )]
776 |window| {
777 let c1 = &window[0];
778 let c2 = &window[1];
779 (*c1).id().hex_string() != (*c2).id().hex_string()
780 }) {
781 warnings.push(
782 // TODO this should also include the branch
783 // name. BUT: the branch name may be different
784 // between the remotes. Let's just leave it
785 // until I get around to fix that inconsistency
786 // (see module-level doc about), which might be
787 // never, as it's such a rare edge case.
788 Warning("Branch exists on multiple remotes, but they deviate. Selecting default branch instead".to_owned())
789 );
790 Some(Box::new(default_branch_head))
791 } else {
792 Some(commits.swap_remove(0))
793 }
794 });
795 worktree.select_commit(commit)
796 }
797 }
798 }
799 };
800
801 let worktree = if no_track {
802 worktree.set_remote_tracking_branch(None, prefix.map(String::as_str))
803 } else if let Some((remote_name, remote_branch_name)) = track {
804 worktree.set_remote_tracking_branch(
805 Some((&remote_name, &remote_branch_name)),
806 None, // Always disable prefixing when explicitly given --track
807 )
808 } else if !enable_tracking {
809 worktree.set_remote_tracking_branch(None, prefix.map(String::as_str))
810 } else {
811 match remotes.len() {
812 0 => worktree.set_remote_tracking_branch(None, prefix.map(String::as_str)),
813 1 =>
814 {
815 #[expect(clippy::indexing_slicing, reason = "checked for len() explicitly")]
816 worktree.set_remote_tracking_branch(
817 Some((&remotes[0], &BranchName::new(name.as_str().to_owned()))),
818 prefix.map(String::as_str),
819 )
820 }
821 _ => {
822 if let Some(default_remote) = default_remote {
823 worktree.set_remote_tracking_branch(
824 Some((&default_remote, &BranchName::new(name.as_str().to_owned()))),
825 prefix.map(String::as_str),
826 )
827 } else {
828 worktree.set_remote_tracking_branch(None, prefix.map(String::as_str))
829 }
830 }
831 }
832 };
833
834 worktree.create(directory)?;
835
836 Ok(if warnings.is_empty() {
837 None
838 } else {
839 Some(warnings)
840 })
841}
842
843#[cfg(test)]
844mod tests {
845 use super::*;
846
847 #[test]
848 fn invalid_worktree_names() {
849 assert!(
850 add_worktree(
851 Path::new("/tmp/"),
852 &WorktreeName::new("/leadingslash".to_owned()),
853 None,
854 false
855 )
856 .is_err()
857 );
858 assert!(
859 add_worktree(
860 Path::new("/tmp/"),
861 &WorktreeName::new("trailingslash/".to_owned()),
862 None,
863 false
864 )
865 .is_err()
866 );
867 assert!(
868 add_worktree(
869 Path::new("/tmp/"),
870 &WorktreeName::new("//".to_owned()),
871 None,
872 false
873 )
874 .is_err()
875 );
876 assert!(
877 add_worktree(
878 Path::new("/tmp/"),
879 &WorktreeName::new("test//test".to_owned()),
880 None,
881 false
882 )
883 .is_err()
884 );
885 assert!(
886 add_worktree(
887 Path::new("/tmp/"),
888 &WorktreeName::new("test test".to_owned()),
889 None,
890 false
891 )
892 .is_err()
893 );
894 assert!(
895 add_worktree(
896 Path::new("/tmp/"),
897 &WorktreeName::new("test\ttest".to_owned()),
898 None,
899 false
900 )
901 .is_err()
902 );
903 }
904}