Files
git-repo-manager/src/worktree.rs

275 lines
11 KiB
Rust

use std::path::Path;
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.
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 {
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)?;
if repo.find_worktree(name).is_ok() {
return Err(format!("Worktree {} already exists", &name));
}
let mut remote_branch_exists = false;
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();
let checkout_commit;
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)?
}
};
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()
));
}
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 {
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)?;
}
}
}
}
}
// 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())?;
}
}
repo.new_worktree(name, &directory.join(&name), &target_branch)?;
Ok(())
}