cargo-husky 0.0.6

husky for cargo
use fs::File;
use io::BufRead;
use io::Read;
use os::unix::fs::OpenOptionsExt;
use path::{Path, PathBuf};
use std::{env, fmt, fs, io, os, path};

enum Error {
    GitDirNotFound,
    Io(io::Error),
    OutDir(env::VarError),
}

type Result<T> = std::result::Result<T, Error>;

impl From<io::Error> for Error {
    fn from(error: io::Error) -> Error {
        Error::Io(error)
    }
}

impl From<env::VarError> for Error {
    fn from(error: env::VarError) -> Error {
        Error::OutDir(error)
    }
}

impl fmt::Debug for Error {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let msg = match self {
            Error::GitDirNotFound => format!(
                ".git directory was not found in '{}' or its parent directories",
                env::var("OUT_DIR").unwrap_or("".to_string()),
            ),
            Error::Io(inner) => format!("IO error: {}", inner),
            Error::OutDir(env::VarError::NotPresent) => unreachable!(),
            Error::OutDir(env::VarError::NotUnicode(msg)) => msg.to_string_lossy().to_string(),
        };
        write!(f, "{}", msg)
    }
}

fn resolve_gitdir() -> Result<PathBuf> {
    let dir = env::var("OUT_DIR")?;
    let mut dir = PathBuf::from(dir);
    if !dir.has_root() {
        dir = fs::canonicalize(dir)?;
    }
    loop {
        let gitdir = dir.join(".git");
        if gitdir.is_dir() {
            return Ok(gitdir);
        }
        if gitdir.is_file() {
            let mut buf = String::new();
            File::open(gitdir)?.read_to_string(&mut buf)?;
            let gitdir = PathBuf::from(buf.trim_right_matches("\n\r"));
            if !gitdir.is_dir() {
                return Err(Error::GitDirNotFound);
            }
            return Ok(gitdir);
        }
        if !dir.pop() {
            return Err(Error::GitDirNotFound);
        }
    }
}

fn hook_already_exists(hook: &Path) -> bool {
    let f = match File::open(hook) {
        Ok(f) => f,
        Err(..) => return false,
    };
    match io::BufReader::new(f).lines().nth(2) {
        None | Some(Err(..)) => false,
        Some(Ok(line)) => {
            let ver_comment = format!("set by cargo-husky v{}", env!("CARGO_PKG_VERSION"));
            line.contains(&ver_comment)
        }
    }
}

fn write_script<W: io::Write>(w: &mut W) -> Result<()> {
    let script = {
        let mut s = String::new();
        if cfg!(feature = "run-cargo-test") {
            s += "\necho '+cargo test'\ncargo test";
        }
        if cfg!(feature = "run-cargo-clippy") {
            s += "\necho '+cargo clippy'\ncargo clippy";
        }
        s
    };

    writeln!(
        w,
        r#"#!/bin/sh
#
# This hook was set by cargo-husky v{}: {}
# Generated by script {}{}build.rs
# Output at {}
#

set -e
{}"#,
        env!("CARGO_PKG_VERSION"),
        env!("CARGO_PKG_HOMEPAGE"),
        env!("CARGO_MANIFEST_DIR"),
        path::MAIN_SEPARATOR,
        env::var("OUT_DIR").unwrap_or("".to_string()),
        script
    )?;
    Ok(())
}

#[cfg(target_os = "win32")]
fn create_script(path: &Path) -> io::Result<File> {
    fs::create(path)
}

#[cfg(not(target_os = "win32"))]
fn create_script(path: &Path) -> io::Result<File> {
    fs::OpenOptions::new()
        .write(true)
        .create(true)
        .truncate(true)
        .mode(0o755)
        .open(path)
}

fn install(hook: &str) -> Result<()> {
    let hook_path = {
        let mut p = resolve_gitdir()?;
        p.push("hooks");
        p.push(hook);
        p
    };
    if !hook_already_exists(hook_path.as_path()) {
        let mut f = create_script(hook_path.as_path())?;
        write_script(&mut f)?;
    }
    Ok(())
}

fn main() -> Result<()> {
    if cfg!(feature = "prepush-hook") {
        install("pre-push")?;
    }
    if cfg!(feature = "precommit-hook") {
        install("pre-commit")?;
    }
    if cfg!(feature = "postmerge-hook") {
        install("post-merge")?;
    }
    Ok(())
}