use git2::BranchType;
use std::{fs, path::Path};
use crate::{
error::Result, find_worktree, get_worktrees, WorkonConfig, WorkonError, WorktreeDescriptor,
WorktreeError,
};
#[derive(Default)]
pub struct MoveOptions {
pub force: bool,
}
pub fn move_worktree(
repo: &git2::Repository,
from: &str,
to: &str,
options: &MoveOptions,
) -> Result<WorktreeDescriptor> {
let source = find_worktree(repo, from)?;
validate_move(repo, &source, to, options)?;
let root = crate::workon_root(repo)?;
let branch_name = source.branch()?.unwrap();
let old_path = source.path().to_path_buf();
let new_path = root.join(to);
let old_name = source.name().unwrap().to_string();
let new_name = Path::new(to)
.file_name()
.and_then(|s| s.to_str())
.ok_or(WorktreeError::InvalidName)?
.to_string();
if let Some(parent) = new_path.parent() {
std::fs::create_dir_all(parent)?;
}
let mut branch = repo.find_branch(&branch_name, BranchType::Local)?;
branch.rename(to, false)?;
if let Err(e) = fs::rename(&old_path, &new_path) {
let _ = branch.rename(&branch_name, false);
return Err(WorkonError::Io(e));
}
let old_meta_dir = repo.path().join("worktrees").join(&old_name);
let new_meta_dir = repo.path().join("worktrees").join(&new_name);
if old_meta_dir != new_meta_dir && old_meta_dir.exists() {
fs::rename(&old_meta_dir, &new_meta_dir)?;
}
if new_meta_dir.exists() {
let new_gitdir = new_meta_dir.join("gitdir");
let new_git = new_path.join(".git");
fs::write(&new_gitdir, format!("{}\n", new_git.display()))?;
fs::write(&new_git, format!("gitdir: {}\n", new_meta_dir.display()))?;
}
WorktreeDescriptor::new(repo, &new_name)
}
pub fn validate_move(
repo: &git2::Repository,
source: &WorktreeDescriptor,
target_name: &str,
options: &MoveOptions,
) -> Result<()> {
if source.is_detached()? {
return Err(WorktreeError::CannotMoveDetached.into());
}
for wt in get_worktrees(repo)? {
if wt.name() == Some(target_name)
|| wt.branch().ok().flatten().as_deref() == Some(target_name)
{
return Err(WorktreeError::TargetExists {
to: target_name.to_string(),
}
.into());
}
}
if repo.find_branch(target_name, BranchType::Local).is_ok() {
return Err(WorktreeError::TargetExists {
to: target_name.to_string(),
}
.into());
}
if !options.force {
let config = WorkonConfig::new(repo)?;
let branch_name = source.branch()?.unwrap();
if config.is_protected(&branch_name) {
return Err(WorktreeError::ProtectedBranchMove(branch_name).into());
}
}
if !options.force && source.is_dirty()? {
return Err(WorktreeError::DirtyWorktree.into());
}
if !options.force && source.has_unpushed_commits()? {
return Err(WorktreeError::UnpushedCommits.into());
}
Ok(())
}