Update worktree handling
That's a big one, see the module-level comment for details.
This commit is contained in:
@@ -513,7 +513,14 @@ fn main() {
|
|||||||
track,
|
track,
|
||||||
action_args.no_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) => {
|
Err(error) => {
|
||||||
print_error(&format!("Error creating worktree: {}", error));
|
print_error(&format!("Error creating worktree: {}", error));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
|
|||||||
830
src/worktree.rs
830
src/worktree.rs
@@ -1,195 +1,451 @@
|
|||||||
|
//! 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 std::path::Path;
|
||||||
|
|
||||||
use super::output::*;
|
// use super::output::*;
|
||||||
use super::repo;
|
use super::repo;
|
||||||
|
|
||||||
pub const GIT_MAIN_WORKTREE_DIRECTORY: &str = ".git-main-working-tree";
|
pub const GIT_MAIN_WORKTREE_DIRECTORY: &str = ".git-main-working-tree";
|
||||||
|
|
||||||
// The logic about the base branch and the tracking branch is as follows:
|
#[cfg(test)]
|
||||||
//
|
mod tests {
|
||||||
// * If a branch with the same name does not exist and no track is given, use the default
|
use super::*;
|
||||||
// branch
|
|
||||||
//
|
#[test]
|
||||||
// * If a branch with the same name exists and no track is given, use that
|
fn invalid_worktree_names() {
|
||||||
//
|
assert!(add_worktree(Path::new("/tmp/"), "/leadingslash", None, false).is_err());
|
||||||
// * If a branch with the same name does not exist and track is given, use the
|
assert!(add_worktree(Path::new("/tmp/"), "trailingslash/", None, false).is_err());
|
||||||
// local branch that tracks that branch
|
assert!(add_worktree(Path::new("/tmp/"), "//", None, false).is_err());
|
||||||
//
|
assert!(add_worktree(Path::new("/tmp/"), "test//test", None, false).is_err());
|
||||||
// * If a branch with the same name exists and track is given, use the locally
|
assert!(add_worktree(Path::new("/tmp/"), "test test", None, false).is_err());
|
||||||
// existing branch. If the locally existing branch is not the local branch to
|
assert!(add_worktree(Path::new("/tmp/"), "test\ttest", None, false).is_err());
|
||||||
// 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.
|
|
||||||
if name.starts_with('/') || name.ends_with('/') {
|
|
||||||
return Err(format!(
|
|
||||||
"Invalid worktree name: {}. It cannot start or end with a slash",
|
|
||||||
name
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let repo = repo::RepoHandle::open(directory, true).map_err(|error| match error.kind {
|
struct Init;
|
||||||
repo::RepoErrorKind::NotFound => {
|
|
||||||
String::from("Current directory does not contain a worktree setup")
|
|
||||||
}
|
|
||||||
_ => format!("Error opening repo: {}", error),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let config = repo::read_worktree_root_config(directory)?;
|
struct WithLocalBranchName<'a> {
|
||||||
|
local_branch_name: String,
|
||||||
if repo.find_worktree(name).is_ok() {
|
/// Outer option: Is there a computed value?
|
||||||
return Err(format!("Worktree {} already exists", &name));
|
/// Inner option: Is there actually a branch?
|
||||||
|
///
|
||||||
|
/// None => No computed value yet
|
||||||
|
/// Some(None) => No branch
|
||||||
|
/// Some(Some(_)) => Branch
|
||||||
|
local_branch: RefCell<Option<Option<repo::Branch<'a>>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut remote_branch_exists = false;
|
struct WithLocalTargetSelected<'a> {
|
||||||
|
local_branch_name: String,
|
||||||
|
local_branch: Option<repo::Branch<'a>>,
|
||||||
|
target_commit: Option<Box<repo::Commit<'a>>>,
|
||||||
|
}
|
||||||
|
|
||||||
let mut target_branch = match repo.find_local_branch(name) {
|
struct WithRemoteTrackingBranch<'a> {
|
||||||
Ok(branchref) => {
|
local_branch_name: String,
|
||||||
if !no_track {
|
local_branch: Option<repo::Branch<'a>>,
|
||||||
if let Some((remote_name, remote_branch_name)) = track {
|
target_commit: Option<Box<repo::Commit<'a>>>,
|
||||||
let remote_branch = repo.find_remote_branch(remote_name, remote_branch_name);
|
remote_tracking_branch: Option<(String, String)>,
|
||||||
if let Ok(remote_branch) = remote_branch {
|
prefix: Option<String>,
|
||||||
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();
|
|
||||||
|
|
||||||
let checkout_commit;
|
struct Worktree<'a, S: WorktreeState> {
|
||||||
|
repo: &'a repo::RepoHandle,
|
||||||
|
extra: S,
|
||||||
|
}
|
||||||
|
|
||||||
if no_track {
|
impl<'a> WithLocalBranchName<'a> {
|
||||||
checkout_commit = default_checkout()?;
|
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::<WithLocalBranchName> {
|
||||||
|
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 {
|
} else {
|
||||||
match track {
|
None
|
||||||
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)?
|
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<Box<repo::Commit<'b>>>,
|
||||||
|
) -> 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::<WithRemoteTrackingBranch> {
|
||||||
|
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<Option<Vec<String>>, String> {
|
||||||
|
let mut warnings: Vec<String> = 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")),
|
||||||
};
|
};
|
||||||
|
|
||||||
fn push(
|
|
||||||
remote: &mut repo::RemoteHandle,
|
|
||||||
branch_name: &str,
|
|
||||||
remote_branch_name: &str,
|
|
||||||
repo: &repo::RepoHandle,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
if !remote.is_pushable()? {
|
if !remote.is_pushable()? {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Cannot push to non-pushable remote {}",
|
"Cannot push to non-pushable remote \"{remote_name}\""
|
||||||
remote.url()
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
remote.push(branch_name, remote_branch_name, repo)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !no_track {
|
if let Some(prefix) = self.extra.prefix {
|
||||||
if let Some((remote_name, remote_branch_name)) = track {
|
remote.push(
|
||||||
if remote_branch_exists {
|
&self.extra.local_branch_name,
|
||||||
target_branch.set_upstream(remote_name, remote_branch_name)?;
|
&format!("{}/{}", prefix, remote_branch_name),
|
||||||
} else {
|
self.repo,
|
||||||
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)?;
|
branch.set_upstream(
|
||||||
}
|
&remote_name,
|
||||||
} else if let Some(config) = config {
|
&format!("{}/{}", prefix, remote_branch_name),
|
||||||
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 {
|
} else {
|
||||||
let remote_branch_name = match track_config.default_remote_prefix {
|
remote.push(
|
||||||
Some(prefix) => {
|
&self.extra.local_branch_name,
|
||||||
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,
|
&remote_branch_name,
|
||||||
&repo,
|
self.repo,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
target_branch.set_upstream(&remote_name, &remote_branch_name)?;
|
branch.set_upstream(&remote_name, &remote_branch_name)?;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -197,8 +453,8 @@ pub fn add_worktree(
|
|||||||
|
|
||||||
// We have to create subdirectories first, otherwise adding the worktree
|
// We have to create subdirectories first, otherwise adding the worktree
|
||||||
// will fail
|
// will fail
|
||||||
if name.contains('/') {
|
if self.extra.local_branch_name.contains('/') {
|
||||||
let path = Path::new(&name);
|
let path = Path::new(&self.extra.local_branch_name);
|
||||||
if let Some(base) = path.parent() {
|
if let Some(base) = path.parent() {
|
||||||
// This is a workaround of a bug in libgit2 (?)
|
// This is a workaround of a bug in libgit2 (?)
|
||||||
//
|
//
|
||||||
@@ -268,7 +524,257 @@ pub fn add_worktree(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
repo.new_worktree(name, &directory.join(&name), &target_branch)?;
|
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",
|
||||||
|
name
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
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(())
|
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<Option<Vec<String>>, String> {
|
||||||
|
let mut warnings: Vec<String> = 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")
|
||||||
|
}
|
||||||
|
_ => 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 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());
|
||||||
|
|
||||||
|
// 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 worktree = Worktree::<Init>::new(&repo).set_local_branch_name(name);
|
||||||
|
|
||||||
|
let get_remote_head = |remote_name: &str,
|
||||||
|
remote_branch_name: &str|
|
||||||
|
-> Result<Option<Box<repo::Commit>>, 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)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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)))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match remotes.len() {
|
||||||
|
0 => worktree.select_commit(Some(Box::new(default_branch_head))),
|
||||||
|
1 => {
|
||||||
|
let remote_name = &remotes[0];
|
||||||
|
let commit: Option<Box<repo::Commit>> = ({
|
||||||
|
if let Some(prefix) = prefix {
|
||||||
|
get_remote_head(remote_name, &format!("{prefix}/{name}"))?
|
||||||
|
} else {
|
||||||
|
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<Box<repo::Commit>> = ({
|
||||||
|
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::<Vec<Box<repo::Commit>>>();
|
||||||
|
// `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()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
worktree.create(directory)?;
|
||||||
|
|
||||||
|
Ok(if warnings.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(warnings)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user