use anyhow::{Context, Result};
use chrono::DateTime;
use std::{
io::{self, BufRead, Write},
path::Path,
};
mod flake;
mod lockfile;
mod plan;
use lockfile::{LockFile, LockNode};
use plan::{Plan, TargetMapping};
fn main() -> Result<()> {
let Some(plan_path) = std::env::args_os().nth(1) else {
eprintln!("usage:");
eprintln!(" iqan plan.json");
eprintln!(" Sync all targets in plan to source.");
eprintln!(" See plan.json.sample and src/main.rs for guidance.");
eprintln!(" iqan source-flake");
eprintln!(" Sync target in cwd to source.");
std::process::exit(1);
};
let plan_path = std::fs::canonicalize(plan_path)?;
let plan_stat = std::fs::metadata(&plan_path)?;
if plan_stat.is_dir() {
println!("Source: {}", plan_path.to_string_lossy());
let source = LockFile::from_root(&plan_path)?;
check(
&source,
&std::fs::canonicalize(std::env::current_dir()?)?,
None,
)
} else {
let plan = Plan::from_path(&plan_path)?;
println!("Source: {}", &plan.source);
let source = LockFile::from_root(plan.source.as_ref())?;
for (root, inputs) in plan.targets {
check(&source, root.as_ref(), Some(inputs))?;
}
Ok(())
}
}
fn check(source: &LockFile, root: &Path, inputs: Option<TargetMapping>) -> Result<()> {
let root_display = root.to_string_lossy();
let mut target = LockFile::from_root(root)?;
println!();
println!("Target: {root_display}");
println!("--------{}", "-".repeat(root_display.len()));
let inputs = inputs.unwrap_or(TargetMapping::Identity(target.get_inputs()));
for (source_input, target_input) in inputs.iter() {
let Some(source_node) = source.get_node_via_root(source_input) else {
println!("[!] source lacked input {source_input}");
continue;
};
let Some(target_node) = target.get_node_via_root(target_input).cloned() else {
println!("[!] target lacked input {target_input}");
continue;
};
if source_node.original.as_ref().unwrap() != target_node.original.as_ref().unwrap() {
println!("input {source_input}->{target_input} has an original mismatch");
println!("in source: {:?}", source_node.original.as_ref().unwrap());
println!("in target: {:?}", target_node.original.as_ref().unwrap());
println!();
println!("If you choose to sync to source, I'll update flake.nix as well.");
match choice("(S)ync to source, or (I)gnore?", &['s', 'i'])? {
's' => {
sync_original(root, target_input, source_node, &target_node)?;
target = sync(&target, target_input, source_node)?;
}
'i' => {}
_ => unreachable!(),
}
continue;
}
let source_locked = source_node.locked.as_ref().unwrap();
let target_locked = target_node.locked.as_ref().unwrap();
if source_locked != target_locked {
let target_modified = target_locked.last_modified();
let source_modified = source_locked.last_modified();
match target_modified.cmp(&source_modified) {
std::cmp::Ordering::Less => {
println!("input {source_input}->{target_input} is behind source");
print_modified(&source_modified, &target_modified);
match choice("(S)ync to source, or (I)gnore?", &['s', 'i'])? {
's' => target = sync(&target, target_input, source_node)?,
'i' => {}
_ => unreachable!(),
}
}
std::cmp::Ordering::Equal => panic!("wat"),
std::cmp::Ordering::Greater => {
println!("input {source_input}->{target_input} is ahead of source (!)");
print_modified(&source_modified, &target_modified);
match choice("(S)ync to source, or (I)gnore?", &['s', 'i'])? {
's' => target = sync(&target, target_input, source_node)?,
'i' => {}
_ => unreachable!(),
}
}
}
continue;
}
println!("input {source_input}->{target_input} is synced");
}
Ok(())
}
fn print_modified(source_modified: &Option<usize>, target_modified: &Option<usize>) {
if let Some(source_modified) = source_modified {
println!(
"source: {source_modified} ({})",
DateTime::from_timestamp(*source_modified as i64, 0).unwrap()
);
} else {
println!("source: (no date)");
}
if let Some(target_modified) = target_modified {
println!(
"target: {target_modified} ({})",
DateTime::from_timestamp(*target_modified as i64, 0).unwrap()
);
} else {
println!("target: (no date)");
}
}
fn choice(query: &str, choices: &[char]) -> Result<char> {
loop {
print!("{query} ");
io::stdout().flush()?;
let mut buf = String::new();
_ = io::stdin().lock().read_line(&mut buf)?;
buf.make_ascii_lowercase();
let check = buf.trim();
if check.len() != 1 {
continue;
}
let check_char = check.chars().next().unwrap();
for choice in choices {
if check_char == *choice {
return Ok(check_char);
}
}
}
}
fn sync(target: &LockFile, input: &str, source_node: &LockNode) -> Result<LockFile> {
let updated_target = target.update_node_via_root(input, source_node);
let json = serde_json::to_string_pretty(&updated_target)?;
let mut f = std::fs::File::create(&target.path)?;
f.write_all(json.as_bytes())?;
f.flush()?;
Ok(updated_target)
}
fn sync_original(
root: &Path,
target_input: &str,
source_node: &LockNode,
target_node: &LockNode,
) -> Result<()> {
let path = root.join("flake.nix");
let flake_nix_content = std::fs::read_to_string(&path)
.with_context(|| format!("failed to read original flake.nix from {path:?}"))?;
let existing = format!("{:?}", target_node.original.as_ref().unwrap());
let new = format!("{:?}", source_node.original.as_ref().unwrap());
let updated = flake::replace_input(&flake_nix_content, target_input, &existing, &new)?;
std::fs::write(&path, updated)
.with_context(|| format!("failed to write new flake.nix at {path:?}"))?;
Ok(())
}