diff options
Diffstat (limited to 'hooked')
-rw-r--r-- | hooked/src/main.rs | 181 | ||||
-rw-r--r-- | hooked/tests/init.rs | 66 |
2 files changed, 211 insertions, 36 deletions
diff --git a/hooked/src/main.rs b/hooked/src/main.rs index 7e33499..1c8388e 100644 --- a/hooked/src/main.rs +++ b/hooked/src/main.rs @@ -1,8 +1,9 @@ //! git hook manager tool -#[cfg(windows)] -use anyhow::bail; -use anyhow::Result; +use anyhow::{ + bail, + Result, +}; use log::*; use shared::find_root; #[cfg(not(windows))] @@ -16,6 +17,7 @@ use std::{ env, fs, io::Write, + path::Path, }; const HOOKS: [&str; 18] = [ @@ -41,8 +43,21 @@ const HOOKS: [&str; 18] = [ #[derive(structopt::StructOpt)] enum Args { - /// Initialize the repo to use ticket - Init, + /// 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] @@ -52,60 +67,172 @@ fn main(args: Args) { .map_or_else(|| env::set_var("RUST_LOG", "info"), drop); pretty_env_logger::init(); if let Err(e) = match args { - Args::Init => init(), + Args::Init(lang) => init(lang), + Args::Link => link(), } { error!("{}", e); std::process::exit(1); } } -fn init() -> Result<()> { - #[cfg(windows)] - bail!("Windows is currently unsupported!"); - +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()); - fs::create_dir_all(&root)?; + let wrapper_dir = &root.join("wrapper"); + fs::create_dir_all(&wrapper_dir)?; for hook in &HOOKS { - let path = &root.join(hook); + 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."); - let mut perms = file.metadata()?.permissions(); - debug!("Setting dev-suite hook to be executable."); - perms.set_mode(0o755); - file.set_permissions(perms)?; - trace!("Permissions were set."); - file.write_all(b"#! /bin/bash")?; + #[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); } - let path = path.canonicalize()?; - if !git_hook.exists() { - debug!("Symlinking git hook {}.", hook); - #[cfg(not(windows))] - symlink(&path, &git_hook)?; - #[cfg(windows)] - symlink_file(&path, &git_hook)?; - trace!("Symlinked git hook {} to .dev-suite/hooked/{}.", 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(()) } diff --git a/hooked/tests/init.rs b/hooked/tests/init.rs index 65c4aab..9c68ff3 100644 --- a/hooked/tests/init.rs +++ b/hooked/tests/init.rs @@ -1,10 +1,10 @@ use assert_cmd::prelude::*; use git2::Repository; +#[cfg(not(windows))] +use std::os::unix::fs::PermissionsExt; use std::{ - env, error::Error, fs, - os::unix::fs::PermissionsExt, process::Command, }; use tempfile::tempdir; @@ -30,19 +30,36 @@ const HOOKS: [&str; 18] = [ "sendemail-validate", ]; -#[test] -fn init() -> Result<(), Box<dyn Error>> { +fn lang(lang: &str) -> Result<(), Box<dyn Error>> { let dir = tempdir()?; let _ = Repository::init(&dir)?; - let mut cmd = Command::cargo_bin("hooked")?; - env::set_current_dir(&dir)?; - let _ = cmd.arg("init").assert().success(); + let _ = Command::cargo_bin("hooked")? + .arg("init") + .arg(lang) + .current_dir(&dir) + .assert() + .success(); let git = &dir.path().join(".git").join("hooks"); let dev = &dir.path().join(".dev-suite").join("hooked"); for hook in &HOOKS { let git_hook = git.join(hook); - let dev_hook = dev.join(hook); + #[cfg(not(windows))] + let mut dev_hook = dev.join(hook); + #[cfg(windows)] + let mut dev_hook = dev.join("wrapper").join(hook); + + #[cfg(not(windows))] + let _ = match lang { + "bash" => dev_hook.set_extension("sh"), + "python" => dev_hook.set_extension("py"), + "ruby" => dev_hook.set_extension("rb"), + _ => unreachable!(), + }; + + #[cfg(windows)] + let _ = dev_hook.set_extension("sh"); + assert!(&git_hook.exists()); assert!(&dev_hook.exists()); assert!(fs::symlink_metadata(&git_hook)?.file_type().is_symlink()); @@ -54,8 +71,39 @@ fn init() -> Result<(), Box<dyn Error>> { // numbers essentially, allowing us to test the actual value we wanted to // test, and making this test work without special casing it if git ever // changes. + #[cfg(not(windows))] assert_eq!(dev_hook.metadata()?.permissions().mode() & 511, 0o755); - } + let shebang = fs::read_to_string(&dev_hook)? + .lines() + .nth(0) + .ok_or_else(|| "File is empty and has no shebang line")? + .to_owned(); + + #[cfg(not(windows))] + match lang { + "bash" => assert_eq!(shebang, "#!/usr/bin/env bash"), + "python" => assert_eq!(shebang, "#!/usr/bin/env python3"), + "ruby" => assert_eq!(shebang, "#!/usr/bin/env ruby"), + _ => unreachable!(), + } + #[cfg(windows)] + assert_eq!(shebang, "#!C:\\Program Files\\Git\\bin\\sh.exe") + } Ok(()) } + +#[test] +fn init_bash() -> Result<(), Box<dyn Error>> { + lang("bash") +} + +#[test] +fn init_python() -> Result<(), Box<dyn Error>> { + lang("python") +} + +#[test] +fn init_ruby() -> Result<(), Box<dyn Error>> { + lang("ruby") +} |