use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
use crate::git::Git;
use crate::progress;
#[derive(Debug, PartialEq, Eq)]
pub enum Merge {
Clean,
Conflicted,
}
pub struct Worktree {
launch: Git,
branch: String,
git: Git,
}
impl Worktree {
pub fn create_fresh(launch: &Git, repo: &str, branch: &str, base: &str) -> Result<Self> {
launch.run(&["fetch", "origin", base])?;
Self::add(
launch,
repo,
branch,
&["-b", branch],
&format!("origin/{base}"),
)
}
pub fn continue_existing(launch: &Git, repo: &str, branch: &str, base: &str) -> Result<Self> {
launch.run(&["fetch", "origin", base, branch])?;
Self::add(
launch,
repo,
branch,
&["-B", branch],
&format!("origin/{branch}"),
)
}
fn add(
launch: &Git,
repo: &str,
branch: &str,
branch_args: &[&str],
start: &str,
) -> Result<Self> {
let root = PathBuf::from(launch.run(&["rev-parse", "--show-toplevel"])?);
let path = root
.parent()
.context("the repository root has no parent directory")?
.join(format!("{repo}-{branch}"));
let path_arg = path.to_str().context("worktree path is not UTF-8")?;
progress::step(format_args!(
"creating worktree {} on {branch} from {start}",
path.display()
));
let mut args = vec!["worktree", "add"];
args.extend_from_slice(branch_args);
args.extend([path_arg, start]);
launch.run(&args)?;
Ok(Worktree {
launch: Git::new(root),
branch: branch.to_string(),
git: Git::new(path),
})
}
pub fn path(&self) -> &Path {
self.git.dir()
}
pub fn branch(&self) -> &str {
&self.branch
}
pub fn git(&self) -> &Git {
&self.git
}
pub fn push(&self) -> Result<()> {
progress::step(format_args!("pushing {}", self.branch));
self.git.run(&["push", "origin", &self.branch])?;
Ok(())
}
pub fn head(&self) -> Result<String> {
self.git.run(&["rev-parse", "HEAD"])
}
pub fn merge_base_branch(&self, base: &str) -> Result<Merge> {
progress::step(format_args!("merging origin/{base} into {}", self.branch));
self.git.run(&["fetch", "origin", base])?;
match self
.git
.run(&["merge", "--no-edit", "--ff", &format!("origin/{base}")])
{
Ok(_) => Ok(Merge::Clean),
Err(_) if self.merge_in_progress()? => Ok(Merge::Conflicted),
Err(error) => Err(error),
}
}
pub fn ensure_base_branch_merged(&self, base: &str) -> Result<()> {
if self.merge_in_progress()? {
bail!("the merge of origin/{base} is still in progress");
}
let upstream = format!("origin/{base}");
if !self
.git
.succeeds(&["merge-base", "--is-ancestor", &upstream, "HEAD"])?
{
bail!("{upstream} is not merged into {}", self.branch);
}
Ok(())
}
pub fn merge_in_progress(&self) -> Result<bool> {
self.git
.succeeds(&["rev-parse", "-q", "--verify", "MERGE_HEAD"])
}
}
impl Drop for Worktree {
fn drop(&mut self) {
let path = self.path().to_string_lossy().into_owned();
progress::step(format_args!(
"cleaning up the worktree and local branch {}",
self.branch
));
let steps: [&[&str]; 2] = [
&["worktree", "remove", "--force", &path],
&["branch", "-D", &self.branch],
];
for step in steps {
if let Err(error) = self.launch.run(step) {
progress::step(format_args!("cleanup incomplete: {error:#}"));
}
}
}
}