use std::path::{Path, PathBuf};
use std::process::Command;
#[derive(Debug, PartialEq, Eq)]
pub enum SyncAction {
Noop,
ReAdd(PathBuf),
Add(PathBuf),
}
pub fn decide_action<F>(
enabled: bool,
existed_before: bool,
path: &Path,
mut managed_probe: F,
) -> SyncAction
where
F: FnMut(&Path) -> bool,
{
if !enabled {
return SyncAction::Noop;
}
if existed_before {
if managed_probe(path) {
SyncAction::ReAdd(path.to_path_buf())
} else {
SyncAction::Noop
}
} else {
let mut current = path.parent();
while let Some(p) = current {
if p.exists() && managed_probe(p) {
return SyncAction::Add(path.to_path_buf());
}
current = p.parent();
}
SyncAction::Noop
}
}
fn is_managed_via_chezmoi(p: &Path) -> bool {
match Command::new("chezmoi").arg("source-path").arg(p).output() {
Ok(o) => o.status.success(),
Err(e) => {
eprintln!("\u{26a0} chezmoi source-path {} failed: {}", p.display(), e,);
false
}
}
}
fn is_chezmoi_available() -> bool {
Command::new("chezmoi")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
pub fn sync(enabled: bool, existed_before: bool, path: &Path) {
if !enabled {
return;
}
if !is_chezmoi_available() {
eprintln!(
"\u{26a0} options.chezmoi = true but `chezmoi` is not in PATH. \
Skipping sync for {} (install chezmoi or set chezmoi = false).",
path.display(),
);
return;
}
let action = decide_action(true, existed_before, path, is_managed_via_chezmoi);
match action {
SyncAction::Noop => {}
SyncAction::ReAdd(p) => run_chezmoi(&["re-add"], &p),
SyncAction::Add(p) => run_chezmoi(&["add"], &p),
}
}
fn run_chezmoi(args: &[&str], path: &Path) {
let mut cmd = Command::new("chezmoi");
cmd.args(args).arg(path);
match cmd.status() {
Ok(s) if s.success() => {}
Ok(s) => eprintln!(
"\u{26a0} chezmoi {} {} failed (exit {})",
args.join(" "),
path.display(),
s.code().unwrap_or(-1),
),
Err(e) => eprintln!(
"\u{26a0} chezmoi {} {} could not be spawned: {}",
args.join(" "),
path.display(),
e,
),
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_decide_action_disabled_is_noop() {
let tmp = tempdir().unwrap();
let file = tmp.path().join("config.toml");
std::fs::write(&file, "").unwrap();
let got = decide_action(false, true, &file, |_| true);
assert_eq!(got, SyncAction::Noop);
}
#[test]
fn test_decide_action_existing_managed_is_re_add() {
let tmp = tempdir().unwrap();
let file = tmp.path().join("config.toml");
std::fs::write(&file, "").unwrap();
let got = decide_action(true, true, &file, |p| p == file);
assert_eq!(got, SyncAction::ReAdd(file));
}
#[test]
fn test_decide_action_existing_not_managed_is_noop() {
let tmp = tempdir().unwrap();
let file = tmp.path().join("config.toml");
std::fs::write(&file, "").unwrap();
let got = decide_action(true, true, &file, |_| false);
assert_eq!(got, SyncAction::Noop);
}
#[test]
fn test_decide_action_new_file_ancestor_managed_is_add() {
let tmp = tempdir().unwrap();
let plugins = tmp.path().join("plugins");
std::fs::create_dir_all(&plugins).unwrap();
let new_file = plugins
.join("github.com")
.join("foo")
.join("bar")
.join("init.lua");
let plugins_clone = plugins.clone();
let got = decide_action(true, false, &new_file, move |p| p == plugins_clone);
assert_eq!(got, SyncAction::Add(new_file));
}
#[test]
fn test_decide_action_new_file_ancestor_not_managed_is_noop() {
let tmp = tempdir().unwrap();
let plugins = tmp.path().join("plugins");
std::fs::create_dir_all(&plugins).unwrap();
let new_file = plugins.join("foo").join("init.lua");
let got = decide_action(true, false, &new_file, |_| false);
assert_eq!(got, SyncAction::Noop);
}
#[test]
fn test_decide_action_file_exists_but_was_just_created_is_add() {
let tmp = tempdir().unwrap();
let plugins = tmp.path().join("plugins");
std::fs::create_dir_all(&plugins).unwrap();
let new_file = plugins.join("bar").join("init.lua");
std::fs::create_dir_all(new_file.parent().unwrap()).unwrap();
std::fs::write(&new_file, "-- new hook").unwrap();
let plugins_clone = plugins.clone();
let got = decide_action(true, false, &new_file, move |p| p == plugins_clone);
assert_eq!(got, SyncAction::Add(new_file));
}
}