diff --git a/src/grm/main.rs b/src/grm/main.rs index f502bd2..d3748cc 100644 --- a/src/grm/main.rs +++ b/src/grm/main.rs @@ -513,7 +513,14 @@ fn main() { track, action_args.no_track, ) { - Ok(_) => print_success(&format!("Worktree {} created", &action_args.name)), + Ok(warnings) => { + if let Some(warnings) = warnings { + for warning in warnings { + print_warning(&warning); + } + } + print_success(&format!("Worktree {} created", &action_args.name)); + } Err(error) => { print_error(&format!("Error creating worktree: {}", error)); process::exit(1); diff --git a/src/worktree.rs b/src/worktree.rs index 631aa79..abbdf61 100644 --- a/src/worktree.rs +++ b/src/worktree.rs @@ -1,30 +1,546 @@ +//! This handles worktrees for repositories. Some considerations to take care +//! of: +//! +//! * Which branch to check out / create +//! * Which commit to check out +//! * Whether to track a remote branch, and which +//! +//! There are a general rules. The main goal is to do the least surprising thing +//! in each situation, and to never change existing setups (e.g. tracking, +//! branch states) except when explicitly told to. In 99% of all cases, the +//! workflow will be quite straightforward. +//! +//! * The name of the worktree (and therefore the path) is **always** the same +//! as the name of the branch. +//! * Never modify existing local branches +//! * Only modify tracking branches for existing local branches if explicitly +//! requested +//! * By default, do not do remote operations. This means that we do no do any +//! tracking setup (but of course, the local branch can already have a +//! tracking branch set up, which will just be left alone) +//! * Be quite lax with finding a remote tracking branch (as using an existing +//! branch is most likely preferred to creating a new branch) +//! +//! There are a few different options that can be given: +//! +//! * Explicit track (`--track`) and explicit no-track (`--no-track`) +//! * A configuration may specify to enable tracking a remote branch by default +//! * A configuration may specify a prefix for remote branches +//! +//! # How to handle the local branch? +//! +//! That one is easy: If a branch with the desired name already exists, all is +//! well. If not, we create a new one. +//! +//! # Which commit should be checked out? +//! +//! The most imporant rule: If the local branch already existed, just leave it +//! as it is. Only if a new branch is created do we need to answer the question +//! which commit to set it to. Generally, we set the branch to whatever the +//! "default" branch of the repository is (something like "main" or "master"). +//! But there are a few cases where we can use remote branches to make the +//! result less surprising. +//! +//! First, if tracking is explicitly disabled, we still try to guess! But we +//! *do* ignore `--track`, as this is how it's done everywhere else. +//! +//! As an example: If `origin/foobar` exists and we run `grm worktree add foobar +//! --no-track`, we create a new worktree called `foobar` that's on the same +//! state as `origin/foobar` (but we will not set up tracking, see below). +//! +//! If tracking is explicitly requested to a certain state, we use that remote +//! branch. If it exists, easy. If not, no more guessing! +//! +//! Now, it's important to select the correct remote. In the easiest case, there +//! is only one remote, so we just use that one. If there is more than one +//! remote, we check whether there is a default remote configured via +//! `track.default_remote`. If yes, we use that one. If not, we have to do the +//! selection process below *for each of them*. If only one of them returns +//! some branch to track, we use that one. If more than one remote returns +//! information, we only use it if it's identical for each. Otherwise we bail, +//! as there is no point in guessing. +//! +//! The commit selection process looks like this: +//! +//! * If a prefix is specified in the configuration, we look for +//! `{remote}/{prefix}/{worktree_name}` +//! +//! * We look for `{remote}/{worktree_name}` (yes, this means that even when a +//! prefix is configured, we use a branch *without* a prefix if one with +//! prefix does not exist) +//! +//! Note that we may select different branches for different remotes when +//! prefixes is used. If remote1 has a branch with a prefix and remote2 only has +//! a branch *without* a prefix, we select them both when a prefix is used. This +//! could lead to the following situation: +//! +//! * There is `origin/prefix/foobar` and `remote2/foobar`, with different +//! states +//! * You set `track.default_prefix = "prefix"` (and no default remote!) +//! * You run `grm worktree add `prefix/foobar` +//! * Instead of just picking `origin/prefix/foobar`, grm will complain because +//! it also selected `remote2/foobar`. +//! +//! This is just emergent behaviour of the logic above. Fixing it would require +//! additional logic for that edge case. I assume that it's just so rare to get +//! that behaviour that it's acceptable for now. +//! +//! Now we either have a commit, we aborted, or we do not have commit. In the +//! last case, as stated above, we check out the "default" branch. +//! +//! # The remote tracking branch +//! +//! First, the only remote operations we do is branch creation! It's +//! unfortunately not possible to defer remote branch creation until the first +//! `git push`, which would be ideal. The remote tracking branch has to already +//! exist, so we have to do the equivalent of `git push --set-upstream` during +//! worktree creation. +//! +//! Whether (and which) remote branch to track works like this: +//! +//! * If `--no-track` is given, we never track a remote branch, except when +//! branch already has a tracking branch. So we'd be done already! +//! +//! * If `--track` is given, we always track this branch, regardless of anything +//! else. If the branch exists, cool, otherwise we create it. +//! +//! If neither is given, we only set up tracking if requested in the +//! configuration file (`track.default = true`) +//! +//! The rest of the process is similar to the commit selection above. The only +//! difference is the remote selection. If there is only one, we use it, as +//! before. Otherwise, we try to use `default_remote` from the configuration, if +//! available. If not, we do not set up a remote tracking branch. It works like +//! this: +//! +//! * If a prefix is specified in the configuration, we use +//! `{remote}/{prefix}/{worktree_name}` +//! +//! * If no prefix is specified in the configuration, we use +//! `{remote}/{worktree_name}` +//! +//! Now that we have a remote, we use the same process as above: +//! +//! * If a prefix is specified in the configuration, we use for +//! `{remote}/{prefix}/{worktree_name}` +//! * We use for `{remote}/{worktree_name}` +//! +//! --- +//! +//! All this means that in some weird situation, you may end up with the state +//! of a remote branch while not actually tracking that branch. This can only +//! happen in repositories with more than one remote. Imagine the following: +//! +//! The repository has two remotes (`remote1` and `remote2`) which have the +//! exact same remote state. But there is no `default_remote` in the +//! configuration (or no configuration at all). There is a remote branch +//! `foobar`. As both `remote1/foobar` and `remote2/foobar` as the same, the new +//! worktree will use that as the state of the new branch. But as `grm` cannot +//! tell which remote branch to track, it will not set up remote tracking. This +//! behaviour may be a bit confusing, but first, there is no good way to resolve +//! this, and second, the situation should be really rare (when having multiple +//! remotes, you would generally have a `default_remote` configured). +//! +//! # Implementation +//! +//! To reduce the chance of bugs, the implementation uses the [typestate +//! pattern](http://cliffle.com/blog/rust-typestate/). Here are the states we +//! are moving through linearily: +//! +//! * Init +//! * A local branch name is set +//! * A local commit to set the new branch to is selected +//! * A remote tracking branch is selected +//! * The new branch is created with all the required settings +//! +//! Don't worry about the lifetime stuff: There is only one single lifetime, as +//! everything (branches, commits) is derived from the single repo::Repo +//! instance +//! +//! # Testing +//! +//! There are two types of input to the tests: +//! +//! 1) The parameters passed to `grm`, either via command line or via +//! configuration file +//! 2) The circumstances in the repository and remotes +//! +//! ## Parameters +//! +//! * The name of the worktree +//! * Whether it contains slashes or not +//! * Whether it is invalid +//! * `--track` and `--no-track` +//! * Whether there is a configuration file and what it contains +//! * Whether `track.default` is enabled or disabled +//! * Whether `track.default_remote_prefix` is there or missing +//! * Whether `track.default_remote` is there or missing +//! * Whether that remote exists or not +//! +//! ## Situations +//! +//! ### The local branch +//! +//! * Whether the branch already exists +//! * Whether the branch has a remote tracking branch and whether it differs +//! from the desired tracking branch (i.e. `--track` or config) +//! +//! ### Remotes +//! +//! * How many remotes there are, if any +//! * If more than two remotes exist, whether their desired tracking branch +//! differs +//! +//! ### The remote tracking branch branch +//! +//! * Whether a remote branch with the same name as the worktree exists +//! * Whether a remote branch with the same name as the worktree plus prefix +//! exists +//! +//! ## Outcomes +//! +//! We have to check the following afterwards: +//! +//! * Does the worktree exist in the correct location? +//! * Does the local branch have the same name as the worktree? +//! * Does the local branch have the correct commit? +//! * Does the local branch track the correct remote branch? +//! * Does that remote branch also exist? +use std::cell::RefCell; use std::path::Path; -use super::output::*; +// use super::output::*; use super::repo; pub const GIT_MAIN_WORKTREE_DIRECTORY: &str = ".git-main-working-tree"; -// The logic about the base branch and the tracking branch is as follows: -// -// * If a branch with the same name does not exist and no track is given, use the default -// branch -// -// * If a branch with the same name exists and no track is given, use that -// -// * If a branch with the same name does not exist and track is given, use the -// local branch that tracks that branch -// -// * If a branch with the same name exists and track is given, use the locally -// existing branch. If the locally existing branch is not the local branch to -// the remote tracking branch, issue a warning -pub fn add_worktree( - directory: &Path, - name: &str, - track: Option<(&str, &str)>, - no_track: bool, -) -> Result<(), String> { - // A branch name must never start or end with a slash. Everything else is ok. +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn invalid_worktree_names() { + assert!(add_worktree(Path::new("/tmp/"), "/leadingslash", None, false).is_err()); + assert!(add_worktree(Path::new("/tmp/"), "trailingslash/", None, false).is_err()); + assert!(add_worktree(Path::new("/tmp/"), "//", None, false).is_err()); + assert!(add_worktree(Path::new("/tmp/"), "test//test", None, false).is_err()); + assert!(add_worktree(Path::new("/tmp/"), "test test", None, false).is_err()); + assert!(add_worktree(Path::new("/tmp/"), "test\ttest", None, false).is_err()); + } +} + +struct Init; + +struct WithLocalBranchName<'a> { + local_branch_name: String, + /// Outer option: Is there a computed value? + /// Inner option: Is there actually a branch? + /// + /// None => No computed value yet + /// Some(None) => No branch + /// Some(Some(_)) => Branch + local_branch: RefCell>>>, +} + +struct WithLocalTargetSelected<'a> { + local_branch_name: String, + local_branch: Option>, + target_commit: Option>>, +} + +struct WithRemoteTrackingBranch<'a> { + local_branch_name: String, + local_branch: Option>, + target_commit: Option>>, + remote_tracking_branch: Option<(String, String)>, + prefix: Option, +} + +struct Worktree<'a, S: WorktreeState> { + repo: &'a repo::RepoHandle, + extra: S, +} + +impl<'a> WithLocalBranchName<'a> { + fn new(name: String) -> Self { + Self { + local_branch_name: name, + local_branch: RefCell::new(None), + } + } +} + +trait WorktreeState {} + +impl WorktreeState for Init {} +impl<'a> WorktreeState for WithLocalBranchName<'a> {} +impl<'a> WorktreeState for WithLocalTargetSelected<'a> {} +impl<'a> WorktreeState for WithRemoteTrackingBranch<'a> {} + +impl<'a> Worktree<'a, Init> { + fn new(repo: &'a repo::RepoHandle) -> Self { + Self { + repo, + extra: Init {}, + } + } + + fn set_local_branch_name(self, name: &str) -> Worktree<'a, WithLocalBranchName<'a>> { + Worktree:: { + repo: self.repo, + extra: WithLocalBranchName::new(name.to_string()), + } + } +} + +impl<'a, 'b> Worktree<'a, WithLocalBranchName<'b>> +where + 'a: 'b, +{ + fn check_local_branch(&self) { + let mut branchref = self.extra.local_branch.borrow_mut(); + if branchref.is_none() { + let branch = self.repo.find_local_branch(&self.extra.local_branch_name); + *branchref = Some(if let Ok(branch) = branch { + Some(branch) + } else { + None + }); + } + } + + fn local_branch_already_exists(&self) -> bool { + if let Some(branch) = &*self.extra.local_branch.borrow() { + return branch.is_some(); + } + self.check_local_branch(); + // As we just called `check_local_branch`, we can be sure that + // `self.extra.local_branch` is set to some `Some` value + (*self.extra.local_branch.borrow()) + .as_ref() + .unwrap() + .is_some() + } + + fn select_commit( + self, + commit: Option>>, + ) -> Worktree<'a, WithLocalTargetSelected<'b>> { + self.check_local_branch(); + + Worktree::<'a, WithLocalTargetSelected> { + repo: self.repo, + extra: WithLocalTargetSelected::<'b> { + local_branch_name: self.extra.local_branch_name, + // As we just called `check_local_branch`, we can be sure that + // `self.extra.local_branch` is set to some `Some` value + local_branch: self.extra.local_branch.into_inner().unwrap(), + target_commit: commit, + }, + } + } +} + +impl<'a> Worktree<'a, WithLocalTargetSelected<'a>> { + fn set_remote_tracking_branch( + self, + branch: Option<(&str, &str)>, + prefix: Option<&str>, + ) -> Worktree<'a, WithRemoteTrackingBranch<'a>> { + Worktree:: { + repo: self.repo, + extra: WithRemoteTrackingBranch { + local_branch_name: self.extra.local_branch_name, + local_branch: self.extra.local_branch, + target_commit: self.extra.target_commit, + remote_tracking_branch: branch.map(|(s1, s2)| (s1.to_string(), s2.to_string())), + prefix: prefix.map(|prefix| prefix.to_string()), + }, + } + } +} + +impl<'a> Worktree<'a, WithRemoteTrackingBranch<'a>> { + fn create(self, directory: &Path) -> Result>, String> { + let mut warnings: Vec = vec![]; + + let mut branch = if let Some(branch) = self.extra.local_branch { + branch + } else { + self.repo.create_branch( + &self.extra.local_branch_name, + // TECHDEBT + // We must not call this with `Some()` without a valid target. + // I'm sure this can be improved, just not sure how. + &*self.extra.target_commit.unwrap(), + )? + }; + + if let Some((remote_name, remote_branch_name)) = self.extra.remote_tracking_branch { + let remote_branch_with_prefix = if let Some(ref prefix) = self.extra.prefix { + if let Ok(remote_branch) = self + .repo + .find_remote_branch(&remote_name, &format!("{prefix}/{remote_branch_name}")) + { + Some(remote_branch) + } else { + None + } + } else { + None + }; + + let remote_branch_without_prefix = if let Ok(remote_branch) = self + .repo + .find_remote_branch(&remote_name, &remote_branch_name) + { + Some(remote_branch) + } else { + None + }; + + let remote_branch = if let Some(ref _prefix) = self.extra.prefix { + remote_branch_with_prefix + } else { + remote_branch_without_prefix + }; + + match remote_branch { + Some(remote_branch) => { + if branch.commit()?.id().hex_string() + != remote_branch.commit()?.id().hex_string() + { + 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)); + } + + branch.set_upstream(&remote_name, &remote_branch.basename()?)?; + } + None => { + let mut remote = match self.repo.find_remote(&remote_name)? { + Some(remote) => remote, + None => return Err(format!("Remote \"{remote_name}\" not found")), + }; + + if !remote.is_pushable()? { + return Err(format!( + "Cannot push to non-pushable remote \"{remote_name}\"" + )); + } + + if let Some(prefix) = self.extra.prefix { + remote.push( + &self.extra.local_branch_name, + &format!("{}/{}", prefix, remote_branch_name), + self.repo, + )?; + + branch.set_upstream( + &remote_name, + &format!("{}/{}", prefix, remote_branch_name), + )?; + } else { + remote.push( + &self.extra.local_branch_name, + &remote_branch_name, + self.repo, + )?; + + branch.set_upstream(&remote_name, &remote_branch_name)?; + } + } + } + } + + // We have to create subdirectories first, otherwise adding the worktree + // will fail + if self.extra.local_branch_name.contains('/') { + let path = Path::new(&self.extra.local_branch_name); + if let Some(base) = path.parent() { + // This is a workaround of a bug in libgit2 (?) + // + // When *not* doing this, we will receive an error from the `Repository::worktree()` + // like this: + // + // > failed to make directory '/{repo}/.git-main-working-tree/worktrees/dir/test + // + // This is a discrepancy between the behaviour of libgit2 and the + // git CLI when creating worktrees with slashes: + // + // The git CLI will create the worktree's configuration directory + // inside {git_dir}/worktrees/{last_path_component}. Look at this: + // + // ``` + // $ git worktree add 1/2/3 -b 1/2/3 + // $ ls .git/worktrees + // 3 + // ``` + // + // Interesting: When adding a worktree with a different name but the + // same final path component, git starts adding a counter suffix to + // the worktree directories: + // + // ``` + // $ git worktree add 1/3/3 -b 1/3/3 + // $ git worktree add 1/4/3 -b 1/4/3 + // $ ls .git/worktrees + // 3 + // 31 + // 32 + // ``` + // + // I *guess* that the mapping back from the worktree directory under .git to the actual + // worktree directory is done via the `gitdir` file inside `.git/worktrees/{worktree}. + // This means that the actual directory would not matter. You can verify this by + // just renaming it: + // + // ``` + // $ mv .git/worktrees/3 .git/worktrees/foobar + // $ git worktree list + // /tmp/ fcc8a2a7 [master] + // /tmp/1/2/3 fcc8a2a7 [1/2/3] + // /tmp/1/3/3 fcc8a2a7 [1/3/3] + // /tmp/1/4/3 fcc8a2a7 [1/4/3] + // ``` + // + // => Still works + // + // Anyway, libgit2 does not do this: It tries to create the worktree + // directory inside .git with the exact name of the worktree, including + // any slashes. It should be this code: + // + // https://github.com/libgit2/libgit2/blob/f98dd5438f8d7bfd557b612fdf1605b1c3fb8eaf/src/libgit2/worktree.c#L346 + // + // As a workaround, we can create the base directory manually for now. + // + // Tracking upstream issue: https://github.com/libgit2/libgit2/issues/6327 + std::fs::create_dir_all( + directory + .join(GIT_MAIN_WORKTREE_DIRECTORY) + .join("worktrees") + .join(base), + ) + .map_err(|error| error.to_string())?; + std::fs::create_dir_all(base).map_err(|error| error.to_string())?; + } + } + + self.repo.new_worktree( + &self.extra.local_branch_name, + &directory.join(&self.extra.local_branch_name), + &branch, + )?; + + Ok(if warnings.is_empty() { + None + } else { + Some(warnings) + }) + } +} + +/// A branch name must never start or end with a slash, and it cannot have two +/// consecutive slashes +fn validate_worktree_name(name: &str) -> Result<(), String> { if name.starts_with('/') || name.ends_with('/') { return Err(format!( "Invalid worktree name: {}. It cannot start or end with a slash", @@ -32,6 +548,37 @@ pub fn add_worktree( )); } + if name.contains("//") { + return Err(format!( + "Invalid worktree name: {}. It cannot contain two consecutive slashes", + name + )); + } + + if name.contains(char::is_whitespace) { + return Err(format!( + "Invalid worktree name: {}. It cannot contain whitespace", + name + )); + } + + Ok(()) +} + +// TECHDEBT +// +// Instead of opening the repo & reading configuration inside the function, it +// should be done by the caller and given as a parameter +pub fn add_worktree( + directory: &Path, + name: &str, + track: Option<(&str, &str)>, + no_track: bool, +) -> Result>, String> { + let mut warnings: Vec = vec![]; + + validate_worktree_name(name)?; + let repo = repo::RepoHandle::open(directory, true).map_err(|error| match error.kind { repo::RepoErrorKind::NotFound => { String::from("Current directory does not contain a worktree setup") @@ -39,236 +586,195 @@ pub fn add_worktree( _ => format!("Error opening repo: {}", error), })?; + let remotes = &repo.remotes()?; + let config = repo::read_worktree_root_config(directory)?; if repo.find_worktree(name).is_ok() { return Err(format!("Worktree {} already exists", &name)); } - let mut remote_branch_exists = false; + let track_config = config.and_then(|config| config.track); + let prefix = track_config + .as_ref() + .and_then(|track| track.default_remote_prefix.as_ref()); + let enable_tracking = track_config.as_ref().map_or(false, |track| track.default); + let default_remote = track_config + .as_ref() + .map(|track| track.default_remote.clone()); - let mut target_branch = match repo.find_local_branch(name) { - Ok(branchref) => { - if !no_track { - if let Some((remote_name, remote_branch_name)) = track { - let remote_branch = repo.find_remote_branch(remote_name, remote_branch_name); - if let Ok(remote_branch) = remote_branch { - remote_branch_exists = true; - if let Ok(local_upstream_branch) = branchref.upstream() { - if remote_branch.name()? != local_upstream_branch.name()? { - print_warning(&format!( - "You specified a tracking branch ({}/{}) for an existing branch ({}), but \ - it differs from the current upstream ({}). Will keep current upstream" - , remote_name, remote_branch_name, branchref.name()?, local_upstream_branch.name()?)) - } - } - } - } - } - branchref - } - Err(_) => { - let default_checkout = || repo.default_branch()?.to_commit(); + // Note that we have to define all variables that borrow from `repo` + // *first*, otherwise we'll receive "borrowed value does not live long + // enough" errors. This is due to the `repo` reference inside `Worktree` that is + // passed through each state type. + // + // The `commit` variable will be dropped at the end of the scope, together with all + // worktree variables. It will be done in the opposite direction of delcaration (FILO). + // + // So if we define `commit` *after* the respective worktrees, it will be dropped first while + // still being borrowed by `Worktree`. + let default_branch_head = repo.default_branch()?.commit_owned()?; - let checkout_commit; + let worktree = Worktree::::new(&repo).set_local_branch_name(name); - if no_track { - checkout_commit = default_checkout()?; - } else { - match track { - Some((remote_name, remote_branch_name)) => { - let remote_branch = - repo.find_remote_branch(remote_name, remote_branch_name); - match remote_branch { - Ok(branch) => { - remote_branch_exists = true; - checkout_commit = branch.to_commit()?; - } - Err(_) => { - remote_branch_exists = false; - checkout_commit = default_checkout()?; - } - } - } - None => match &config { - None => checkout_commit = default_checkout()?, - Some(config) => match &config.track { - None => checkout_commit = default_checkout()?, - Some(track_config) => { - if track_config.default { - let remote_branch = - repo.find_remote_branch(&track_config.default_remote, name); - match remote_branch { - Ok(branch) => { - remote_branch_exists = true; - checkout_commit = branch.to_commit()?; - } - Err(_) => { - checkout_commit = default_checkout()?; - } - } - } else { - checkout_commit = default_checkout()?; - } - } - }, - }, - }; - } - - repo.create_branch(name, &checkout_commit)? + let get_remote_head = |remote_name: &str, + remote_branch_name: &str| + -> Result>, String> { + if let Ok(remote_branch) = repo.find_remote_branch(remote_name, remote_branch_name) { + Ok(Some(Box::new(remote_branch.commit_owned()?))) + } else { + Ok(None) } }; - fn push( - remote: &mut repo::RemoteHandle, - branch_name: &str, - remote_branch_name: &str, - repo: &repo::RepoHandle, - ) -> Result<(), String> { - if !remote.is_pushable()? { - return Err(format!( - "Cannot push to non-pushable remote {}", - remote.url() - )); + let worktree = if worktree.local_branch_already_exists() { + worktree.select_commit(None) + } else if let Some((remote_name, remote_branch_name)) = if no_track { None } else { track } { + if let Ok(remote_branch) = repo.find_remote_branch(remote_name, remote_branch_name) { + worktree.select_commit(Some(Box::new(remote_branch.commit_owned()?))) + } else { + worktree.select_commit(Some(Box::new(default_branch_head))) } - remote.push(branch_name, remote_branch_name, repo) - } - - if !no_track { - if let Some((remote_name, remote_branch_name)) = track { - if remote_branch_exists { - target_branch.set_upstream(remote_name, remote_branch_name)?; - } else { - let mut remote = repo - .find_remote(remote_name) - .map_err(|error| format!("Error getting remote {}: {}", remote_name, error))? - .ok_or_else(|| format!("Remote {} not found", remote_name))?; - - push( - &mut remote, - &target_branch.name()?, - remote_branch_name, - &repo, - )?; - - target_branch.set_upstream(remote_name, remote_branch_name)?; - } - } else if let Some(config) = config { - if let Some(track_config) = config.track { - if track_config.default { - let remote_name = track_config.default_remote; - if remote_branch_exists { - target_branch.set_upstream(&remote_name, name)?; + } else { + match remotes.len() { + 0 => worktree.select_commit(Some(Box::new(default_branch_head))), + 1 => { + let remote_name = &remotes[0]; + let commit: Option> = ({ + if let Some(prefix) = prefix { + get_remote_head(remote_name, &format!("{prefix}/{name}"))? } else { - let remote_branch_name = match track_config.default_remote_prefix { - Some(prefix) => { - format!("{}{}{}", &prefix, super::BRANCH_NAMESPACE_SEPARATOR, &name) - } - None => name.to_string(), - }; - - let mut remote = repo - .find_remote(&remote_name) - .map_err(|error| { - format!("Error getting remote {}: {}", remote_name, error) - })? - .ok_or_else(|| format!("Remote {} not found", remote_name))?; - - if !remote.is_pushable()? { - return Err(format!( - "Cannot push to non-pushable remote {}", - remote.url() - )); - } - push( - &mut remote, - &target_branch.name()?, - &remote_branch_name, - &repo, - )?; - - target_branch.set_upstream(&remote_name, &remote_branch_name)?; + None } + }) + .or(get_remote_head(remote_name, name)?) + .or_else(|| Some(Box::new(default_branch_head))); + + worktree.select_commit(commit) + } + _ => { + let commit = if let Some(ref default_remote) = default_remote { + if let Some(ref prefix) = prefix { + if let Ok(remote_branch) = repo + .find_remote_branch(default_remote, &format!("{prefix}/{name}")) + { + Some(Box::new(remote_branch.commit_owned()?)) + } else { + None + } + } else { + None + } + .or({ + if let Ok(remote_branch) = + repo.find_remote_branch(default_remote, name) + { + Some(Box::new(remote_branch.commit_owned()?)) + } else { + None + } + }) + } else { + None + }.or({ + let mut commits = vec![]; + for remote_name in remotes.iter() { + let remote_head: Option> = ({ + if let Some(ref prefix) = prefix { + if let Ok(remote_branch) = repo.find_remote_branch( + remote_name, + &format!("{prefix}/{name}"), + ) { + Some(Box::new(remote_branch.commit_owned()?)) + } else { + None + } + } else { + None + } + }) + .or({ + if let Ok(remote_branch) = + repo.find_remote_branch(remote_name, name) + { + Some(Box::new(remote_branch.commit_owned()?)) + } else { + None + } + }) + .or(None); + commits.push(remote_head); + } + + let mut commits = commits + .into_iter() + .flatten() + // have to collect first because the `flatten()` return + // typedoes not implement `windows()` + .collect::>>(); + // `flatten()` takes care of `None` values here. If all + // remotes return None for the branch, we do *not* abort, we + // continue! + if commits.is_empty() { + Some(Box::new(default_branch_head)) + } else if commits.len() == 1 { + Some(commits.swap_remove(0)) + } else if commits.windows(2).any(|window| { + let c1 = &window[0]; + let c2 = &window[1]; + (*c1).id().hex_string() != (*c2).id().hex_string() + }) { + warnings.push( + // TODO this should also include the branch + // name. BUT: the branch name may be different + // between the remotes. Let's just leave it + // until I get around to fix that inconsistency + // (see module-level doc about), which might be + // never, as it's such a rare edge case. + "Branch exists on multiple remotes, but they deviate. Selecting default branch instead".to_string() + ); + Some(Box::new(default_branch_head)) + } else { + Some(commits.swap_remove(0)) + } + }); + worktree.select_commit(commit) + } + } + }; + + let worktree = if no_track { + worktree.set_remote_tracking_branch(None, prefix.map(|s| s.as_str())) + } else if let Some((remote_name, remote_branch_name)) = track { + worktree.set_remote_tracking_branch( + Some((remote_name, remote_branch_name)), + None, // Always disable prefixing when explicitly given --track + ) + } else if !enable_tracking { + worktree.set_remote_tracking_branch(None, prefix.map(|s| s.as_str())) + } else { + match remotes.len() { + 0 => worktree.set_remote_tracking_branch(None, prefix.map(|s| s.as_str())), + 1 => worktree + .set_remote_tracking_branch(Some((&remotes[0], name)), prefix.map(|s| s.as_str())), + _ => { + if let Some(default_remote) = default_remote { + worktree.set_remote_tracking_branch( + Some((&default_remote, name)), + prefix.map(|s| s.as_str()), + ) + } else { + worktree.set_remote_tracking_branch(None, prefix.map(|s| s.as_str())) } } } - } + }; - // We have to create subdirectories first, otherwise adding the worktree - // will fail - if name.contains('/') { - let path = Path::new(&name); - if let Some(base) = path.parent() { - // This is a workaround of a bug in libgit2 (?) - // - // When *not* doing this, we will receive an error from the `Repository::worktree()` - // like this: - // - // > failed to make directory '/{repo}/.git-main-working-tree/worktrees/dir/test - // - // This is a discrepancy between the behaviour of libgit2 and the - // git CLI when creating worktrees with slashes: - // - // The git CLI will create the worktree's configuration directory - // inside {git_dir}/worktrees/{last_path_component}. Look at this: - // - // ``` - // $ git worktree add 1/2/3 -b 1/2/3 - // $ ls .git/worktrees - // 3 - // ``` - // - // Interesting: When adding a worktree with a different name but the - // same final path component, git starts adding a counter suffix to - // the worktree directories: - // - // ``` - // $ git worktree add 1/3/3 -b 1/3/3 - // $ git worktree add 1/4/3 -b 1/4/3 - // $ ls .git/worktrees - // 3 - // 31 - // 32 - // ``` - // - // I *guess* that the mapping back from the worktree directory under .git to the actual - // worktree directory is done via the `gitdir` file inside `.git/worktrees/{worktree}. - // This means that the actual directory would not matter. You can verify this by - // just renaming it: - // - // ``` - // $ mv .git/worktrees/3 .git/worktrees/foobar - // $ git worktree list - // /tmp/ fcc8a2a7 [master] - // /tmp/1/2/3 fcc8a2a7 [1/2/3] - // /tmp/1/3/3 fcc8a2a7 [1/3/3] - // /tmp/1/4/3 fcc8a2a7 [1/4/3] - // ``` - // - // => Still works - // - // Anyway, libgit2 does not do this: It tries to create the worktree - // directory inside .git with the exact name of the worktree, including - // any slashes. It should be this code: - // - // https://github.com/libgit2/libgit2/blob/f98dd5438f8d7bfd557b612fdf1605b1c3fb8eaf/src/libgit2/worktree.c#L346 - // - // As a workaround, we can create the base directory manually for now. - // - // Tracking upstream issue: https://github.com/libgit2/libgit2/issues/6327 - std::fs::create_dir_all( - directory - .join(GIT_MAIN_WORKTREE_DIRECTORY) - .join("worktrees") - .join(base), - ) - .map_err(|error| error.to_string())?; - std::fs::create_dir_all(base).map_err(|error| error.to_string())?; - } - } + worktree.create(directory)?; - repo.new_worktree(name, &directory.join(&name), &target_branch)?; - - Ok(()) + Ok(if warnings.is_empty() { + None + } else { + Some(warnings) + }) }