diff --git a/Cargo.lock b/Cargo.lock index 8880391..9c1317d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,7 +8,7 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom", + "getrandom 0.2.15", "once_cell", "version_check", ] @@ -85,9 +85,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "assert_cmd" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" +checksum = "2bd389a4b2970a01282ee455294913c0a43724daedcd1a24c3eb0ec1c1320b66" dependencies = [ "anstyle", "bstr", @@ -139,9 +139,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.20" +version = "4.5.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" +checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" dependencies = [ "clap_builder", "clap_derive", @@ -149,9 +149,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.20" +version = "4.5.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" dependencies = [ "anstream", "anstyle", @@ -161,9 +161,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.18" +version = "4.5.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" dependencies = [ "heck", "proc-macro2", @@ -173,13 +173,13 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.2" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "codeowners" -version = "0.2.14" +version = "0.2.15" dependencies = [ "assert_cmd", "clap", @@ -307,12 +307,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -372,14 +372,26 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "globset" @@ -453,9 +465,9 @@ dependencies = [ [[package]] name = "indoc" -version = "2.0.5" +version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" +checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" [[package]] name = "is_terminal_polyfill" @@ -486,15 +498,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.161" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "log" @@ -671,6 +683,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rayon" version = "1.10.0" @@ -746,15 +764,15 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.38" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -798,18 +816,18 @@ checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" [[package]] name = "serde" -version = "1.0.214" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.214" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", @@ -818,9 +836,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.132" +version = "1.0.143" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" dependencies = [ "itoa", "memchr", @@ -886,12 +904,12 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.13.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" dependencies = [ - "cfg-if", "fastrand", + "getrandom 0.3.3", "once_cell", "rustix", "windows-sys 0.59.0", @@ -924,9 +942,9 @@ dependencies = [ [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -935,9 +953,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", @@ -946,9 +964,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", "valuable", @@ -967,9 +985,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ "matchers", "nu-ansi-term", @@ -1044,6 +1062,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1084,15 +1111,6 @@ dependencies = [ "windows-targets 0.48.0", ] -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.59.0" @@ -1223,6 +1241,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + [[package]] name = "yansi" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index d031960..7c3c47c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "codeowners" -version = "0.2.14" +version = "0.2.15" edition = "2024" [profile.release] @@ -10,13 +10,13 @@ debug = true path = "src/lib.rs" [dependencies] -clap = { version = "4.5.20", features = ["derive"] } -clap_derive = "4.5.18" +clap = { version = "4.5.45", features = ["derive"] } +clap_derive = "4.5.45" crossbeam-channel = "0.5.15" error-stack = "0.5.0" enum_dispatch = "0.3.13" fast-glob = "1.0.0" -glob = "0.3.2" +glob = "0.3.3" ignore = "0.4.23" itertools = "0.14.0" lazy_static = "1.5.0" @@ -24,16 +24,16 @@ memoize = "0.5.1" path-clean = "1.0.1" rayon = "1.10.0" regex = "1.11.1" -serde = { version = "1.0.214", features = ["derive"] } -serde_json = "1.0.132" +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.143" serde_yaml = "0.9.34" -tempfile = "3.13.0" -tracing = "0.1.40" -tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } +tempfile = "3.21.0" +tracing = "0.1.41" +tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } [dev-dependencies] -assert_cmd = "2.0.16" +assert_cmd = "2.0.17" rusty-hook = "^0.11.2" predicates = "3.1.3" pretty_assertions = "1.4.1" # Shows a more readable diff when comparing objects -indoc = "2.0.5" +indoc = "2.0.6" diff --git a/src/ownership.rs b/src/ownership.rs index f6323ee..88516f8 100644 --- a/src/ownership.rs +++ b/src/ownership.rs @@ -9,11 +9,11 @@ use std::{ }; use tracing::{info, instrument}; +pub(crate) mod codeowners_file_parser; mod file_generator; mod file_owner_finder; pub mod for_file_fast; pub(crate) mod mapper; -pub(crate) mod parser; mod validator; use crate::{ @@ -24,9 +24,9 @@ use crate::{ pub use validator::Errors as ValidatorErrors; use self::{ + codeowners_file_parser::parse_for_team, file_generator::FileGenerator, mapper::{JavascriptPackageMapper, Mapper, RubyPackageMapper, TeamFileMapper, TeamGemMapper, TeamGlobMapper, TeamYmlMapper}, - parser::parse_for_team, validator::Validator, }; diff --git a/src/ownership/parser.rs b/src/ownership/codeowners_file_parser.rs similarity index 87% rename from src/ownership/parser.rs rename to src/ownership/codeowners_file_parser.rs index 9681420..75ab6c7 100644 --- a/src/ownership/parser.rs +++ b/src/ownership/codeowners_file_parser.rs @@ -4,6 +4,7 @@ use crate::{ }; use fast_glob::glob_match; use memoize::memoize; +use rayon::prelude::*; use regex::Regex; use std::{ collections::HashMap, @@ -22,29 +23,57 @@ pub struct Parser { } impl Parser { - pub fn team_from_file_path(&self, file_path: &Path) -> Result, Box> { - let file_path_str = file_path - .to_str() - .ok_or(IoError::new(std::io::ErrorKind::InvalidInput, "Invalid file path"))?; - let slash_prefixed = if file_path_str.starts_with("/") { - file_path_str.to_string() - } else { - format!("/{}", file_path_str) - }; - - let codeowners_lines_in_priorty = build_codeowners_lines_in_priority(self.codeowners_file_path.to_string_lossy().into_owned()); - for line in codeowners_lines_in_priorty { - let (glob, team_name) = line - .split_once(' ') - .ok_or(IoError::new(std::io::ErrorKind::InvalidInput, "Invalid line"))?; - if glob_match(glob, &slash_prefixed) { - let tbn = teams_by_github_team_name(self.absolute_team_files_globs()); - let team: Option = tbn.get(team_name.to_string().as_str()).cloned(); - return Ok(team); - } + pub fn teams_from_files_paths(&self, file_paths: &[PathBuf]) -> Result>, Box> { + let file_inputs: Vec<(String, String)> = file_paths + .iter() + .map(|path| { + let file_path_str = path + .to_str() + .ok_or(IoError::new(std::io::ErrorKind::InvalidInput, "Invalid file path"))?; + let original = file_path_str.to_string(); + let prefixed = if file_path_str.starts_with('/') { + original.clone() + } else { + format!("/{}", file_path_str) + }; + Ok((original, prefixed)) + }) + .collect::, IoError>>()?; + + if file_inputs.is_empty() { + return Ok(HashMap::new()); } - Ok(None) + let codeowners_entries: Vec<(String, String)> = + build_codeowners_lines_in_priority(self.codeowners_file_path.to_string_lossy().into_owned()) + .iter() + .map(|line| { + line.split_once(' ') + .map(|(glob, team_name)| (glob.to_string(), team_name.to_string())) + .ok_or_else(|| IoError::new(std::io::ErrorKind::InvalidInput, "Invalid line")) + }) + .collect::>() + .map_err(|e| Box::new(e) as Box)?; + + let teams_by_name = teams_by_github_team_name(self.absolute_team_files_globs()); + + let result: HashMap> = file_inputs + .par_iter() + .map(|(key, prefixed)| { + let team = codeowners_entries + .iter() + .find(|(glob, _)| glob_match(glob, prefixed)) + .and_then(|(_, team_name)| teams_by_name.get(team_name).cloned()); + (key.clone(), team) + }) + .collect(); + + Ok(result) + } + + pub fn team_from_file_path(&self, file_path: &Path) -> Result, Box> { + let teams = self.teams_from_files_paths(&[file_path.to_path_buf()])?; + Ok(teams.get(file_path.to_string_lossy().into_owned().as_str()).cloned().flatten()) } fn absolute_team_files_globs(&self) -> Vec { diff --git a/src/runner.rs b/src/runner.rs index fab6096..114ab04 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -1,5 +1,6 @@ use core::fmt; use std::{ + collections::HashMap, fs::File, path::{Path, PathBuf}, process::Command, @@ -280,17 +281,36 @@ fn for_file_codeowners_only(run_config: &RunConfig, file_path: &str) -> RunResul }, } } -pub fn team_for_file_from_codeowners(run_config: &RunConfig, file_path: &str) -> Result, Error> { + +// For an array of file paths, return a map of file path to its owning team +pub fn teams_for_files_from_codeowners(run_config: &RunConfig, file_paths: &[String]) -> Result>, Error> { + let relative_file_paths: Vec = file_paths + .iter() + .map(|path| Path::new(path).strip_prefix(&run_config.project_root).unwrap_or(Path::new(path))) + .map(|path| path.to_path_buf()) + .collect(); + + let parser = build_codeowners_parser(run_config)?; + Ok(parser + .teams_from_files_paths(&relative_file_paths) + .map_err(|e| Error::Io(e.to_string()))?) +} + +fn build_codeowners_parser(run_config: &RunConfig) -> Result { let config = config_from_path(&run_config.config_path)?; + Ok(crate::ownership::codeowners_file_parser::Parser { + codeowners_file_path: run_config.codeowners_file_path.clone(), + project_root: run_config.project_root.clone(), + team_file_globs: config.team_file_glob.clone(), + }) +} + +pub fn team_for_file_from_codeowners(run_config: &RunConfig, file_path: &str) -> Result, Error> { let relative_file_path = Path::new(file_path) .strip_prefix(&run_config.project_root) .unwrap_or(Path::new(file_path)); - let parser = crate::ownership::parser::Parser { - project_root: run_config.project_root.clone(), - codeowners_file_path: run_config.codeowners_file_path.clone(), - team_file_globs: config.team_file_glob.clone(), - }; + let parser = build_codeowners_parser(run_config)?; Ok(parser .team_from_file_path(Path::new(relative_file_path)) .map_err(|e| Error::Io(e.to_string()))?) @@ -342,9 +362,8 @@ fn for_file_optimized(run_config: &RunConfig, file_path: &str) -> RunResult { mod tests { use tempfile::tempdir; - use crate::{common_test, ownership::mapper::Source}; - use super::*; + use crate::{common_test, ownership::mapper::Source}; #[test] fn test_version() { @@ -397,4 +416,62 @@ mod tests { assert_eq!(team.github_team, "@b"); assert!(team.path.to_string_lossy().ends_with("config/teams/b.yml")); } + + #[test] + fn test_teams_for_files_from_codeowners() { + let project_root = Path::new("tests/fixtures/valid_project"); + let file_paths = [ + "javascript/packages/items/item.ts", + "config/teams/payroll.yml", + "ruby/app/models/bank_account.rb", + "made/up/file.rb", + "ruby/ignored_files/git_ignored.rb", + ]; + let run_config = RunConfig { + project_root: project_root.to_path_buf(), + codeowners_file_path: project_root.join(".github/CODEOWNERS").to_path_buf(), + config_path: project_root.join("config/code_ownership.yml").to_path_buf(), + no_cache: false, + }; + let teams = + teams_for_files_from_codeowners(&run_config, &file_paths.iter().map(|s| s.to_string()).collect::>()).unwrap(); + assert_eq!(teams.len(), 5); + assert_eq!( + teams + .get("javascript/packages/items/item.ts") + .unwrap() + .as_ref() + .map(|t| t.name.as_str()), + Some("Payroll") + ); + assert_eq!( + teams.get("config/teams/payroll.yml").unwrap().as_ref().map(|t| t.name.as_str()), + Some("Payroll") + ); + assert_eq!( + teams + .get("ruby/app/models/bank_account.rb") + .unwrap() + .as_ref() + .map(|t| t.name.as_str()), + Some("Payments") + ); + assert_eq!(teams.get("made/up/file.rb").unwrap().as_ref().map(|t| t.name.as_str()), None); + assert_eq!( + teams + .get("ruby/ignored_files/git_ignored.rb") + .unwrap() + .as_ref() + .map(|t| t.name.as_str()), + None + ); + assert_eq!( + teams + .get("ruby/ignored_files/git_ignored.rb") + .unwrap() + .as_ref() + .map(|t| t.name.as_str()), + None + ); + } }