codebase 0.11.0

Manage your codebase like a boss!
Documentation
use std::collections::BTreeMap;
use std::fs;
use std::io::Write;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;

use serde::Deserialize;
use walkdir::WalkDir;

use crate::git_util;

const HOOK_FILE: &str = "hook.json";
const HOOK_DIR: &str = "hooks";
const HOOK_REPO: &str = "https://github.com/codebase-rs/hooks.git";
const HOOK_SCRIPT: &str = "script";

#[derive(Deserialize, Clone)]
pub struct Hook {
    /// Hook description
    pub description: String,
    /// Hook author
    pub author: String,
}

/// Find an hook using his name
///
/// # Arguments
/// * `name` - the name of the hook
/// * `config_dir` - path to the .codebase-config directory
pub fn find<P: AsRef<Path>>(name: &str, config_dir: P) -> anyhow::Result<Option<Hook>> {
    let hooks = hooks(&config_dir)?;
    Ok(hooks
        .iter()
        .find(|(hook_name, _)| *hook_name == &name.to_string())
        .map(|(_, hook)| hook.clone()))
}

/// Get existing hooks.
/// This will refresh them.
///
/// # Arguments
/// * `config_dir` - path to the .codebase-config directory
pub fn hooks<P: AsRef<Path>>(config_dir: P) -> anyhow::Result<BTreeMap<String, Hook>> {
    // Clone hooks / update it
    let hooks_dir = config_dir.as_ref().join(HOOK_DIR);

    if !hooks_dir.exists() {
        git2::Repository::clone(HOOK_REPO, &hooks_dir)?;
    } else {
        let repo = git2::Repository::open(&hooks_dir)?;
        git_util::pull(&repo, "origin", "master")?;
    }

    let mut hooks: BTreeMap<String, Hook> = BTreeMap::new();

    for entry in WalkDir::new(&hooks_dir)
        .into_iter()
        .filter_map(Result::ok)
        .filter(|e| e.file_name().to_str().unwrap() == HOOK_FILE)
    {
        // Extract 'local' manifest directory
        let hooks_dir = hooks_dir.to_str().unwrap();
        let local_path: String = entry
            .path()
            .to_str()
            .unwrap()
            .replace(&format!("{}/", hooks_dir), "")
            .replace(&format!("/{}", HOOK_FILE), "");
        let hook_id = local_path;

        // Read manifest file
        let json = fs::read_to_string(&entry.path()).unwrap();
        let json: Hook = serde_json::from_str(&json).unwrap();

        hooks.insert(hook_id, json);
    }

    Ok(hooks)
}

/// Install given hook into given repository
/// Please note that at the moment, only pre-commit hook are allowed
///
/// # Arguments
/// * `hook_id` - the Hook ID
/// * `repo_path` - the Git repository path
/// * `config_dir` - path to the .codebase-config directory
pub fn install<A: AsRef<Path>, B: AsRef<Path>>(
    hook_id: &str,
    repo_path: A,
    config_dir: B,
) -> anyhow::Result<()> {
    // Refresh hooks before using them
    hooks(&config_dir)?;

    let src_path = config_dir
        .as_ref()
        .join(HOOK_DIR)
        .join(hook_id)
        .join(HOOK_SCRIPT);

    if !src_path.exists() {
        return Err(anyhow::anyhow!(format!(
            "No script found for hook `{}`",
            hook_id
        )));
    }

    let content = fs::read_to_string(src_path)?;

    let dst_path = repo_path.as_ref().join("hooks").join("pre-commit");
    let mut file = fs::OpenOptions::new()
        .write(true)
        .create(true)
        .truncate(true)
        .open(&dst_path)?;

    file.write_all(content.as_bytes())?;

    // Set permissions (executable bit)
    let mut permissions = file.metadata()?.permissions();
    permissions.set_mode(0o750);
    file.set_permissions(permissions)?;

    Ok(())
}