Conversation
Use `:" instead of "@" as the version separator in additional_dependencies to match pre-commit convention (e.g., "csharpier:1.2.6").
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1783 +/- ##
==========================================
+ Coverage 91.66% 92.06% +0.39%
==========================================
Files 98 101 +3
Lines 19878 20893 +1015
==========================================
+ Hits 18222 19235 +1013
- Misses 1656 1658 +2 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
📦 Cargo Bloat ComparisonBinary size change: +0.40% (24.7 MiB → 24.8 MiB) Expand for cargo-bloat outputHead Branch ResultsBase Branch Results |
⚡️ Hyperfine BenchmarksSummary: 0 regressions, 0 improvements above the 10% threshold. Environment
CLI CommandsBenchmarking basic commands in the main repo:
|
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base --version |
2.4 ± 0.1 | 2.2 | 2.9 | 1.01 ± 0.08 |
prek-head --version |
2.3 ± 0.1 | 2.2 | 3.4 | 1.00 |
prek list
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base list |
8.8 ± 0.1 | 8.6 | 9.1 | 1.00 |
prek-head list |
8.8 ± 0.1 | 8.6 | 9.4 | 1.00 ± 0.02 |
prek validate-config .pre-commit-config.yaml
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base validate-config .pre-commit-config.yaml |
3.1 ± 0.2 | 3.0 | 4.4 | 1.00 |
prek-head validate-config .pre-commit-config.yaml |
3.1 ± 0.3 | 3.0 | 5.1 | 1.00 ± 0.12 |
prek sample-config
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base sample-config |
2.6 ± 0.1 | 2.5 | 3.0 | 1.00 |
prek-head sample-config |
2.6 ± 0.1 | 2.5 | 2.8 | 1.01 ± 0.03 |
Cold vs Warm Runs
Comparing first run (cold) vs subsequent runs (warm cache):
prek run --all-files (cold - no cache)
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base run --all-files |
149.0 ± 3.5 | 144.8 | 154.7 | 1.00 |
prek-head run --all-files |
149.3 ± 2.1 | 146.9 | 153.0 | 1.00 ± 0.03 |
prek run --all-files (warm - with cache)
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base run --all-files |
150.0 ± 2.4 | 146.7 | 156.4 | 1.00 ± 0.02 |
prek-head run --all-files |
149.8 ± 2.3 | 144.8 | 153.7 | 1.00 |
Full Hook Suite
Running the builtin hook suite on the benchmark workspace:
prek run --all-files (full builtin hook suite)
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base run --all-files |
150.2 ± 1.9 | 147.1 | 156.1 | 1.00 |
prek-head run --all-files |
155.4 ± 32.2 | 147.2 | 377.7 | 1.03 ± 0.22 |
Individual Hook Performance
Benchmarking each hook individually on the test repo:
prek run trailing-whitespace --all-files
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base run trailing-whitespace --all-files |
21.4 ± 0.4 | 20.8 | 22.3 | 1.00 |
prek-head run trailing-whitespace --all-files |
21.4 ± 0.4 | 20.7 | 22.3 | 1.00 ± 0.03 |
prek run end-of-file-fixer --all-files
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base run end-of-file-fixer --all-files |
28.4 ± 2.2 | 25.2 | 33.8 | 1.03 ± 0.10 |
prek-head run end-of-file-fixer --all-files |
27.6 ± 1.8 | 25.1 | 31.8 | 1.00 |
prek run check-json --all-files
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base run check-json --all-files |
12.2 ± 0.4 | 11.7 | 13.2 | 1.01 ± 0.04 |
prek-head run check-json --all-files |
12.1 ± 0.2 | 11.7 | 12.6 | 1.00 |
prek run check-yaml --all-files
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base run check-yaml --all-files |
11.9 ± 0.3 | 11.6 | 13.0 | 1.01 ± 0.03 |
prek-head run check-yaml --all-files |
11.8 ± 0.2 | 11.6 | 12.3 | 1.00 |
prek run check-toml --all-files
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base run check-toml --all-files |
12.1 ± 0.3 | 11.6 | 12.9 | 1.01 ± 0.03 |
prek-head run check-toml --all-files |
12.1 ± 0.3 | 11.6 | 12.6 | 1.00 |
prek run check-xml --all-files
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base run check-xml --all-files |
12.0 ± 0.4 | 11.4 | 12.8 | 1.00 |
prek-head run check-xml --all-files |
12.1 ± 0.3 | 11.5 | 12.7 | 1.01 ± 0.04 |
Installation Performance
Benchmarking hook installation (fast path hooks skip Python setup):
prek install-hooks (cold - no cache)
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base install-hooks |
4.7 ± 0.1 | 4.7 | 4.8 | 1.00 |
prek-head install-hooks |
4.8 ± 0.1 | 4.7 | 4.9 | 1.01 ± 0.02 |
prek install-hooks (warm - with cache)
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base install-hooks |
4.7 ± 0.1 | 4.7 | 4.8 | 1.00 |
prek-head install-hooks |
4.8 ± 0.1 | 4.7 | 4.9 | 1.02 ± 0.02 |
File Filtering/Scoping Performance
Testing different file selection modes:
prek run (staged files only)
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base run |
18.6 ± 0.1 | 18.4 | 18.9 | 1.01 ± 0.01 |
prek-head run |
18.5 ± 0.1 | 18.2 | 18.8 | 1.00 |
prek run --files '*.json' (specific file type)
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base run --files '*.json' |
7.5 ± 0.1 | 7.3 | 7.6 | 1.00 |
prek-head run --files '*.json' |
7.5 ± 0.1 | 7.3 | 7.7 | 1.01 ± 0.02 |
Workspace Discovery & Initialization
Benchmarking hook discovery and initialization overhead:
prek run --dry-run --all-files (measures init overhead)
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base run --dry-run --all-files |
12.3 ± 0.1 | 12.1 | 12.7 | 1.00 ± 0.01 |
prek-head run --dry-run --all-files |
12.3 ± 0.1 | 12.1 | 12.6 | 1.00 |
Meta Hooks Performance
Benchmarking meta hooks separately:
prek run check-hooks-apply --all-files
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base run check-hooks-apply --all-files |
12.8 ± 0.4 | 12.4 | 14.1 | 1.02 ± 0.03 |
prek-head run check-hooks-apply --all-files |
12.5 ± 0.1 | 12.3 | 12.7 | 1.00 |
prek run check-useless-excludes --all-files
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base run check-useless-excludes --all-files |
12.6 ± 0.2 | 12.3 | 13.2 | 1.00 ± 0.02 |
prek-head run check-useless-excludes --all-files |
12.5 ± 0.1 | 12.4 | 12.8 | 1.00 |
prek run identity --all-files
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base run identity --all-files |
11.1 ± 0.3 | 10.9 | 12.2 | 1.01 ± 0.03 |
prek-head run identity --all-files |
11.0 ± 0.1 | 10.9 | 11.2 | 1.00 |
0108fa0 to
9e90893
Compare
9e90893 to
3a39aac
Compare
2625a90 to
c44b3ea
Compare
There was a problem hiding this comment.
Pull request overview
Adds first-class .NET (language: dotnet) support to prek, including SDK selection via language_version, tool installation via additional_dependencies, documentation updates, and CI wiring to run the new language test suite.
Changes:
- Implement
dotnetlanguage: SDK discovery/download +dotnet tool install --tool-pathdependency support. - Add unit + integration tests for dotnet behavior and wire dotnet into language test matrix.
- Update docs and nextest profiles to reflect supported dotnet hooks.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/languages.md | Documents dotnet language support, language_version, and additional_dependencies. |
| crates/prek/tests/run.rs | Updates an invalid-config test now that dotnet supports deps (uses swift instead). |
| crates/prek/tests/languages/main.rs | Registers the new dotnet language test module. |
| crates/prek/tests/languages/dotnet.rs | Adds end-to-end integration tests for dotnet hooks, versions, deps, and error handling. |
| crates/prek/tests/common/mod.rs | Adds an insta output filter intended for dotnet version output. |
| crates/prek/src/store.rs | Adds ToolBucket::Dotnet for tool storage. |
| crates/prek/src/languages/version.rs | Adds LanguageRequest::Dotnet parsing/satisfaction plumbing. |
| crates/prek/src/languages/mod.rs | Wires dotnet into supported languages, tool buckets, version support, install/run/health dispatch. |
| crates/prek/src/languages/dotnet/version.rs | Implements dotnet language_version parsing and matching (major / major.minor / exact / netX.Y). |
| crates/prek/src/languages/dotnet/mod.rs | Adds the dotnet language module exports. |
| crates/prek/src/languages/dotnet/installer.rs | Implements system discovery + managed install via dotnet-install scripts. |
| crates/prek/src/languages/dotnet/dotnet.rs | Implements hook install/run/health-check and dependency installation for dotnet hooks. |
| crates/prek-consts/src/env_vars.rs | Adds DOTNET_ROOT env var constant. |
| .github/workflows/ci.yml | Installs .NET in CI for the dotnet language matrix entry. |
| .config/nextest.toml | Adds a lang-dotnet nextest profile to run dotnet language tests. |
| let env_dir = hook.env_path().expect("Dotnet must have env path"); | ||
| let tool_path = tools_path(env_dir); | ||
| let dotnet_path = hook | ||
| .install_info() | ||
| .expect("Dotnet must have install info") | ||
| .toolchain | ||
| .parent() | ||
| .expect("dotnet executable must have parent"); | ||
|
|
||
| // Prepend both dotnet and tools to PATH | ||
| let new_path = prepend_paths(&[&tool_path, dotnet_path]).context("Failed to join PATH")?; | ||
| let entry = hook.entry.resolve(Some(&new_path))?; | ||
|
|
||
| let run = async |batch: &[&Path]| { | ||
| let mut output = Cmd::new(&entry[0], "run dotnet hook") | ||
| .current_dir(hook.work_dir()) | ||
| .args(&entry[1..]) | ||
| .env(EnvVars::PATH, &new_path) | ||
| .env(EnvVars::DOTNET_ROOT, dotnet_path) | ||
| .envs(&hook.env) |
There was a problem hiding this comment.
This was my fear with dotnet, it's going to be a bit spiky because of stuff like this 🙃 at least CoPilot has a bit of a clue
| async fn install_tool(dotnet: &Path, tool_path: &Path, dependency: &str) -> Result<()> { | ||
| let (package, version) = dependency | ||
| .split_once(':') | ||
| .map_or((dependency, None), |(pkg, ver)| (pkg, Some(ver))); | ||
|
|
| cmd.check(true) | ||
| .output() | ||
| .await | ||
| .with_context(|| format!("Failed to install dotnet tool: {dependency}"))?; | ||
|
|
||
| Ok(()) |
| You can request a specific .NET SDK version: | ||
|
|
||
| - `language_version: "8"` – any .NET 8.x SDK | ||
| - `language_version: "8.0"` – any .NET 8.0.x SDK | ||
| - `language_version: "8.0.100"` – exactly .NET SDK 8.0.100 | ||
|
|
||
| Tracking: [#48](https://github.com/j178/prek/issues/48) | ||
| prek first looks for a matching system-installed `dotnet`, then falls back to downloading the SDK via the official install script if needed. | ||
|
|
||
| #### `additional_dependencies` | ||
|
|
||
| Tools are specified in `additional_dependencies` using the format `package:version` or `package@version`: | ||
|
|
| /// Test that `language_version` can specify a dotnet SDK version. | ||
| #[test] | ||
| fn language_version() { | ||
| let context = TestContext::new(); | ||
| context.init_project(); | ||
|
|
||
| context.write_pre_commit_config(indoc::indoc! {r" | ||
| repos: | ||
| - repo: local | ||
| hooks: | ||
| - id: local | ||
| name: local | ||
| language: dotnet | ||
| entry: dotnet --version | ||
| language_version: '8.0' | ||
| always_run: true | ||
| verbose: true | ||
| pass_filenames: false | ||
| "}); |
| version.major == *major && version.minor == *minor && version.patch == *patch | ||
| } | ||
| }, | ||
| _ => true, | ||
| } | ||
| } | ||
|
|
||
| fn to_dotnet_install_version(request: &LanguageRequest) -> Option<String> { | ||
| match request { | ||
| LanguageRequest::Any { .. } => None, | ||
| LanguageRequest::Dotnet(req) => req.to_install_version(), | ||
| _ => None, | ||
| } | ||
| } | ||
|
|
||
| /// Add channel arguments to the Unix install command. | ||
| fn add_channel_args_unix(cmd: &mut Cmd, version: Option<&str>) { | ||
| if let Some(ver) = version { | ||
| cmd.arg("--channel").arg(ver); | ||
| } else { | ||
| // Default to LTS | ||
| cmd.arg("--channel").arg("LTS"); | ||
| } | ||
| } | ||
|
|
||
| /// Add channel arguments to the Windows install command. | ||
| #[cfg(any(windows, test))] | ||
| fn add_channel_args_windows(cmd: &mut Cmd, version: Option<&str>) { | ||
| if let Some(ver) = version { | ||
| cmd.arg("-Channel").arg(ver); | ||
| } else { | ||
| // Default to LTS | ||
| cmd.arg("-Channel").arg("LTS"); | ||
| } | ||
| } |
| // .NET SDK version output (e.g., "8.0.100" or "9.0.100-preview.1.24101.2") | ||
| (r"(?m)^\d+\.\d+\.\d+(-[\w.]+)?\n", "[DOTNET_VERSION]\n"), |
| // Check if we have a managed installation that satisfies the request | ||
| if let Some(result) = self.find_installed(request).await? { | ||
| debug!(%result, "Using managed dotnet"); | ||
| return Ok(result); | ||
| } | ||
|
|
||
| // If system_only is requested and we didn't find a matching system version, fail | ||
| if matches!(request, LanguageRequest::Any { system_only: true }) { | ||
| anyhow::bail!("No system dotnet installation found"); | ||
| } | ||
|
|
||
| if !allows_download { | ||
| anyhow::bail!("No suitable dotnet version found and downloads are disabled"); | ||
| } | ||
|
|
||
| // Install dotnet SDK | ||
| let version_request = to_dotnet_install_version(request); | ||
| debug!("Installing dotnet SDK"); | ||
| self.download(version_request.as_deref()).await?; | ||
|
|
||
| self.verify_and_query_installation().await | ||
| } |
| pub(crate) async fn query_dotnet_version(dotnet: &Path) -> Result<Version> { | ||
| let stdout = Cmd::new(dotnet, "get dotnet version") | ||
| .arg("--version") | ||
| .check(true) | ||
| .output() | ||
| .await? | ||
| .stdout; | ||
|
|
||
| let version_str = String::from_utf8_lossy(&stdout).trim().to_string(); | ||
| parse_dotnet_version(&version_str).context("Failed to parse dotnet version") | ||
| } |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds first-class .NET (dotnet) language support to prek, including SDK version selection and dotnet tool installation for hooks.
Changes:
- Implement
dotnetlanguage: SDK discovery/download + hook execution environment setup. - Add dotnet-specific tests, nextest profile, and CI setup-dotnet wiring.
- Update documentation to describe supported dotnet behavior and configuration options.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/languages.md | Documents dotnet language support, language_version, and additional_dependencies. |
| crates/prek/tests/run.rs | Updates an “invalid config” test to use swift instead of dotnet. |
| crates/prek/tests/languages/main.rs | Registers new dotnet language test module. |
| crates/prek/tests/languages/dotnet.rs | Adds integration tests for dotnet language behavior. |
| crates/prek/tests/common/mod.rs | Adds an insta filter for dotnet version output. |
| crates/prek/src/store.rs | Introduces a Dotnet tool bucket for caching/install locations. |
| crates/prek/src/languages/version.rs | Adds parsing/dispatch for LanguageRequest::Dotnet. |
| crates/prek/src/languages/mod.rs | Wires the dotnet language into install/health/run and capability flags. |
| crates/prek/src/languages/dotnet/mod.rs | Adds dotnet language module exports. |
| crates/prek/src/languages/dotnet/version.rs | Implements dotnet SDK version request parsing and matching. |
| crates/prek/src/languages/dotnet/installer.rs | Implements system discovery + managed SDK download via dotnet-install scripts. |
| crates/prek/src/languages/dotnet/dotnet.rs | Implements hook install/run logic and dotnet tool installation. |
| crates/prek-consts/src/env_vars.rs | Adds DOTNET_ROOT env var constant. |
| .github/workflows/ci.yml | Installs dotnet in CI for the dotnet language test matrix entry. |
| .config/nextest.toml | Adds a nextest profile to run only dotnet language tests. |
|
|
||
| #### `additional_dependencies` | ||
|
|
||
| Tools are specified in `additional_dependencies` using the format `package:version` or `package@version`: |
| let version_request = to_dotnet_install_version(request); | ||
| debug!("Installing dotnet SDK"); | ||
| self.download(version_request.as_deref()).await?; |
| /// Add channel arguments to the Unix install command. | ||
| fn add_channel_args_unix(cmd: &mut Cmd, version: Option<&str>) { | ||
| if let Some(ver) = version { | ||
| cmd.arg("--channel").arg(ver); | ||
| } else { | ||
| // Default to LTS | ||
| cmd.arg("--channel").arg("LTS"); | ||
| } | ||
| } | ||
|
|
||
| /// Add channel arguments to the Windows install command. | ||
| #[cfg(any(windows, test))] | ||
| fn add_channel_args_windows(cmd: &mut Cmd, version: Option<&str>) { | ||
| if let Some(ver) = version { | ||
| cmd.arg("-Channel").arg(ver); |
| let response = reqwest::get(script_url) | ||
| .await | ||
| .context("Failed to download dotnet-install.ps1")?; | ||
| let script_content = response | ||
| .bytes() | ||
| .await | ||
| .context("Failed to read dotnet-install.ps1")?; |
| fn dotnet_path() -> std::path::PathBuf { | ||
| which::which("dotnet").expect("dotnet must be installed to run this test") | ||
| } |
| // Time seconds | ||
| (r"\b(\d+\.)?\d+(ms|s)\b", "[TIME]"), | ||
| // .NET SDK version output (e.g., "8.0.100" or "9.0.100-preview.1.24101.2") | ||
| (r"(?m)^\d+\.\d+\.\d+(-[\w.]+)?\n", "[DOTNET_VERSION]\n"), |
Hi, I thought I'd give a stab at dotnet support for prek. Not really an amazing language for how it manages tools & its toolchain, so perhaps some nastiness. Seems to work for
csharpierat least for me.There's some missing tests around the windows part of the code in the coverage but I didn't want to get into messing with CI for windows runners, maybe we can drop the powershell stuff if we make windows people run in a bash type environment?
Not very versed in rust but hoping that even if stuff's bad there's a bit of a start for someone better than I to have a stab at.