use super::marker::write_marker;
use super::provision::run_provision;
use crate::git;
use crate::root;
use anyhow::{Context, bail};
use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
pub(super) fn remove_worktree_dir(repo: &Path, dir: &Path) -> Vec<String> {
let mut debris = Vec::new();
if git::git_text(
repo,
&["worktree", "remove", "--force", &dir.to_string_lossy()],
)
.is_err()
&& dir.exists()
{
debris.push(format!("worktree dir {}", dir.display()));
}
if dir.exists() {
drop(fs::remove_dir_all(dir));
let dir_str = dir.display().to_string();
if dir.exists() {
if !debris.iter().any(|d| d.contains(&dir_str)) {
debris.push(format!("dir {dir_str}"));
}
} else {
debris.retain(|d| !d.contains(&dir_str));
}
}
debris
}
pub(super) fn rollback_fork(repo: &Path, branch: &str, dir: &Path) -> Vec<String> {
let mut debris = remove_worktree_dir(repo, dir);
if git::git_opt(repo, &["rev-parse", "--verify", "--quiet", branch])
.ok()
.flatten()
.is_some()
{
drop(git::git_text(repo, &["branch", "-D", branch]));
if git::git_opt(repo, &["rev-parse", "--verify", "--quiet", branch])
.ok()
.flatten()
.is_some()
{
debris.push(format!("branch {branch}"));
}
}
debris
}
pub(super) fn fork_core(
repo: &Path,
base: &str,
branch: &str,
dir: &Path,
worker: bool,
) -> anyhow::Result<()> {
if dir.exists() {
bail!("fork-refused: dir {} already exists", dir.display());
}
if git::git_opt(repo, &["rev-parse", "--verify", "--quiet", branch])
.ok()
.flatten()
.is_some()
{
bail!("fork-refused: branch {branch} already exists");
}
if git::git_opt(
repo,
&[
"rev-parse",
"--verify",
"--quiet",
&format!("{base}^{{commit}}"),
],
)?
.is_none()
{
bail!("fork-refused: base {base} is not a commit");
}
git::git_text(
repo,
&[
"worktree",
"add",
"-b",
branch,
&dir.to_string_lossy(),
base,
],
)
.with_context(|| format!("git worktree add -b {branch} {} {base}", dir.display()))?;
let finish = (|| -> anyhow::Result<()> {
run_provision(Some(repo.to_path_buf()), dir).context("provision fork")?;
if worker {
write_marker(dir).context("stamp worker marker")?;
}
Ok(())
})();
if let Err(cause) = finish {
let debris = rollback_fork(repo, branch, dir);
if debris.is_empty() {
return Err(cause.context(format!(
"fork failed after add; rolled back cleanly (dir {} + branch {branch} removed)",
dir.display()
)));
}
bail!(
"fork-rollback-debris: {} (original cause: {cause:#})",
debris.join(", ")
);
}
Ok(())
}
pub(crate) fn run_fork(
path: Option<PathBuf>,
base: &str,
branch: &str,
dir: &Path,
worker: bool,
) -> anyhow::Result<()> {
let repo = root::find(path, &root::default_markers())?;
fork_core(&repo, base, branch, dir, worker)?;
writeln!(
io::stderr(),
"forked {branch} at {base} → {}{}",
dir.display(),
if worker {
" (worker: marker stamped)"
} else {
""
}
)?;
Ok(())
}