From dc159bc088672a2164aae8e56b2948a667a3dedb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Feb 2026 09:50:15 +0000 Subject: [PATCH 1/4] Initial plan From 7e2c80985ce14f4a2b2df3baec1a8a44f1f826e4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Feb 2026 09:58:37 +0000 Subject: [PATCH 2/4] Fix rust cli git dependency install for multi-bin repos Co-authored-by: j178 <10510431+j178@users.noreply.github.com> --- crates/prek/src/languages/rust/rust.rs | 40 ++++++++++++++++++++------ 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/crates/prek/src/languages/rust/rust.rs b/crates/prek/src/languages/rust/rust.rs index 345e0c5d6..c62354aa1 100644 --- a/crates/prek/src/languages/rust/rust.rs +++ b/crates/prek/src/languages/rust/rust.rs @@ -32,7 +32,7 @@ fn format_cargo_dependency(dep: &str) -> String { } } -fn format_cargo_cli_dependency(dep: &str) -> Vec<&str> { +fn format_cargo_cli_dependency<'a>(dep: &'a str, git_bin: Option<&'a str>) -> Vec<&'a str> { let is_url = dep.starts_with("http://") || dep.starts_with("https://"); let (package, version) = if is_url && dep.matches(':').count() == 1 { (dep, "") // We have a url without version @@ -46,6 +46,9 @@ fn format_cargo_cli_dependency(dep: &str) -> Vec<&str> { if !version.is_empty() { args.extend(["--tag", version]); } + if let Some(bin) = git_bin { + args.extend(["--bin", bin]); + } } else { args.push(package); if !version.is_empty() { @@ -393,11 +396,16 @@ impl LanguageImpl for Rust { } // Install CLI dependencies + let hook_entry = hook.entry.split()?; + let hook_bin = hook_entry + .first() + .map(String::as_str) + .context("Rust hook entry must contain executable name")?; for cli_dep in cli_deps { let mut cmd = Cmd::new(&cargo, "install cli dep"); cmd.args(["install", "--bins", "--root"]) .arg(&info.env_path) - .args(format_cargo_cli_dependency(cli_dep)) + .args(format_cargo_cli_dependency(cli_dep, Some(hook_bin))) .arg("--locked"); cmd.env(EnvVars::PATH, &new_path) .env(EnvVars::CARGO_HOME, &cargo_home) @@ -787,22 +795,38 @@ edition = "2021" #[test] fn test_format_cargo_cli_dependency() { - assert_eq!(format_cargo_cli_dependency("typos-cli"), ["typos-cli"]); assert_eq!( - format_cargo_cli_dependency("typos-cli:1.0"), + format_cargo_cli_dependency("typos-cli", Some("typos-cli")), + ["typos-cli"] + ); + assert_eq!( + format_cargo_cli_dependency("typos-cli:1.0", Some("typos-cli")), ["typos-cli", "--version", "1.0"] ); assert_eq!( - format_cargo_cli_dependency("https://github.com/fish-shell/fish-shell"), - ["--git", "https://github.com/fish-shell/fish-shell"] + format_cargo_cli_dependency( + "https://github.com/fish-shell/fish-shell", + Some("fish_indent") + ), + [ + "--git", + "https://github.com/fish-shell/fish-shell", + "--bin", + "fish_indent" + ] ); assert_eq!( - format_cargo_cli_dependency("https://github.com/fish-shell/fish-shell:4.0"), + format_cargo_cli_dependency( + "https://github.com/fish-shell/fish-shell:4.0", + Some("fish_indent") + ), [ "--git", "https://github.com/fish-shell/fish-shell", "--tag", - "4.0" + "4.0", + "--bin", + "fish_indent" ] ); } From 021e5263b247ebe80884b855cdc936902c7894f7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:11:28 +0000 Subject: [PATCH 3/4] Use package name for rust git cli dependencies Co-authored-by: j178 <10510431+j178@users.noreply.github.com> --- crates/prek/src/languages/rust/rust.rs | 75 +++++++++++++++++++------- 1 file changed, 57 insertions(+), 18 deletions(-) diff --git a/crates/prek/src/languages/rust/rust.rs b/crates/prek/src/languages/rust/rust.rs index c62354aa1..e6caa19c0 100644 --- a/crates/prek/src/languages/rust/rust.rs +++ b/crates/prek/src/languages/rust/rust.rs @@ -12,6 +12,7 @@ use prek_consts::prepend_paths; use tracing::debug; use crate::cli::reporter::{HookInstallReporter, HookRunReporter}; +use crate::git::clone_repo; use crate::hook::{Hook, InstallInfo, InstalledHook}; use crate::languages::LanguageImpl; use crate::languages::rust::RustRequest; @@ -32,7 +33,7 @@ fn format_cargo_dependency(dep: &str) -> String { } } -fn format_cargo_cli_dependency<'a>(dep: &'a str, git_bin: Option<&'a str>) -> Vec<&'a str> { +fn format_cargo_cli_dependency(dep: &str, git_package: Option<&str>) -> Vec { let is_url = dep.starts_with("http://") || dep.starts_with("https://"); let (package, version) = if is_url && dep.matches(':').count() == 1 { (dep, "") // We have a url without version @@ -42,22 +43,61 @@ fn format_cargo_cli_dependency<'a>(dep: &'a str, git_bin: Option<&'a str>) -> Ve let mut args = Vec::new(); if is_url { - args.extend(["--git", package]); + args.push("--git".to_string()); + args.push(package.to_string()); if !version.is_empty() { - args.extend(["--tag", version]); + args.push("--tag".to_string()); + args.push(version.to_string()); } - if let Some(bin) = git_bin { - args.extend(["--bin", bin]); + if let Some(package) = git_package { + args.push(package.to_string()); } } else { - args.push(package); + args.push(package.to_string()); if !version.is_empty() { - args.extend(["--version", version]); + args.push("--version".to_string()); + args.push(version.to_string()); } } args } +async fn find_git_package_name( + dep: &str, + binary_name: &str, + cargo: &Path, + cargo_home: &Path, + new_path: &OsStr, +) -> anyhow::Result> { + let is_url = dep.starts_with("http://") || dep.starts_with("https://"); + if !is_url { + return Ok(None); + } + + let (repo, rev) = if dep.matches(':').count() == 1 { + (dep, "HEAD") + } else { + dep.rsplit_once(':').unwrap_or((dep, "HEAD")) + }; + + let temp = tempfile::tempdir()?; + clone_repo(repo, rev, temp.path()) + .await + .with_context(|| format!("Failed to clone `{repo}` at `{rev}`"))?; + + let (_, package_name, _) = find_package_dir( + temp.path(), + binary_name, + Some(cargo), + Some(cargo_home), + Some(new_path), + ) + .await? + .with_context(|| format!("Binary `{binary_name}` not found in `{repo}`"))?; + + Ok(Some(package_name)) +} + /// Find the package directory that produces the given binary. /// Returns (`package_dir`, `package_name`, `is_workspace`). async fn find_package_dir( @@ -402,10 +442,15 @@ impl LanguageImpl for Rust { .map(String::as_str) .context("Rust hook entry must contain executable name")?; for cli_dep in cli_deps { + let package_name = + find_git_package_name(cli_dep, hook_bin, &cargo, &cargo_home, &new_path).await?; let mut cmd = Cmd::new(&cargo, "install cli dep"); cmd.args(["install", "--bins", "--root"]) .arg(&info.env_path) - .args(format_cargo_cli_dependency(cli_dep, Some(hook_bin))) + .args(format_cargo_cli_dependency( + cli_dep, + package_name.as_deref(), + )) .arg("--locked"); cmd.env(EnvVars::PATH, &new_path) .env(EnvVars::CARGO_HOME, &cargo_home) @@ -806,27 +851,21 @@ edition = "2021" assert_eq!( format_cargo_cli_dependency( "https://github.com/fish-shell/fish-shell", - Some("fish_indent") + Some("fish") ), - [ - "--git", - "https://github.com/fish-shell/fish-shell", - "--bin", - "fish_indent" - ] + ["--git", "https://github.com/fish-shell/fish-shell", "fish"] ); assert_eq!( format_cargo_cli_dependency( "https://github.com/fish-shell/fish-shell:4.0", - Some("fish_indent") + Some("fish") ), [ "--git", "https://github.com/fish-shell/fish-shell", "--tag", "4.0", - "--bin", - "fish_indent" + "fish" ] ); } From 91dcc4b8f2afd8fcfe020ca06c8a409c8bd785df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:17:28 +0000 Subject: [PATCH 4/4] Resolve rust git cli deps using package lookup Co-authored-by: j178 <10510431+j178@users.noreply.github.com> --- crates/prek/src/languages/rust/rust.rs | 67 +++++++++++++++++++------- 1 file changed, 49 insertions(+), 18 deletions(-) diff --git a/crates/prek/src/languages/rust/rust.rs b/crates/prek/src/languages/rust/rust.rs index e6caa19c0..107d8a497 100644 --- a/crates/prek/src/languages/rust/rust.rs +++ b/crates/prek/src/languages/rust/rust.rs @@ -33,19 +33,24 @@ fn format_cargo_dependency(dep: &str) -> String { } } -fn format_cargo_cli_dependency(dep: &str, git_package: Option<&str>) -> Vec { +fn parse_cargo_cli_dependency(dep: &str) -> (&str, Option<&str>, bool) { let is_url = dep.starts_with("http://") || dep.starts_with("https://"); let (package, version) = if is_url && dep.matches(':').count() == 1 { - (dep, "") // We have a url without version + (dep, "") } else { dep.rsplit_once(':').unwrap_or((dep, "")) }; + (package, (!version.is_empty()).then_some(version), is_url) +} + +fn format_cargo_cli_dependency(dep: &str, git_package: Option<&str>) -> Vec { + let (package, version, is_url) = parse_cargo_cli_dependency(dep); let mut args = Vec::new(); if is_url { args.push("--git".to_string()); args.push(package.to_string()); - if !version.is_empty() { + if let Some(version) = version { args.push("--tag".to_string()); args.push(version.to_string()); } @@ -54,7 +59,7 @@ fn format_cargo_cli_dependency(dep: &str, git_package: Option<&str>) -> Vec anyhow::Result> { - let is_url = dep.starts_with("http://") || dep.starts_with("https://"); + let (repo, rev, is_url) = parse_cargo_cli_dependency(dep); if !is_url { return Ok(None); } - - let (repo, rev) = if dep.matches(':').count() == 1 { - (dep, "HEAD") - } else { - dep.rsplit_once(':').unwrap_or((dep, "HEAD")) - }; + let rev = rev.unwrap_or("HEAD"); let temp = tempfile::tempdir()?; clone_repo(repo, rev, temp.path()) @@ -93,7 +93,7 @@ async fn find_git_package_name( Some(new_path), ) .await? - .with_context(|| format!("Binary `{binary_name}` not found in `{repo}`"))?; + .with_context(|| format!("Failed to locate package for binary `{binary_name}` in `{repo}`"))?; Ok(Some(package_name)) } @@ -838,21 +838,38 @@ edition = "2021" assert_eq!(format_cargo_dependency("tokio:1.0.0"), "tokio@1.0.0"); } + #[test] + fn test_parse_cargo_cli_dependency() { + assert_eq!( + parse_cargo_cli_dependency("https://github.com/fish-shell/fish-shell"), + ("https://github.com/fish-shell/fish-shell", None, true) + ); + assert_eq!( + parse_cargo_cli_dependency("https://github.com/fish-shell/fish-shell:4.0"), + ( + "https://github.com/fish-shell/fish-shell", + Some("4.0"), + true + ) + ); + assert_eq!( + parse_cargo_cli_dependency("typos-cli:1.0"), + ("typos-cli", Some("1.0"), false) + ); + } + #[test] fn test_format_cargo_cli_dependency() { assert_eq!( - format_cargo_cli_dependency("typos-cli", Some("typos-cli")), + format_cargo_cli_dependency("typos-cli", None), ["typos-cli"] ); assert_eq!( - format_cargo_cli_dependency("typos-cli:1.0", Some("typos-cli")), + format_cargo_cli_dependency("typos-cli:1.0", None), ["typos-cli", "--version", "1.0"] ); assert_eq!( - format_cargo_cli_dependency( - "https://github.com/fish-shell/fish-shell", - Some("fish") - ), + format_cargo_cli_dependency("https://github.com/fish-shell/fish-shell", Some("fish")), ["--git", "https://github.com/fish-shell/fish-shell", "fish"] ); assert_eq!( @@ -869,4 +886,18 @@ edition = "2021" ] ); } + + #[tokio::test] + async fn test_find_git_package_name_non_url() { + let result = find_git_package_name( + "typos-cli", + "typos-cli", + Path::new("cargo"), + Path::new(""), + OsStr::new(""), + ) + .await + .unwrap(); + assert!(result.is_none()); + } }