git-snapshot 0.1.1

Automate snapshotting git repositories
Documentation
use git_snapshot::repo_watcher::{RepoWatcher, WatchConfig};

use git_snapshot::Repo;
use log::{error, LevelFilter};
use serde_json::{from_reader, to_writer};
use structopt::StructOpt;

use anyhow::{anyhow, Error};

use std::env::current_dir;
use std::fmt::Display;
use std::fs::{create_dir_all, OpenOptions};
use std::io::ErrorKind;
use std::str::FromStr;

use pretty_env_logger::formatted_builder;
use std::path::{Path, PathBuf};
use std::thread::park;

fn default_config_path() -> Result<PathBuf, Error> {
    let home = dirs::home_dir().ok_or(anyhow!("Unable to get home directory"))?;
    Ok(home.join(
        [".config", "git-snapshot", "config.json"]
            .iter()
            .collect::<PathBuf>(),
    ))
}

#[derive(Debug)]
enum LogLevel {
    Off,
    Error,
    Warn,
    Info,
    Debug,
}
impl Default for LogLevel {
    fn default() -> Self {
        Self::Info
    }
}

impl Display for LogLevel {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match *self {
            Self::Off => write!(f, "off"),
            Self::Error => write!(f, "error"),
            Self::Warn => write!(f, "warn"),
            Self::Info => write!(f, "info"),
            Self::Debug => write!(f, "debug"),
        }
    }
}

impl FromStr for LogLevel {
    type Err = Error;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "off" => Ok(LogLevel::Off),
            "error" => Ok(LogLevel::Error),
            "warn" => Ok(LogLevel::Warn),
            "info" => Ok(LogLevel::Info),
            "debug" => Ok(LogLevel::Debug),
            _ => Err(anyhow!("Invalid log level: {}", s)),
        }
    }
}

impl LogLevel {
    fn to_level_filter(&self) -> LevelFilter {
        match self {
            Self::Off => LevelFilter::Off,
            Self::Error => LevelFilter::Error,
            Self::Warn => LevelFilter::Warn,
            Self::Info => LevelFilter::Info,
            Self::Debug => LevelFilter::Debug,
        }
    }
}

#[derive(Debug, StructOpt)]
#[structopt(name = "git-snapshot", about = "Automate snapshots for git")]
struct App {
    #[structopt(subcommand)]
    cmds: Option<AppCommands>,
    #[structopt(default_value, short, long, env = "GIT_SNAPSHOT_LOG_LEVEL")]
    log_level: LogLevel,
}

#[derive(Debug, StructOpt)]
enum AppCommands {
    Watch {
        #[structopt(short, long, env = "GIT_SNAPSHOT_CONFIG")]
        config: Option<PathBuf>,
        path: PathBuf,
    },
    Unwatch {
        #[structopt(short, long, env = "GIT_SNAPSHOT_CONFIG")]
        config: Option<PathBuf>,
        path: PathBuf,
    },
    StartWatcher {
        #[structopt(short, long, env = "GIT_SNAPSHOT_CONFIG")]
        config: Option<PathBuf>,
    },
}

#[tokio::main]
async fn main() {
    let app = App::from_args();
    formatted_builder()
        .filter_level(app.log_level.to_level_filter())
        .init();
    if let Err(err) = run(app) {
        error!("{:?}", err)
    }
}

fn run(app: App) -> Result<(), Error> {
    if let Some(cmds) = app.cmds {
        match cmds {
            AppCommands::StartWatcher { config } => {
                let _watcher = RepoWatcher::with_config(config.unwrap_or(default_config_path()?))?;
                park();
            }
            AppCommands::Watch { config, path } => {
                let p = config.unwrap_or(default_config_path()?);
                let mut config = load_config(&p)?;
                config.add_repo(path)?;
                save_config(&p, &config)?;
            }
            AppCommands::Unwatch { config, path } => {
                let p = config.unwrap_or(default_config_path()?);
                let mut config = load_config(&p)?;
                config.remove_repo(path)?;
                save_config(&p, &config)?;
            }
        }
    } else {
        let cwd = current_dir()?;
        let repo = Repo::from_path(cwd)?;
        repo.snapshot()?;
    }
    Ok(())
}

fn load_config(p: &Path) -> Result<WatchConfig, Error> {
    match OpenOptions::new().read(true).open(p) {
        Ok(f) => from_reader(f).map_err(From::from),
        Err(err) => {
            if err.kind() == ErrorKind::NotFound {
                Ok(WatchConfig::default())
            } else {
                Err(err.into())
            }
        }
    }
}

fn save_config(p: &Path, config: &WatchConfig) -> Result<(), Error> {
    create_dir_all(p.parent().ok_or(anyhow!("Invalid config path"))?)?;
    let f = OpenOptions::new()
        .write(true)
        .truncate(true)
        .create(true)
        .open(p)?;
    to_writer(f, config).map_err(From::from)
}