ghee 0.6.1

That thin layer of data change management over the filesystem
Documentation
use std::{
    fs::{remove_dir, remove_file},
    path::PathBuf,
};

use anyhow::Result;
use path_absolutize::Absolutize;
use reflink::reflink_or_copy;
use thiserror::Error;
use walkdir::WalkDir;

use crate::{
    containing_table_info, is_hidden,
    paths::{table_snapshot_path, PathBufExt},
    XATTR_HEAD,
};

#[derive(Copy, Clone)]
pub enum NewFileHandling {
    Keep,
    Delete,
}

#[derive(Error, Debug)]
pub enum RestoreErr {
    #[error("No prior commit known for table {0:?}")]
    NoPriorCommit(PathBuf),
    #[error("{0:?} is not contained by a table")]
    PathNotContainedByTable(PathBuf),
}

/**
 * Return a path to its state as of the commit with uuid specified in `commit`
 */
pub fn reset_path(
    path: &PathBuf,
    commit: &str,
    recursive: bool,
    verbose: bool,
    new_file_handling: NewFileHandling,
) -> Result<()> {
    let max_depth = if recursive { usize::MAX } else { 0 };

    let info =
        containing_table_info(path)?.ok_or(RestoreErr::PathNotContainedByTable(path.clone()))?;
    let table_path = info.path_abs();

    let snapshot_path = table_snapshot_path(table_path, commit);

    for entry in WalkDir::new(path)
        .max_depth(max_depth)
        .into_iter()
        .filter_entry(|e| !is_hidden(e))
    {
        let entry = entry?;

        let path = entry.path().to_path_buf();

        let abs_path = path.absolutize().unwrap().to_path_buf();

        let rel_path = abs_path.relativize(table_path)?;

        let snapshot_file_path = {
            let mut p = snapshot_path.clone();
            p.push(&rel_path);
            p
        }
        .absolutize()
        .unwrap()
        .to_path_buf();

        if abs_path.exists() {
            if snapshot_file_path.exists() {
                // Both paths exist
                // Now handle files vs directories
                if snapshot_file_path.is_dir() {
                    if abs_path.is_dir() {
                        // do nothing - both paths are directories
                    } else {
                        // Replace file with directory
                        remove_file(&abs_path)?;

                        let working_dest_dir = &abs_path;

                        for entry in WalkDir::new(&snapshot_file_path).into_iter() {
                            let entry = entry?;

                            let snapshot_rel = entry
                                .path()
                                .to_path_buf()
                                .relativize(&snapshot_file_path)
                                .unwrap();
                            let working = snapshot_rel.resolve_curdir(working_dest_dir).unwrap();

                            reflink_or_copy(entry.path(), &working)?;
                        }

                        if verbose {
                            println!("r{}", path.display());
                        }
                    }
                } else {
                    // Snapshot is file
                    // Copy over working either way
                    if abs_path.is_dir() {
                        remove_dir(&abs_path)?;
                        // } else {
                        //     remove_file(&abs_path)?;
                    }

                    reflink_or_copy(&snapshot_file_path, abs_path)?;
                    if verbose {
                        println!("r{}", path.display());
                    }
                }
            } else {
                // working exists; snapshot doesn't
                match new_file_handling {
                    NewFileHandling::Delete => {
                        remove_file(abs_path)?;
                        if verbose {
                            println!("-{}", path.display());
                        }
                    }
                    NewFileHandling::Keep => { /* do nothing, obvvvvvsly */ }
                }
            }
        } else {
            if snapshot_file_path.exists() {
                // snapshot exists; working doesn't
                reflink_or_copy(&snapshot_file_path, abs_path)?;
                if verbose {
                    println!("r{}", path.display());
                }
            } else {
                // neither working nor snapshot exists
                eprintln!(
                    "WTF? {} exists neither in working directory nor in most recent commit",
                    rel_path.display()
                );
            }
        }
    }
    Ok(())
}

/**
 * Return files to their state as of the most recent commit.
*/
pub fn restore(
    paths: &Vec<PathBuf>,
    recursive: bool,
    verbose: bool,
    new_file_handling: NewFileHandling,
) -> Result<()> {
    for path in paths {
        let info = containing_table_info(path)?
            .ok_or(RestoreErr::PathNotContainedByTable(path.clone()))?;
        let table_path = info.path_abs();

        let uuid_bytes = xattr::get(info.path_abs(), XATTR_HEAD.to_osstring())?
            .ok_or(RestoreErr::NoPriorCommit(table_path.clone()))?;

        let uuid = String::from_utf8(uuid_bytes)?;

        reset_path(path, uuid.as_str(), recursive, verbose, new_file_handling)?;
    }

    Ok(())
}