diff --git a/Cargo.lock b/Cargo.lock index d0786ae5..ef6946b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,9 +88,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.83" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3" +checksum = "27a4bd113ab6da4cd0f521068a6e2ee1065eab54107266a11835d02c8ec86a37" [[package]] name = "async-convert" @@ -134,7 +134,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.64", ] [[package]] @@ -272,7 +272,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.64", ] [[package]] @@ -609,7 +609,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.64", ] [[package]] @@ -906,9 +906,9 @@ dependencies = [ [[package]] name = "instant" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if", ] @@ -979,9 +979,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.154" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libgit2-sys" @@ -1031,19 +1031,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" - -[[package]] -name = "lock_api" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" -dependencies = [ - "autocfg", - "scopeguard", -] +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "log" @@ -1081,9 +1071,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" dependencies = [ "adler", ] @@ -1193,7 +1183,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.64", ] [[package]] @@ -1224,29 +1214,6 @@ dependencies = [ "hashbrown 0.12.3", ] -[[package]] -name = "parking_lot" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets 0.52.5", -] - [[package]] name = "pathdiff" version = "0.2.1" @@ -1290,7 +1257,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.64", ] [[package]] @@ -1382,15 +1349,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "redox_syscall" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" -dependencies = [ - "bitflags 2.5.0", -] - [[package]] name = "regex" version = "1.10.4" @@ -1602,12 +1560,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - [[package]] name = "sct" version = "0.7.1" @@ -1653,22 +1605,22 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.201" +version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c" +checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.201" +version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" +checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.64", ] [[package]] @@ -1716,15 +1668,6 @@ dependencies = [ "digest", ] -[[package]] -name = "signal-hook-registry" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" -dependencies = [ - "libc", -] - [[package]] name = "slab" version = "0.4.9" @@ -1734,12 +1677,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "smallvec" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" - [[package]] name = "socket2" version = "0.5.7" @@ -1781,9 +1718,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.63" +version = "2.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf5be731623ca1a1fb7d8be6f261a3be6d3e2337b8a1f97be944d020c8fcb704" +checksum = "7ad3dee41f36859875573074334c200d1add8e4a87bb37113ebd31d926b7b11f" dependencies = [ "proc-macro2", "quote", @@ -1850,22 +1787,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.60" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.60" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.64", ] [[package]] @@ -1894,9 +1831,7 @@ dependencies = [ "libc", "mio", "num_cpus", - "parking_lot", "pin-project-lite", - "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.48.0", @@ -1910,7 +1845,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.64", ] [[package]] @@ -1991,7 +1926,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.64", ] [[package]] @@ -2134,7 +2069,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.64", "wasm-bindgen-shared", ] @@ -2168,7 +2103,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.64", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/Cargo.toml b/Cargo.toml index 0838e351..c7c4a4c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ default-features = true version = "4.5.3" [dependencies] -tokio = { version = "1.36.0", features = ["full"] } +tokio = { version = "1.36.0", features = ["rt-multi-thread", "macros"] } reqwest = { version = "0.11.27", features = ["json"] } serde = { version = "1", features = ["derive"] } serde_derive = "1.0.197" diff --git a/src/filesystem.rs b/src/filesystem.rs new file mode 100644 index 00000000..e4296cd3 --- /dev/null +++ b/src/filesystem.rs @@ -0,0 +1,167 @@ +use std::path::{Path, PathBuf}; +use std::{env, fs}; +use std::os::unix::fs::symlink as symlink_unix; + +use anyhow::{bail, Context, Result}; +use git2::{Repository, RepositoryOpenFlags as Flags}; + +#[derive(Debug, Clone)] +pub struct Filesystem { + git_ai_hook_bin_path: PathBuf, + git_hooks_path: PathBuf +} + +#[derive(Debug, Clone)] +pub struct File { + path: PathBuf +} + +impl File { + pub fn new(path: PathBuf) -> Self { + Self { path } + } + + pub fn exists(&self) -> bool { + self.path.exists() + } + + pub fn delete(&self) -> Result<()> { + log::debug!("Removing file at {}", self); + fs::remove_file(&self.path).context(format!("Failed to remove file at {}", self)) + } + + pub fn symlink(&self, target: File) -> Result<()> { + log::debug!("Symlinking {} to {}", target, self); + symlink_unix(&target.path, &self.path).context(format!("Failed to symlink {} to {}", target, self)) + } + + pub fn relative_path(&self) -> Result { + Dir::new( + self + .path + .strip_prefix(env::current_dir().context("Failed to get current directory")?) + .context(format!("Failed to strip prefix from {}", self.path.display()))? + .to_path_buf() + ) + .into() + } + + pub fn parent(&self) -> Dir { + Dir::new(self.path.parent().unwrap_or(Path::new("")).to_path_buf()).into() + } +} + +impl From<&File> for Dir { + fn from(file: &File) -> Self { + file.parent() + } +} + +impl std::fmt::Display for File { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.relative_path().unwrap_or(self.into()).path.display()) + } +} + +impl Into> for File { + fn into(self) -> Result { + Ok(self) + } +} + +impl Into> for Dir { + fn into(self) -> Result { + Ok(self) + } +} + +#[derive(Debug, Clone)] +pub struct Dir { + path: PathBuf +} + +impl std::fmt::Display for Dir { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.path.display()) + } +} + +impl Into> for Filesystem { + fn into(self) -> Result { + Ok(self) + } +} + +impl Dir { + pub fn new(path: PathBuf) -> Self { + Self { path } + } + + pub fn exists(&self) -> bool { + self.path.exists() + } + + pub fn create_dir_all(&self) -> Result<()> { + log::debug!("Creating directory at {}", self); + fs::create_dir_all(&self.path).context(format!("Failed to create directory at {}", self)) + } + + pub fn relative_path(&self) -> Result { + Self::new( + self + .path + .strip_prefix(env::current_dir().context("Failed to get current directory")?) + .context(format!("Failed to strip prefix from {}", self.path.display()))? + .to_path_buf() + ) + .into() + } +} + +impl Filesystem { + pub fn new() -> Result { + let current_dir = env::current_dir().context("Failed to get current directory")?; + let git_ai_bin_path = env::current_exe().context("Failed to get current executable")?; + + let repo = Repository::open_ext(current_dir.clone(), Flags::empty(), Vec::<&Path>::new()) + .context(format!("Failed to open repository at {}", current_dir.clone().display()))?; + + let mut git_path = repo.path().to_path_buf(); + // if relative, make it absolute + if git_path.is_relative() { + // make git_path absolute using the current folder as the base + git_path = current_dir.join(git_path); + } + + let git_ai_hook_bin_path = git_ai_bin_path + .parent() + .context(format!("Failed to get parent directory of {}", git_ai_bin_path.display()))? + .join("git-ai-hook"); + + if !git_ai_hook_bin_path.exists() { + bail!("Hook binary not found at {}", git_ai_hook_bin_path.display()); + } + + Self { + git_ai_hook_bin_path, + git_hooks_path: git_path.join("hooks") + } + .into() + } + + pub fn git_ai_hook_bin_path(&self) -> Result { + File::new(self.git_ai_hook_bin_path.clone()).into() + } + + pub fn git_hooks_path(&self) -> Dir { + Dir::new(self.git_hooks_path.clone()).into() + } + + pub fn prepare_commit_msg_path(&self) -> Result { + if !self.git_hooks_path.exists() { + bail!("Hooks directory not found at {}", self.git_hooks_path.display()); + } + + File::new(self.git_hooks_path.join("prepare-commit-msg")).into() + } +} diff --git a/src/install.rs b/src/install.rs index c26d95f1..c0fdfa0e 100644 --- a/src/install.rs +++ b/src/install.rs @@ -1,83 +1,27 @@ -use std::path::{Path, PathBuf}; -use std::{env, fs}; - use colored::Colorize; -use ai::style::Styled; +use anyhow::{bail, Result}; +use ai::filesystem::Filesystem; use console::Emoji; -use git2::{Repository, RepositoryOpenFlags as Flags}; -use anyhow::{Context, Result}; -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum InstallError { - #[error("Failed to get current directory")] - CurrentDirectory(#[from] std::io::Error), - #[error(transparent)] - Anyhow(#[from] anyhow::Error), - #[error("Git error: {0}")] - Git(#[from] git2::Error), - #[error("Strip prefix error: {0}")] - StripPrefix(#[from] std::path::StripPrefixError), - #[error("Hook binary at {0} not found")] - HookBinNotFound(PathBuf), - #[error("Git hook already exists at {0}")] - GitHookExists(PathBuf), - - #[error("Git repository not found at {0}")] - GitRepoNotFound(PathBuf) -} const EMOJI: Emoji<'_, '_> = Emoji("🔗", ""); -fn can_override_hook() -> bool { - std::env::args() - .collect::>() - .iter() - .any(|arg| arg == "-f") -} - -// Git hook: prepare-commit-msg -// Crates an executable git hook (prepare-commit-msg) in the .git/hooks directory -pub fn run() -> Result<(), InstallError> { - let curr_bin = env::current_exe()?; - let exec_path = curr_bin - .parent() - .context("Failed to get parent directory")?; - let hook_bin = exec_path.join("git-ai-hook"); +pub fn run() -> Result<()> { + let filesystem = Filesystem::new()?; - // Check if the hook binary exists - if !hook_bin.exists() { - return Err(InstallError::HookBinNotFound(hook_bin)); + if !filesystem.git_hooks_path().exists() { + filesystem.git_hooks_path().create_dir_all()?; } - let current_dir = env::current_dir()?; - let repo = Repository::open_ext(current_dir, Flags::empty(), Vec::<&Path>::new())?; - let repo_path = repo - .path() - .parent() - .context("Failed to get parent directory")?; - let git_path = match repo_path.file_name() { - Some(name) if name == ".git" => repo_path.to_path_buf(), - Some(_) => repo_path.join(".git"), - None => return Err(InstallError::GitRepoNotFound(repo_path.to_path_buf())) - }; - - let hook_dir = git_path.join("hooks"); - if !hook_dir.exists() { - fs::create_dir_all(&hook_dir)?; - } + let hook_file = filesystem.prepare_commit_msg_path()?; + let hook_bin = filesystem.git_ai_hook_bin_path()?; - let hook_file = hook_dir.join("prepare-commit-msg"); - if hook_file.exists() && !can_override_hook() { - return Err(InstallError::GitHookExists(hook_file.relative_path())); + if hook_file.exists() { + bail!("Hook already exists at {}, please run 'git ai hook reinstall'", hook_file); } - std::fs::copy(&hook_bin, &hook_file)?; + hook_file.symlink(hook_bin)?; - println!( - "{EMOJI} Hook symlinked successfully to {}", - hook_file.relative_path().display().to_string().italic() - ); + println!("{EMOJI} Hook symlinked successfully to {}", hook_file.to_string().italic()); Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index 183eb039..85d0962b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,3 +2,4 @@ pub mod commit; pub mod config; pub mod hook; pub mod style; +pub mod filesystem; diff --git a/src/main.rs b/src/main.rs index ae8b9f62..62fc62b0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ mod uninstall; mod install; +mod reinstall; mod config; mod examples; @@ -17,6 +18,7 @@ fn cli() -> Command { .about("Installs the git-ai hook") .subcommand(Command::new("install").about("Installs the git-ai hook")) .subcommand(Command::new("uninstall").about("Uninstalls the git-ai hook")) + .subcommand(Command::new("reinstall").about("Reinstalls the git-ai hook")) ) .subcommand( Command::new("config") @@ -67,7 +69,7 @@ fn cli() -> Command { .subcommand(Command::new("examples").about("Runs examples of generated commit messages")) } -#[tokio::main] +#[tokio::main(flavor = "multi_thread")] async fn main() -> Result<()> { env_logger::init(); dotenv().ok(); @@ -80,9 +82,14 @@ async fn main() -> Result<()> { Some(("install", _)) => { install::run()?; } + Some(("uninstall", _)) => { uninstall::run()?; } + + Some(("reinstall", _)) => { + reinstall::run()?; + } _ => unreachable!() }, Some(("config", args)) => diff --git a/src/reinstall.rs b/src/reinstall.rs new file mode 100644 index 00000000..350df05e --- /dev/null +++ b/src/reinstall.rs @@ -0,0 +1,31 @@ +use console::Emoji; +use anyhow::Result; +use ai::filesystem::Filesystem; +use colored::*; + +const EMOJI: Emoji<'_, '_> = Emoji("🔗", ""); + +pub fn run() -> Result<()> { + let filesystem = Filesystem::new()?; + + if !filesystem.git_hooks_path().exists() { + filesystem.git_hooks_path().create_dir_all()?; + } + + let hook_file = filesystem.prepare_commit_msg_path()?; + let hook_bin = filesystem.git_ai_hook_bin_path()?; + + if hook_file.exists() { + log::debug!("Removing existing hook file: {}", hook_file); + hook_file.delete()?; + } + + hook_file.symlink(hook_bin)?; + + println!( + "{EMOJI} Hook symlinked successfully to {}", + hook_file.relative_path()?.to_string().italic() + ); + + Ok(()) +} diff --git a/tools/test.sh b/tools/test.sh index 5c3d4fb8..a3c233b9 100755 --- a/tools/test.sh +++ b/tools/test.sh @@ -45,6 +45,8 @@ echo "Testing git-ai hook uninstallation..." git-ai hook uninstall echo "Re-testing git-ai hook installation..." git-ai hook install +echo "Re-testing git-ai hook reinstallation..." +git-ai hook reinstall # Set various configuration values echo "Setting configuration values..."