pub mod authoring;
pub(crate) mod checkout;
pub mod descriptions;
use crate::harness_root::Roots;
use include_dir::{Dir, include_dir};
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::process::Command;
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),
}
pub trait GitRunner {
fn run(&self, dest: &Path, args: &[&str]) -> io::Result<()>;
fn run_capture(&self, dest: &Path, args: &[&str]) -> io::Result<String>;
}
pub struct RealGit {
bin: PathBuf,
}
impl RealGit {
pub fn new() -> Self {
Self {
bin: PathBuf::from("git"),
}
}
}
impl Default for RealGit {
fn default() -> Self {
Self::new()
}
}
impl GitRunner for RealGit {
fn run(&self, dest: &Path, args: &[&str]) -> io::Result<()> {
self.run_capture(dest, args).map(|_| ())
}
fn run_capture(&self, dest: &Path, args: &[&str]) -> io::Result<String> {
let mut cmd = Command::new(&self.bin);
for var in INHERITED_GIT_ENV {
cmd.env_remove(var);
}
let out = cmd.arg("-C").arg(dest).args(args).output()?;
if !out.status.success() {
return Err(io::Error::other(format!(
"git {args:?} exited with {}: {}",
out.status,
String::from_utf8_lossy(&out.stderr).trim()
)));
}
Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
}
}
const INHERITED_GIT_ENV: &[&str] = &[
"GIT_DIR",
"GIT_WORK_TREE",
"GIT_INDEX_FILE",
"GIT_OBJECT_DIRECTORY",
"GIT_PREFIX",
"GIT_COMMON_DIR",
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
];
pub fn scaffold<G: GitRunner>(dest: &Path, roots: &Roots, git: &G) -> 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)?;
descriptions::snapshot(&roots.data, &author).map_err(ScaffoldError::Descriptions)?;
let msg = format!("config: init [{config_ref}]");
commit_checkout(git, &author, &msg).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<G: GitRunner>(git: &G, author: &Path, msg: &str) -> io::Result<bool> {
git.run(author, &["add", "-A"])?;
if git
.run_capture(author, &["status", "--porcelain"])?
.is_empty()
{
return Ok(false);
}
git.run(author, &["commit", "-m", msg])?;
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;