//! git hook manager tool use anyhow::{ bail, Result, }; use log::*; use shared::find_root; #[cfg(not(windows))] use std::os::unix::fs::{ symlink, PermissionsExt, }; #[cfg(windows)] use std::os::windows::fs::symlink_file; use std::{ env, fs, io::Write, path::Path, }; const HOOKS: [&str; 18] = [ "applypatch-msg", "post-applypatch", "pre-commit", "prepare-commit-msg", "commit-msg", "post-commit", "pre-rebase", "post-checkout", "post-merge", "pre-push", "pre-receive", "update", "post-receive", "post-update", "push-to-checkout", "pre-auto-gc", "post-rewrite", "sendemail-validate", ]; #[derive(structopt::StructOpt)] enum Args { /// Initialize the repo to use hooked Init(Language), /// Link pre existing hooks to your .git folder Link, } /// Which language the repo should be initialized with for hooks #[derive(Clone, Copy, structopt::StructOpt)] enum Language { /// Use Bash for your git hooks Bash, /// Use Python 3 for your git hooks Python, /// Use Ruby for your git hooks Ruby, } #[paw::main] fn main(args: Args) { env::var("RUST_LOG") .ok() .map_or_else(|| env::set_var("RUST_LOG", "info"), drop); pretty_env_logger::init(); if let Err(e) = match args { Args::Init(lang) => init(lang), Args::Link => link(), } { error!("{}", e); std::process::exit(1); } } fn init(lang: Language) -> Result<()> { let root = find_root()?; let git_hooks = &root.join(".git").join("hooks"); debug!("git_hooks base path: {}", git_hooks.display()); let root = root.join(".dev-suite").join("hooked"); debug!("root base path: {}", root.display()); let wrapper_dir = &root.join("wrapper"); fs::create_dir_all(&wrapper_dir)?; for hook in &HOOKS { let mut path = (&root).join(hook); debug!("dev-suite hook path: {}", path.display()); let git_hook = &git_hooks.join(hook); debug!("git_hook path: {}", git_hook.display()); let mut wrapper_hook = (&wrapper_dir).join(hook); let _ = wrapper_hook.set_extension("sh"); let _ = match lang { Language::Bash => path.set_extension("sh"), Language::Python => path.set_extension("py"), Language::Ruby => path.set_extension("rb"), }; if path.exists() { debug!("git hook {} already exists. Skipping creation.", hook); } else { debug!("Creating dev-suite hook."); let mut file = fs::File::create(&path)?; let mut wrapper = fs::File::create(&wrapper_hook)?; trace!("File created."); #[cfg(not(windows))] { let mut perms = file.metadata()?.permissions(); let mut wrapper_perms = wrapper.metadata()?.permissions(); debug!("Setting dev-suite hook to be executable."); perms.set_mode(0o755); wrapper_perms.set_mode(0o755); file.set_permissions(perms)?; wrapper.set_permissions(wrapper_perms)?; trace!("Permissions were set."); } match lang { Language::Bash => { file.write_all(b"#!/usr/bin/env bash")?; wrapper.write_all( format!( "#!C:\\Program Files\\Git\\bin\\sh.exe\n\ bash.exe .dev-suite/hooked/{}.sh\n", hook ) .as_bytes(), )?; } Language::Python => { file.write_all(b"#!/usr/bin/env python3")?; wrapper.write_all( format!( "#!C:\\Program Files\\Git\\bin\\sh.exe\n\ py.exe .dev-suite/hooked/{}.py\n", hook ) .as_bytes(), )?; } Language::Ruby => { file.write_all(b"#!/usr/bin/env ruby")?; wrapper.write_all( format!( "#!C:\\Program Files\\Git\\bin\\sh.exe\n\ ruby.exe .dev-suite/hooked/{}.rb\n", hook ) .as_bytes(), )?; } } debug!("Writing data to file."); debug!("Created git hook {}.", hook); } #[cfg(not(windows))] let link_path = path.canonicalize()?; #[cfg(windows)] let link_path = wrapper_hook.canonicalize()?; inner_link(&link_path, &git_hook, hook)?; } info!( "Created and symlinked tickets to .git/hooks from {}.", root.display() ); #[cfg(windows)] { warn!("Make sure to add the hooks into git with 'git add --chmod=+x .dev-suite\\hooked'"); warn!("If you don't they won't be set as executable on unix systems"); } Ok(()) } fn link() -> Result<()> { let root = find_root()?; let git_hooks = &root.join(".git").join("hooks"); debug!("git_hooks base path: {}", git_hooks.display()); let root = root.join(".dev-suite").join("hooked"); debug!("root base path: {}", root.display()); for hook in &HOOKS { let path = { #[cfg(windows)] let mut path = root.join("wrapper").join(hook); #[cfg(not(windows))] let mut path = root.join(hook); debug!("PATH: {}", path.display()); let mut path_python = path.clone(); let _ = path_python.set_extension("py"); let mut path_ruby = path.clone(); let _ = path_ruby.set_extension("rb"); let mut path_bash = path.clone(); let _ = path_bash.set_extension("sh"); if path_python.exists() { path_python } else if path_ruby.exists() { path_ruby } else if path_bash.exists() { path_bash } else { let _ = path.set_extension(""); bail!( "The path {} does not exist. Have you initialized the repo to use hooked?", path.display() ); } }; let path = path.canonicalize()?; debug!("dev-suite hook path: {}", path.display()); let git_hook = &git_hooks.join(hook); debug!("git_hook path: {}", git_hook.display()); inner_link(&path, &git_hook, hook)?; } info!("Successfully symlinked all githooks to .git/hooks"); Ok(()) } fn inner_link(path: &Path, git_hook: &Path, hook: &str) -> Result<()> { if !git_hook.exists() { debug!("Symlinking git hook {}.", hook); #[cfg(not(windows))] symlink(&path, &git_hook)?; #[cfg(windows)] symlink_file(&path, &git_hook)?; debug!( "Symlinked git hook {} to {}", git_hook.display(), path.display() ); } Ok(()) }