use crate::cli_outbound::{Chunk, Cli, ExitInfo};
use crate::config_edit::apply::EDITOR_APPLY_FLAG;
use crate::opslog::{self, OpEntry, Origin};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
const SUBCOMMAND_CONFIG: &str = "config";
const ENV_EDITOR: &str = "EDITOR";
const ENV_EDIT_SRC: &str = "YOG_EDIT_SRC";
use crate::scratch::STALE_SECS;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EditOrigin {
Advance,
Fork { source: String },
Orphan,
}
impl EditOrigin {
fn flags(&self) -> Vec<String> {
match self {
EditOrigin::Advance => Vec::new(),
EditOrigin::Fork { source } => vec!["--from".to_string(), source.clone()],
EditOrigin::Orphan => vec!["--orphan".to_string()],
}
}
}
fn sh_quote(s: &str) -> String {
format!("'{}'", s.replace('\'', "'\\''"))
}
pub fn editor_env_value(yog_binary: &Path) -> String {
format!(
"{} {EDITOR_APPLY_FLAG}",
sh_quote(&yog_binary.display().to_string())
)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DraftFile {
pub rel_path: String,
pub bytes: Vec<u8>,
}
pub fn next_nonce() -> String {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
format!("{}-{n}", std::process::id())
}
pub fn stage_files(
staging_root: &Path,
nonce: &str,
files: &[DraftFile],
) -> std::io::Result<PathBuf> {
let dir = staging_root.join(nonce);
std::fs::create_dir_all(&dir)?;
for f in files {
let dest = dir.join(&f.rel_path);
std::fs::create_dir_all(dest.parent().unwrap_or(&dir))?;
std::fs::write(&dest, &f.bytes)?;
}
Ok(dir)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EditPlan {
argv: Vec<String>,
editor: String,
staging: String,
}
impl EditPlan {
pub fn compose(
yog_binary: &Path,
workspace: &Path,
name: &str,
origin: &EditOrigin,
staging_dir: &Path,
) -> Self {
let mut argv = vec![
SUBCOMMAND_CONFIG.to_string(),
workspace.display().to_string(),
name.to_string(),
];
argv.extend(origin.flags());
Self {
argv,
editor: editor_env_value(yog_binary),
staging: staging_dir.display().to_string(),
}
}
#[cfg(test)]
pub(crate) fn argv(&self) -> &[String] {
&self.argv
}
pub fn env(&self) -> [(&str, &str); 2] {
[(ENV_EDITOR, &self.editor), (ENV_EDIT_SRC, &self.staging)]
}
}
pub fn drive(
cli: &Cli,
workspace: &Path,
plan: &EditPlan,
ts: &str,
state_root: &Path,
origin: Origin,
) -> OpEntry {
let args: Vec<&str> = plan.argv.iter().map(String::as_str).collect();
let (stdout, stderr, exit) = match cli.run_env(&plan.env(), &args) {
Ok(stream) => collect(stream),
Err(e) => (String::new(), e.to_string(), -1),
};
let mut argv = vec![cli.binary().display().to_string()];
argv.extend(plan.argv.iter().cloned());
let entry = OpEntry {
ts: ts.to_string(),
argv,
cwd: workspace.display().to_string(),
exit,
stdout,
stderr,
origin,
};
let _ = opslog::append(state_root, &entry);
entry
}
fn collect(stream: crate::cli_outbound::Stream) -> (String, String, i32) {
let (mut out, mut err, mut code) = (Vec::new(), Vec::new(), -1);
for chunk in stream {
match chunk {
Chunk::Stdout(b) => out.extend(b),
Chunk::Stderr(b) => err.extend(b),
Chunk::Exited(ExitInfo::Code(c)) => code = c,
Chunk::Exited(_) => code = -1,
}
}
(
String::from_utf8_lossy(&out).into_owned(),
String::from_utf8_lossy(&err).into_owned(),
code,
)
}
pub fn stale_staging(now_secs: i64, dirs: &[(PathBuf, i64)]) -> Vec<PathBuf> {
dirs.iter()
.filter(|(_, mtime)| now_secs - mtime > STALE_SECS)
.map(|(p, _)| p.clone())
.collect()
}
pub fn sweep_staging(stage_root: &Path, now_secs: i64) -> Vec<PathBuf> {
let stale = stale_staging(now_secs, &staging_dirs(stage_root));
for dir in &stale {
let _ = std::fs::remove_dir_all(dir);
}
stale
}
fn staging_dirs(stage_root: &Path) -> Vec<(PathBuf, i64)> {
use std::os::unix::fs::MetadataExt;
let Ok(entries) = std::fs::read_dir(stage_root) else {
return Vec::new();
};
let mut out = Vec::new();
for entry in entries.flatten() {
if let Ok(meta) = entry.metadata()
&& meta.is_dir()
{
out.push((entry.path(), meta.mtime()));
}
}
out
}
#[cfg(test)]
mod tests;