ghee 0.6.1

That thin layer of data change management over the filesystem
Documentation
use std::{
    cmp::Ordering,
    fs::File,
    io::Read,
    path::{Path, PathBuf},
};

use anyhow::Result;
use sha2::{Digest, Sha256};
use walkdir::WalkDir;

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

/**
    Statuses: same, added, removed, modified
*/
pub fn status(path: &PathBuf) -> Result<()> {
    if let Some(table_info) = containing_table_info(&path)? {
        let table_path = table_info.path_abs();
        let table_path_rel_to_curdir = table_path.relative_to_curdir_if_possible();

        let mut working_it = WalkDir::new(&table_path)
            .sort_by_file_name()
            .into_iter()
            .filter_entry(|e| !is_hidden(e))
            .map(|e| e.unwrap());

        if let Ok(commit) = xattr_value(&table_path, &XATTR_HEAD) {
            // A prior commit exists; compare to it

            // Buffer for file reading, for hashes
            let mut buf: Vec<u8> = vec![0; 2048];

            let snapshot_path = table_snapshot_path(&table_path, commit);

            let mut snapshot_it = WalkDir::new(&snapshot_path)
                .sort_by_file_name()
                .into_iter()
                .filter_entry(|e| !is_hidden(e))
                .map(|e| e.unwrap());

            let mut working_latest = working_it.next();
            let mut snapshot_latest = snapshot_it.next();

            while working_latest.is_some() || snapshot_latest.is_some() {
                if let Some(working_entry) = working_latest.as_ref() {
                    let working_entry_path = working_entry.path().to_path_buf();
                    let working_entry_path_rel = working_entry_path.relativize(table_path).unwrap();
                    let working_entry_path_rerel = working_entry_path_rel
                        .resolve_curdir(&table_path_rel_to_curdir)
                        .unwrap();

                    if let Some(snapshot_entry) = snapshot_latest.as_ref() {
                        // working entry and snapshot entry:
                        // based on filenames and hashes, do the right thing
                        let snapshot_entry_path = snapshot_entry.path().to_path_buf();
                        let snapshot_entry_path_rel =
                            snapshot_entry_path.relativize(&snapshot_path).unwrap();
                        let snapshot_entry_path_rerel = snapshot_entry_path_rel
                            .resolve_curdir(&table_path_rel_to_curdir)
                            .unwrap();

                        // println!(
                        //     "Comparing {} {}",
                        //     working_entry_path_rel.display(),
                        //     snapshot_entry_path_rel.display()
                        // );
                        match working_entry_path_rel.cmp(&snapshot_entry_path_rel) {
                            Ordering::Less => {
                                // working < snapshot: working added; advance working
                                // println!(
                                //     "working {} < snapshot {}; advancing working",
                                //     working_entry_path_rel.display(),
                                //     snapshot_entry_path_rel.display()
                                // );
                                working_latest = working_it.next();

                                println!("+{}", working_entry_path_rerel.display());
                            }
                            Ordering::Equal => {
                                // Same/modified: advance both
                                // println!(
                                //     "working {} == snapshot {}; advancing both",
                                //     working_entry_path_rel.display(),
                                //     snapshot_entry_path_rel.display()
                                // );
                                working_latest = working_it.next();
                                snapshot_latest = snapshot_it.next();

                                let changed = if snapshot_entry_path.is_dir() {
                                    if working_entry_path.is_dir() {
                                        // both dirs: do nothing
                                        false
                                    } else {
                                        // dir became file
                                        true
                                    }
                                } else {
                                    if working_entry_path.is_dir() {
                                        // file became dir
                                        true
                                    } else {
                                        // both files: compare hashes
                                        let working_entry_hash =
                                            hash_file(&working_entry_path, &mut buf)?;
                                        let snapshot_entry_hash =
                                            hash_file(&snapshot_entry_path, &mut buf)?;

                                        working_entry_hash != snapshot_entry_hash
                                    }
                                };

                                if changed {
                                    println!("m{}", working_entry_path_rerel.display());
                                }
                            }
                            Ordering::Greater => {
                                // working > snapshot: snapshot deleted; advance snapshot
                                // println!(
                                //     "working {} > snapshot {}; advancing snapshot",
                                //     working_entry_path_rel.display(),
                                //     snapshot_entry_path_rel.display()
                                // );
                                snapshot_latest = snapshot_it.next();

                                println!("-{}", snapshot_entry_path_rerel.display());
                            }
                        }
                    } else {
                        // working entry, but not snapshot entry: it's a new item
                        // same as working < snapshot: working added; advance working
                        // println!(
                        //     "working {} but not snapshot; advancing working",
                        //     working_entry_path_rel.display(),
                        // );
                        working_latest = working_it.next();

                        println!("+{}", working_entry_path_rerel.display());
                    }
                } else if let Some(snapshot_entry) = snapshot_latest {
                    // snapshot entry, but not working entry
                    // same as working > snapshot: snapshot deleted, advance snapshot
                    snapshot_latest = snapshot_it.next();

                    // snapshot entry, but no working entyr: it was removed
                    let snapshot_entry_path = snapshot_entry.path().to_path_buf();
                    let snapshot_entry_path_rel =
                        snapshot_entry_path.relativize(&snapshot_path).unwrap();
                    let snapshot_entry_path_rerel = snapshot_entry_path_rel
                        .resolve_curdir(&table_path_rel_to_curdir)
                        .unwrap();

                    // println!(
                    //     "snapshot {} but not working; advancing snapshot",
                    //     snapshot_entry_path_rel.display()
                    // );
                    println!("-{}", snapshot_entry_path_rerel.display());
                } else {
                    // neither snapshot entry nor working entry: this should never happen
                    unreachable!(
                        "This branch should never be reached, based on the while condition"
                    );
                }

                // println!(
                //     "End of iteration working {:?} snapshot {:?}",
                //     working_latest, snapshot_latest
                // );
            } // end while
        } else {
            // No prior commit exists; list the contents of this table all as added

            for entry in working_it {
                let entry_path = entry.path().to_path_buf();
                let entry_display = entry_path.relative_to_curdir_if_possible();
                println!("+{}", entry_display.display());
            }
        }
    } else {
        println!("No table found");
    }

    Ok(())
}

fn hash_file<P: AsRef<Path>>(path: P, buf: &mut [u8]) -> Result<Vec<u8>> {
    let mut hasher = Sha256::new();

    let mut r = File::open(path)?;

    loop {
        let n = r.read(buf)?;
        if n == 0 {
            break;
        }
        hasher.update(&buf[..n]);
    }

    Ok(hasher.finalize().as_slice().to_vec())
}

#[cfg(test)]
mod test {
    use std::{
        env::current_dir,
        fs::create_dir,
        path::{Path, PathBuf},
        thread::sleep,
        time::Duration,
    };

    use clap::Parser;
    use ghee_cli::{Cli, Commands};
    use ghee_lang::Key;

    use crate::{
        cmd::create,
        test_support::{CurrentDirGuard, TempDirAuto},
    };

    use super::status;

    #[test]
    fn test_status_relative_path() {
        let dir = TempDirAuto::new("ghee-test-status-relative-path");

        let prior = current_dir().unwrap();
        let prior_guard = CurrentDirGuard::new(&dir);

        let child = dir.push("child");

        let key = Key::from_string("direction");

        create(&child, &key, false).unwrap();

        let relpath = Path::new("./child").to_path_buf();

        status(&relpath).unwrap();

        // Keep guard alive
        assert_eq!(prior_guard.prior_dir, prior);
    }

    #[test]
    fn test_status_cli_relpath() {
        let dir1 = TempDirAuto::new("ghee-test-status-cli-relpath");
        let dir2 = dir1.push("dbs");
        let dir3 = dir2.push("pizza");

        create_dir(&dir2).unwrap();

        let key = Key::from_string("topping");
        create(&dir3, &key, false).unwrap();

        let prior = current_dir().unwrap();
        let prior_guard = CurrentDirGuard::new(&dir1);

        // Let the dir change propagate?
        sleep(Duration::from_millis(3000));

        let cmd = vec!["ghee", "status", "./dbs/pizza"];
        match Cli::try_parse_from(cmd).unwrap().command.unwrap() {
            Commands::Status { path } => {
                let path = path.unwrap();
                assert_eq!(path, PathBuf::from("./dbs/pizza"));

                status(&path).unwrap();
            }
            _ => panic!(),
        }

        assert_eq!(current_dir().unwrap(), *dir1);

        // Keep guard alive
        assert_eq!(prior_guard.prior_dir, prior);
    }
}