rvcs 0.1.0

A rusty version control system.
Documentation
use flate2::read::GzDecoder;
use std::{
    fs,
    fs::File,
    io,
    io::{Read, Write},
    path::Path,
    time::{SystemTime, UNIX_EPOCH},
};

use chrono::prelude::*;
use sha2::{Digest, Sha256};

use crate::{error, get_current_branch, in_repo, ok};

pub fn pull(commit: &str) -> io::Result<()> {
    if !in_repo() {
        return Err(error!("Current directory is not a rvcs repository.")); 
    }

    let commits = fs::read_dir(&format!(".rvcs/commits/{}/", get_current_branch()?))?
        .collect::<Vec<_>>()
        .into_iter()
        .rev()
        .map(|e| e.unwrap().path().to_str().unwrap().to_owned())
        .collect::<Vec<_>>();
    let mut to_pull_path = "".to_owned();

    for i in 0..commits.len() {
        if &commits[i] == &format!(".rvcs/commits/{}/HEAD", get_current_branch()?) {
            continue;
        }
        let msg_path = &format!("{}/MESSAGE", commits[i]);
        let f_content = fs::read_to_string(msg_path)?;
        let first_line = f_content.split("\r\n").collect::<Vec<&str>>();
        let hash = first_line[0].split(" - ").collect::<Vec<&str>>()[0];


        if commit == hash || &hash[..8] == commit {
            to_pull_path = commits[i].clone();
            break;
        }
    }

    if &to_pull_path == "" {
        return Err(error!("Invalid commit hash.")); 
    }

    let _ = fs::read_dir(".")?
        .map(|e| -> io::Result<String> {
            let epath = e?.path();
            let path = &epath.to_str().unwrap();

            if path != &"./.rvcs" {
                if epath.is_dir() {
                    fs::remove_dir_all(&epath)?;
                } else {
                    fs::remove_file(&epath)?;
                }
            }
            Ok(path.to_string())
        }).collect::<Vec<_>>();

    let links_path =  &format!("{}/links", to_pull_path);
    let links = fs::read_to_string(links_path)?;
    let mut total = 0;
    for _ in (&links).lines() {
        total += 1;
    }
    for (i, line) in links.lines().enumerate() {
        let splited = line.split('|').collect::<Vec<_>>();

        if splited.len() != 2 {
            continue;
        }
        let f_path = &format!("{}/{}", to_pull_path, splited[1]);
        let mut buffer = vec![];
        File::open(f_path)?.read_to_end(&mut buffer)?;
        let mut to_write = vec![];
        GzDecoder::new(&*buffer).read_to_end(&mut to_write)?;
        File::create(splited[0])?.write_all(&to_write)?;
        
        print!("\r");
        for _ in 0..((i+1)/total*50) {
            print!("â–ˆ");
        }

        for _ in 0..((total - (i+1))/total*50) {
            print!("â–‘");
        }

        print!("\t{}/{}", i+1, total);
    }
    println!();
    println!("{} Sucessfully pulled commit files.", ok());

    Ok(())

}

pub fn reset(commit: &str, hard: bool) -> io::Result<()> {
    if !in_repo() {
        return Err(error!("Current directory is not a rvcs repository."));
    }

    let ancient = fs::read_to_string(&format!(".rvcs/commits/{}/HEAD", get_current_branch()?))?;

    let commits = fs::read_dir(&format!(".rvcs/commits/{}/", get_current_branch()?))?
        .collect::<Vec<_>>()
        .into_iter()
        .rev()
        .map(|e| e.unwrap().path().to_str().unwrap().to_owned())
        .collect::<Vec<_>>();
    let mut got_it = false;

    for i in 0..commits.len() {
        if &commits[i] == &format!(".rvcs/commits/{}/HEAD", get_current_branch()?) {
            continue;
        }

        let msg_path = &format!("{}/MESSAGE", commits[i]);

        let f_content = fs::read_to_string(msg_path)?;
        let first_line = f_content.split("\r\n").collect::<Vec<&str>>();
        let hash = first_line[0].split(" - ").collect::<Vec<&str>>()[0];


        if !got_it && (commit == hash || &hash[..8] == commit) {
            got_it = true;
            File::create(&format!(".rvcs/commits/{}/HEAD", get_current_branch()?))?.write_all(hash.as_bytes())?;
            if hard {
                continue;
            } else {
                break;
            }
        }
        if got_it && hard {
            fs::remove_dir_all(&commits[i])?;
        }
    }
    println!("{} Sucessfully reset from commit `{}` to `{}`.", ok(), &ancient[..8], &commit[..8]);
    Ok(())
}

pub fn log(details: bool) -> io::Result<()> {
    if !in_repo() {
        return Err(error!("Current folder is not a rvcs repository"));
    }
    let head = fs::read_to_string(&format!(".rvcs/commits/{}/HEAD", get_current_branch()?))?;
    let content = fs::read_dir(format!(".rvcs/commits/{}", get_current_branch()?))?;
    let elements = content
        .collect::<Vec<_>>()
        .into_iter()
        .rev()
        .collect::<Vec<_>>();
    for element in elements {
        let epath = element?.path();
        if !epath.is_dir() {
            continue;
        }
        let path = epath.to_str().unwrap();

        let m_content = fs::read_to_string(&format!("{}/MESSAGE", path))?;
        let splited = m_content.split("\r\n").collect::<Vec<_>>();

        if splited.len() != 3 {
            continue;
        }

        let first_ln = splited[0].split(" - ").collect::<Vec<_>>();
        if first_ln.len() != 2 {
            continue;
        }

        let hash = first_ln[0];
        let message = first_ln[1];
        let body = splited[1];
        let date = splited[2];
        if details {
            println!(
                "{}{} - \x1b[0;35m{}\x1b[0m",
                if hash == head {
                    "(\x1b[0;33mHEAD\x1b[0m) "
                } else {
                    "       "
                },
                message,
                hash,
                );
            println!("{}", body);
            println!("Date: {}", date);
        } else {
            println!(
                "{}\x1b[0;35m{}\x1b[0m - {}",
                if hash == head {
                    "(\x1b[0;33mHEAD\x1b[0m) "
                } else {
                    "       "
                },
                &hash[..8],
                message
                );
        }
    }

    Ok(())
}

pub fn commit(message: &str, body: &str) -> io::Result<()> {
    if !in_repo() {
        return Err(error!("Current folder is not a rvcs repository."));
    }
    let branch_folder = &format!(".rvcs/commits/{}", get_current_branch()?);

    if !Path::new(branch_folder).exists() {
        return Err(error!(
                (&format!(
                        "No `{}` branch in the current repository.",
                        get_current_branch()?
                        ))
                ));
    }

    let files = fs::read_dir(".rvcs/objects/")?.collect::<Vec<_>>();

    if files.len() < 2
        /* link file and at least one file */
    {
        return Err(error!(
                "No changes since last commit. Consider running `rvcs add <files>` before commiting."
                ));
    }

    let date = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_secs();
    let mut hasher = Sha256::new();
    hasher.update(format!("{}{}{}", date, message, body).as_bytes());
    let hash = &format!("{:x}", hasher.finalize());
    let folder_name = format!("{}/{}{}", branch_folder, date, hash);

    fs::create_dir(&folder_name)?;

    for entry in fs::read_dir(".rvcs/objects/")? {
        let epath = entry?.path();
        let path = epath.to_str().unwrap();

        let mut buffer = vec![];
        File::open(path)?.read_to_end(&mut buffer)?;
        File::create(&path.replace(".rvcs/objects", &folder_name))?.write_all(&buffer)?;
    }
    // <nb_secs_since_1970><hash_date+message+body>
    File::create(&format!("{}/MESSAGE", folder_name))?.write_all(
        format!(
            "{} - {}\r\n{}\r\n{}",
            hash,
            message,
            body,
            Local::now().format("%a %b %d %Y %H:%M:%S")
            )
        .as_bytes(),
        )?;

    fs::remove_dir_all(".rvcs/objects/")?;
    fs::create_dir(".rvcs/objects/")?;

    File::create(&format!(".rvcs/commits/{}/HEAD", get_current_branch()?))?
        .write_all(hash.as_bytes())?;

    println!("{} Sucessfully commited changes into `{}`.", ok(), &hash[..8]);

    Ok(())
}