use crate::manifest::Manifest;
use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
#[derive(Debug, Clone)]
pub struct Entry {
pub rel: PathBuf,
pub src: PathBuf,
pub dest: PathBuf,
pub state: State,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum State {
Linked,
Missing,
Wrong { current: PathBuf },
Occupied,
}
impl State {
pub fn needs_change(&self) -> bool {
!matches!(self, State::Linked)
}
}
pub struct Plan {
pub entries: Vec<Entry>,
}
impl Plan {
pub fn build(root: &Path, home: &Path, manifest: &Manifest) -> Result<Self> {
let mut entries = Vec::new();
for target in manifest.targets() {
let abs = root.join(target);
if !abs.exists() {
continue;
}
if abs.is_file() {
Self::push(&mut entries, root, home, manifest, Path::new(target))?;
continue;
}
for e in WalkDir::new(&abs).into_iter().filter_map(Result::ok) {
if !e.file_type().is_file() {
continue;
}
let rel = e
.path()
.strip_prefix(root)
.context("path escaped repository root")?;
Self::push(&mut entries, root, home, manifest, rel)?;
}
}
entries.sort_by(|a, b| a.rel.cmp(&b.rel));
Ok(Plan { entries })
}
fn push(
entries: &mut Vec<Entry>,
root: &Path,
home: &Path,
manifest: &Manifest,
rel: &Path,
) -> Result<()> {
if manifest.is_ignored(rel) {
return Ok(());
}
let src = root.join(rel);
let dest = home.join(rel);
let state = inspect(&src, &dest);
entries.push(Entry {
rel: rel.to_path_buf(),
src,
dest,
state,
});
Ok(())
}
pub fn changes(&self) -> impl Iterator<Item = &Entry> {
self.entries.iter().filter(|e| e.state.needs_change())
}
}
fn inspect(src: &Path, dest: &Path) -> State {
match std::fs::symlink_metadata(dest) {
Err(_) => State::Missing,
Ok(meta) => {
if !meta.file_type().is_symlink() {
return State::Occupied;
}
match std::fs::read_link(dest) {
Ok(current) if current == src => State::Linked,
Ok(current) => State::Wrong { current },
Err(_) => State::Occupied,
}
}
}
}