pub mod authoring;
pub(crate) mod checkout;
pub mod descriptions;
mod git;
pub use git::{GitRunner, RealGit};
use crate::harness_root::Roots;
use include_dir::{Dir, include_dir};
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
pub static TEMPLATE: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/template");
pub const TEMPLATE_OVERRIDE_DIR: &str = "template";
#[derive(Debug, thiserror::Error)]
pub enum ScaffoldError {
#[error("destination {0} already exists and is not empty")]
DestNotEmpty(PathBuf),
#[error("destination {0} already exists and is not a directory")]
DestNotDir(PathBuf),
#[error("I/O error: {0}")]
Io(#[source] io::Error),
#[error("git error: {0}")]
Git(#[source] io::Error),
#[error("descriptions-always: {0}")]
Descriptions(#[source] descriptions::Error),
#[error(transparent)]
Facts(#[from] crate::facts::OverCap),
}
pub fn scaffold(dest: &Path, roots: &Roots, git: &dyn GitRunner) -> Result<(), ScaffoldError> {
check_dest(dest)?;
let repo = crate::workspace::repo_git(dest);
let config_ref = crate::workspace::config_ref(crate::workspace::DEFAULT_CONFIG_NAME);
let config_ref = config_ref.as_str();
fs::create_dir_all(&repo).map_err(ScaffoldError::Io)?;
let init_args = ["init", "--bare", "-b", config_ref];
git.run(&repo, &init_args).map_err(ScaffoldError::Git)?;
let author = checkout::path(dest);
let author_str = author.to_string_lossy().to_string();
let mut add_args = vec!["worktree", "add", "--orphan", "-b", config_ref];
add_args.push(author_str.as_str());
let checkout = checkout::Checkout::add(git, &repo, &author, &add_args, None)
.map_err(ScaffoldError::Git)?;
fs::create_dir_all(&author).map_err(ScaffoldError::Io)?;
TEMPLATE.extract(&author).map_err(ScaffoldError::Io)?;
overlay(&roots.config.join(TEMPLATE_OVERRIDE_DIR), &author).map_err(ScaffoldError::Io)?;
crate::facts::require_within_cap(&author)?;
descriptions::snapshot(&roots.data, &author).map_err(ScaffoldError::Descriptions)?;
let msg = format!("config: init [{config_ref}]");
commit_checkout(git, &author, &msg, false).map_err(ScaffoldError::Git)?;
checkout.landed().map_err(ScaffoldError::Git)?;
Ok(())
}
fn overlay(src: &Path, dst: &Path) -> io::Result<()> {
let entries = match fs::read_dir(src) {
Ok(entries) => entries,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
Err(e) => return Err(e),
};
for entry in entries {
let entry = entry?;
let to = dst.join(entry.file_name());
if entry.file_type()?.is_dir() {
fs::create_dir_all(&to)?;
overlay(&entry.path(), &to)?;
} else {
fs::copy(entry.path(), &to)?;
}
}
Ok(())
}
pub(crate) fn commit_checkout(
git: &dyn GitRunner,
author: &Path,
msg: &str,
amend: bool,
) -> io::Result<bool> {
git.run(author, &["add", "-A"])?;
if git
.run_capture(author, &["status", "--porcelain"])?
.is_empty()
{
return Ok(false);
}
let mut args = vec!["commit", "-m", msg];
if amend {
args.push("--amend");
}
git.run(author, &args)?;
Ok(true)
}
fn check_dest(dest: &Path) -> Result<(), ScaffoldError> {
match fs::read_dir(dest) {
Ok(mut entries) => {
if entries.next().is_some() {
Err(ScaffoldError::DestNotEmpty(dest.to_path_buf()))
} else {
Ok(())
}
}
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
Err(e) if e.kind() == io::ErrorKind::NotADirectory && dest.exists() => {
Err(ScaffoldError::DestNotDir(dest.to_path_buf()))
}
Err(e) => Err(ScaffoldError::Io(e)),
}
}
#[cfg(test)]
mod tests;
#[cfg(test)]
mod tests_descriptions;
#[cfg(test)]
mod tests_dest;
#[cfg(test)]
mod tests_override;
#[cfg(test)]
mod tests_realgit;
#[cfg(test)]
mod tests_scaffold;