use super::checkout::{self, Checkout};
use super::{GitRunner, TEMPLATE, descriptions};
use crate::workspace::{self, config_ref};
use std::io;
use std::path::Path;
#[derive(Debug, PartialEq, Eq)]
pub enum Pass {
Landed,
Declined {
target: String,
},
}
pub enum Origin<'a> {
Advance,
Fork { source: &'a str },
Orphan,
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error(transparent)]
Layout(#[from] workspace::LayoutError),
#[error("config authoring I/O: {0}")]
Io(#[source] io::Error),
#[error("config authoring git: {0}")]
Git(#[source] io::Error),
#[error("descriptions-always: {0}")]
Descriptions(#[source] descriptions::Error),
#[error("edit step: {0}")]
Edit(#[source] io::Error),
#[error("pass --from <source> or --orphan, not both")]
Conflict,
#[error(transparent)]
NoSuchLineage(#[from] workspace::UnknownLineage),
}
pub fn from_cli<G: GitRunner>(
workspace: &Path,
data_root: &Path,
name: Option<&str>,
from: Option<&str>,
orphan: bool,
edit: impl FnOnce(&Path) -> io::Result<()>,
git: &G,
) -> Result<Pass, Error> {
let origin = match (from, orphan) {
(Some(source), false) => Origin::Fork { source },
(None, true) => Origin::Orphan,
(None, false) => Origin::Advance,
(Some(_), true) => return Err(Error::Conflict),
};
let name = name.unwrap_or(workspace::DEFAULT_CONFIG_NAME);
author(workspace, data_root, name, origin, edit, git)
}
pub fn author<G: GitRunner>(
workspace: &Path,
data_root: &Path,
name: &str,
origin: Origin,
edit: impl FnOnce(&Path) -> io::Result<()>,
git: &G,
) -> Result<Pass, Error> {
workspace::require(workspace)?;
require_source(workspace, &origin, git)?;
let repo = workspace::repo_git(workspace);
let author = checkout::path(workspace);
let target = config_ref(name);
checkout::heal(git, &repo, &author).map_err(Error::Io)?;
let checkout = materialize(git, &repo, &target, &author, &origin)?;
std::fs::create_dir_all(&author).map_err(Error::Io)?;
if matches!(origin, Origin::Orphan) {
TEMPLATE.extract(&author).map_err(Error::Io)?;
}
descriptions::snapshot(data_root, &author).map_err(Error::Descriptions)?;
edit(&author).map_err(Error::Edit)?;
if !super::commit_checkout(git, &author, &commit_message(name, &origin)).map_err(Error::Git)? {
return Ok(Pass::Declined { target });
}
checkout.landed().map_err(Error::Git)?;
Ok(Pass::Landed)
}
fn require_source<G: GitRunner>(workspace: &Path, origin: &Origin, git: &G) -> Result<(), Error> {
let Origin::Fork { source } = origin else {
return Ok(());
};
workspace::require_lineage(workspace, source, git).map_err(Error::NoSuchLineage)
}
fn materialize<'a, G: GitRunner>(
git: &'a G,
repo: &'a Path,
target: &str,
author: &Path,
origin: &Origin,
) -> Result<Checkout<'a, G>, Error> {
let src = match origin {
Origin::Fork { source } => config_ref(source),
_ => String::new(),
};
let author_str = author.to_string_lossy().to_string();
let (args, created): (Vec<&str>, _) = match origin {
Origin::Advance => (vec!["worktree", "add", &author_str, target], None),
Origin::Fork { .. } => (
vec!["worktree", "add", "-b", target, &author_str, &src],
Some(target.to_string()),
),
Origin::Orphan => (
vec!["worktree", "add", "--orphan", "-b", target, &author_str],
Some(target.to_string()),
),
};
Checkout::add(git, repo, author, &args, created).map_err(Error::Git)
}
fn commit_message(name: &str, origin: &Origin) -> String {
let target = config_ref(name);
match origin {
Origin::Advance => format!("config: advance [{target}]"),
Origin::Fork { source } => format!("config: fork {} [{target}]", config_ref(source)),
Origin::Orphan => format!("config: init [{target}]"),
}
}
#[cfg(test)]
mod tests;
#[cfg(test)]
mod tests_descriptions;
#[cfg(test)]
mod tests_lineage;
#[cfg(test)]
mod tests_stub;
#[cfg(test)]
mod tests_teardown;