use std::io::{self, Write};
use std::path::{Path, PathBuf};
use crate::git;
use crate::root;
use anyhow::{Context, bail};
use super::fork::{remove_worktree_dir, rollback_fork};
use super::provision::run_provision;
use super::shared::{gather_fork_worktree, matches, resolve_commit};
pub(crate) struct CoordOutcome {
pub dispatch_tip: String,
}
pub(crate) fn run_branch_point_check(
path: Option<PathBuf>,
base: &str,
head: Option<String>,
) -> anyhow::Result<()> {
let root = root::find(path, &root::default_markers())?;
let head = head.unwrap_or_else(|| "HEAD".to_owned());
let base_sha = resolve_commit(&root, base)?;
let head_sha = resolve_commit(&root, &head)?;
if matches(&base_sha, &head_sha) {
writeln!(io::stdout(), "stationary: HEAD == base {base_sha}")?;
Ok(())
} else {
bail!("HEAD moved: base {base_sha} != HEAD {head_sha}");
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CoordAction {
Create,
Resume,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CoordRefusal {
LiveWorktree,
}
impl CoordRefusal {
pub(crate) fn token(self) -> &'static str {
match self {
CoordRefusal::LiveWorktree => "coordination-live",
}
}
}
pub(crate) fn classify_coordinate(
exists: bool,
has_live_worktree: bool,
) -> Result<CoordAction, CoordRefusal> {
match (exists, has_live_worktree) {
(false, _) => Ok(CoordAction::Create),
(true, true) => Err(CoordRefusal::LiveWorktree),
(true, false) => Ok(CoordAction::Resume),
}
}
pub(crate) fn base_has_slice_plan(root: &Path, base: &str, slice: u32) -> anyhow::Result<bool> {
let pathspec = format!(".doctrine/slice/{slice:03}/plan.toml");
let listing = git::git_opt(root, &["ls-tree", base, "--", &pathspec])?;
Ok(listing.is_some_and(|out| !out.is_empty()))
}
pub(crate) fn ensure_base_corpus_fresh(
root: &Path,
authoring_branch: Option<&str>,
base: &str,
) -> anyhow::Result<()> {
let Some(corpus_ref) = authoring_branch else {
return Ok(());
};
if let Some(corpus_tip) =
git::last_corpus_commit(root, corpus_ref, crate::corpus_guard::DOCTRINE_PATHSPEC)?
{
anyhow::ensure!(
git::is_ancestor(root, &corpus_tip, base)?,
"{}: base {base} predates corpus tip {corpus_tip} on {corpus_ref} — promote \
the base first (`git fetch . {corpus_ref}:<trunk>`), never fork the stale base",
crate::corpus_guard::BASE_CORPUS_STALE,
);
}
Ok(())
}
pub(crate) fn coordinate(
root: &Path,
slice: u32,
dir: &Path,
authoring_branch: Option<&str>,
) -> anyhow::Result<CoordOutcome> {
let branch = format!("dispatch/{slice:03}");
if dir.exists() {
bail!("coordinate-refused: dir {} already exists", dir.display());
}
let exists = git::git_opt(
root,
&[
"rev-parse",
"--verify",
"--quiet",
&format!("refs/heads/{branch}^{{commit}}"),
],
)?
.is_some();
let live_worktree = gather_fork_worktree(root, &branch)?;
let action = match classify_coordinate(exists, live_worktree.is_some()) {
Ok(action) => action,
Err(refusal) => {
let at = live_worktree
.map(|p| p.display().to_string())
.unwrap_or_default();
bail!(
"coordinate-refused: {} — {branch} has a live worktree at {at}",
refusal.token()
);
}
};
match action {
CoordAction::Create => {
let trunk = git::trunk_commit(root)?.ok_or_else(|| {
anyhow::anyhow!(
"coordinate-refused: no trunk ref resolves (set DOCTRINE_TRUNK_REF)"
)
})?;
if !base_has_slice_plan(root, &trunk, slice)? {
bail!(
"coordinate-refused: base {trunk} lacks .doctrine/slice/{slice:03}/plan.toml \
— the trunk base predates this slice's plan; set DOCTRINE_TRUNK_REF to a base \
that carries it (e.g. DOCTRINE_TRUNK_REF=main)"
);
}
ensure_base_corpus_fresh(root, authoring_branch, &trunk)?;
git::git_text(
root,
&[
"worktree",
"add",
"-b",
&branch,
&dir.to_string_lossy(),
&trunk,
],
)
.with_context(|| format!("git worktree add -b {branch} {} {trunk}", dir.display()))?;
}
CoordAction::Resume => {
git::git_text(root, &["worktree", "add", &dir.to_string_lossy(), &branch])
.with_context(|| format!("git worktree add {} {branch}", dir.display()))?;
}
}
let finish = (|| -> anyhow::Result<()> {
run_provision(Some(root.to_path_buf()), dir).context("provision coordination worktree")?;
crate::slice::run_phases(Some(dir.to_path_buf()), slice, false)
.context("regenerate runtime phase sheets")?;
Ok(())
})();
if let Err(cause) = finish {
let debris = match action {
CoordAction::Create => rollback_fork(root, &branch, dir),
CoordAction::Resume => remove_worktree_dir(root, dir),
};
if debris.is_empty() {
return Err(cause.context(format!(
"coordinate failed after add; rolled back cleanly (worktree {} removed)",
dir.display()
)));
}
bail!(
"coordinate-rollback-debris: {} (original cause: {cause:#})",
debris.join(", ")
);
}
let dispatch_tip = git::git_text(
root,
&["rev-parse", "--short", &format!("refs/heads/{branch}")],
)?;
Ok(CoordOutcome { dispatch_tip })
}
pub(crate) fn run_coordinate(
path: Option<PathBuf>,
slice: u32,
dir: &Path,
authoring_branch: Option<&str>,
) -> anyhow::Result<()> {
let repo = root::find(path, &root::default_markers())?;
let branch = format!("dispatch/{slice:03}");
let branch_existed = git::git_opt(
&repo,
&[
"rev-parse",
"--verify",
"--quiet",
&format!("refs/heads/{branch}^{{commit}}"),
],
)?
.is_some();
let _outcome = coordinate(&repo, slice, dir, authoring_branch)?;
let verb = if branch_existed { "resumed" } else { "created" };
writeln!(
io::stderr(),
"coordination worktree {verb}: {branch} → {} (markerless)",
dir.display()
)?;
Ok(())
}