diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 17aafe5..6ec0feb 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -8,6 +8,10 @@ updates:
cargo:
patterns:
- "*"
+ # serde-saphyr patches only its latest release, and its releases can change how YAML parses,
+ # so its bumps get their own pull request instead of waiting on unrelated breakage.
+ exclude-patterns:
+ - "serde-saphyr"
cooldown:
default-days: 7
- package-ecosystem: "github-actions"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b31e0a4..b6bf4ee 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -32,6 +32,11 @@ jobs:
- name: Run cargo check
run: cargo check
+
+ - name: Collect the release archives' license notices
+ # The release jobs run only on main, so this catches a dependency change that breaks the
+ # script on its pull request rather than at release time.
+ run: dev/license-notices "$RUNNER_TEMP/licenses"
test:
name: Test Suite
runs-on: ubuntu-latest
@@ -129,10 +134,14 @@ jobs:
# This combines the intel and m1 binaries into a single binary
lipo -create -output target/codeowners target/aarch64-apple-darwin/release/codeowners target/x86_64-apple-darwin/release/codeowners
- # Creates artifact for homebrew. -C means run from `target` directory
- tar -czf target/codeowners-mac.tar.gz -C target codeowners
+ # Notices that some dependencies' licenses require redistributions of the binary to carry
+ dev/license-notices target/licenses
+
+ # Creates artifact for homebrew. -C means run from `target` directory, which keeps the
+ # binary at the archive root, where dotslash-config.json looks for it
+ tar -czf target/codeowners-mac.tar.gz -C target codeowners licenses
- # This tarball is a binary that is executable
+ # This tarball holds the executable binary and the license notices
gh release upload $NEW_VERSION target/codeowners-mac.tar.gz
env:
@@ -158,8 +167,12 @@ jobs:
cross build --release --target aarch64-unknown-linux-gnu
- name: Upload linux binaries
run: |
- tar -czf target/x86_64-unknown-linux-gnu.tar.gz -C target/x86_64-unknown-linux-gnu/release codeowners
- tar -czf target/aarch64-unknown-linux-gnu.tar.gz -C target/aarch64-unknown-linux-gnu/release codeowners
+ # Notices that some dependencies' licenses require redistributions of the binary to carry.
+ # Each -C keeps the binary at its archive's root, where dotslash-config.json looks for it.
+ dev/license-notices target/x86_64-unknown-linux-gnu/release/licenses
+ dev/license-notices target/aarch64-unknown-linux-gnu/release/licenses
+ tar -czf target/x86_64-unknown-linux-gnu.tar.gz -C target/x86_64-unknown-linux-gnu/release codeowners licenses
+ tar -czf target/aarch64-unknown-linux-gnu.tar.gz -C target/aarch64-unknown-linux-gnu/release codeowners licenses
gh release upload $NEW_VERSION target/x86_64-unknown-linux-gnu.tar.gz
gh release upload $NEW_VERSION target/aarch64-unknown-linux-gnu.tar.gz
env:
diff --git a/Cargo.lock b/Cargo.lock
index eac3781..3fe878a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -11,6 +11,17 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "annotate-snippets"
+version = "0.12.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c86cd1c51b95d71dde52bca69ed225008f6ff4c8cc825b08042aa1ef823e1980"
+dependencies = [
+ "anstyle",
+ "memchr",
+ "unicode-width 0.2.2",
+]
+
[[package]]
name = "anstream"
version = "0.6.18"
@@ -66,6 +77,12 @@ version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
+[[package]]
+name = "arraydeque"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
+
[[package]]
name = "arrayvec"
version = "0.7.6"
@@ -189,8 +206,8 @@ dependencies = [
"regex",
"rusty-hook",
"serde",
+ "serde-saphyr",
"serde_json",
- "serde_yaml",
"similar",
"tempfile",
"tracing",
@@ -203,6 +220,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
+[[package]]
+name = "core_detect"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48"
+
[[package]]
name = "crossbeam-channel"
version = "0.5.17"
@@ -256,6 +279,29 @@ version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
+[[package]]
+name = "encoding_rs"
+version = "0.8.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679"
+dependencies = [
+ "cfg-if",
+ "core_detect",
+ "multiversion_no_op",
+ "rustversion",
+ "scopeguard",
+ "simdutf8",
+]
+
+[[package]]
+name = "encoding_rs_io"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fba3fe847045ecff794b9c138293a80db914678c453ad63fbf0c6a9eb6e00b22"
+dependencies = [
+ "encoding_rs",
+]
+
[[package]]
name = "enum_dispatch"
version = "0.3.13"
@@ -275,15 +321,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2d328fc287c61314c4a61af7cfdcbd7e678e39778488c7cb13ec133ce0f4059"
dependencies = [
"fsio",
- "indexmap 1.9.3",
+ "indexmap",
]
-[[package]]
-name = "equivalent"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
-
[[package]]
name = "errno"
version = "0.3.13"
@@ -340,7 +380,7 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
dependencies = [
- "unicode-width",
+ "unicode-width 0.1.10",
]
[[package]]
@@ -375,16 +415,20 @@ dependencies = [
]
[[package]]
-name = "hashbrown"
-version = "0.12.3"
+name = "granit-parser"
+version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+checksum = "da55feca5d1625466bae068f9fd4b87892caeae70c3fe671ddb0f50d41aebf37"
+dependencies = [
+ "arraydeque",
+ "smallvec",
+]
[[package]]
name = "hashbrown"
-version = "0.15.1"
+version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "heck"
@@ -415,17 +459,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
- "hashbrown 0.12.3",
-]
-
-[[package]]
-name = "indexmap"
-version = "2.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
-dependencies = [
- "equivalent",
- "hashbrown 0.15.1",
+ "hashbrown",
]
[[package]]
@@ -519,6 +553,12 @@ dependencies = [
"syn 1.0.109",
]
+[[package]]
+name = "multiversion_no_op"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d"
+
[[package]]
name = "nias"
version = "0.5.0"
@@ -720,12 +760,6 @@ dependencies = [
"toml",
]
-[[package]]
-name = "ryu"
-version = "1.0.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
-
[[package]]
name = "same-file"
version = "1.0.6"
@@ -735,6 +769,12 @@ dependencies = [
"winapi-util",
]
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
[[package]]
name = "semver"
version = "1.0.17"
@@ -751,6 +791,20 @@ dependencies = [
"serde_derive",
]
+[[package]]
+name = "serde-saphyr"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a253edfdea34032da21af84e16fcc94f4c7a6ba237f9f3bc2dbf43cea8535c44"
+dependencies = [
+ "annotate-snippets",
+ "encoding_rs_io",
+ "granit-parser",
+ "num-traits",
+ "serde_core",
+ "smallvec",
+]
+
[[package]]
name = "serde_core"
version = "1.0.229"
@@ -784,19 +838,6 @@ dependencies = [
"zmij",
]
-[[package]]
-name = "serde_yaml"
-version = "0.9.34+deprecated"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
-dependencies = [
- "indexmap 2.6.0",
- "itoa",
- "ryu",
- "serde",
- "unsafe-libyaml",
-]
-
[[package]]
name = "sharded-slab"
version = "0.1.4"
@@ -806,6 +847,12 @@ dependencies = [
"lazy_static",
]
+[[package]]
+name = "simdutf8"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
+
[[package]]
name = "similar"
version = "3.2.0"
@@ -972,10 +1019,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
[[package]]
-name = "unsafe-libyaml"
-version = "0.2.11"
+name = "unicode-width"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
+checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
name = "utf8parse"
diff --git a/Cargo.toml b/Cargo.toml
index 1f4937b..6bcf46c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -38,7 +38,9 @@ rayon = "1.10.0"
regex = "1.11.1"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.143"
-serde_yaml = "0.9.34"
+# Deserialize only: `default-features = false` leaves out the serializer and YAML comment
+# capture, which we do not use.
+serde-saphyr = { version = "2.0.0", default-features = false, features = ["deserialize"] }
similar = "3.2.0"
tempfile = "3.21.0"
tracing = "0.1.41"
diff --git a/README.md b/README.md
index ebbafcf..bec715a 100644
--- a/README.md
+++ b/README.md
@@ -47,6 +47,8 @@ You can run `codeowners` without installing a platform-specific binary by using
2. Download the latest DotSlash text file from a release, for example [https://github.com/rubyatscale/codeowners-rs/releases](https://github.com/rubyatscale/codeowners-rs/releases).
3. Execute the downloaded file with DotSlash; it will fetch and run the correct binary.
+Release archives after v0.5.0 also hold a `licenses` directory next to the `codeowners` binary. It has the license notices that the dependencies listed in `dev/license-notices` require redistributions of the binary to include.
+
### Option B: From source with Cargo
Requires Rust toolchain.
@@ -214,6 +216,7 @@ codeowners gv --no-cache
- `cache_directory` (default: `'tmp/cache/codeowners'`)
- `ignore_dirs` (default includes: `.git`, `node_modules`, `tmp`, etc.)
- `executable_name` (default: `'codeowners'`): Customize the command name shown in validation error messages. Useful when using `codeowners-rs` via wrappers like the [code_ownership](https://github.com/rubyatscale/code_ownership) Ruby gem.
+- `yaml_max_nodes` (default: `20000`): The most YAML nodes (mappings, sequences and scalars) allowed in one team file or `package.yml`. A file over it, or over the limits on total scalar bytes and anchor copies that grow with it, fails with a `budget breached` error and a hint to raise this key in the config file in use. Raise it if a large team file hits the limit; real files have at most a few thousand nodes. It does not apply to the config file itself. Raising it also raises how much memory a single file can take: a file crafted to waste memory can make `codeowners` use about 280 MB more at the default and about 700 MB more at 50,000, measured on macOS, while ordinary files add almost nothing. Separately, any of these files, or the config, fails if it nests more than 16 levels deep, counting the top level. That limit can't be raised.
Example configuration with custom executable name:
diff --git a/dev/license-notices b/dev/license-notices
new file mode 100755
index 0000000..94987f8
--- /dev/null
+++ b/dev/license-notices
@@ -0,0 +1,66 @@
+#!/usr/bin/env bash
+# Copies, verbatim, the license files of the crates listed below into
/-/ for
+# the release archives, since their licenses require redistributions of the binary to carry those
+# notices. Versions come from Cargo.lock.
+#
+# Usage: dev/license-notices
+#
+# must not exist yet, so an archive never carries notices left over from other versions. Fails
+# if a listed crate is no longer a dependency or ships no license files, so whoever drops or
+# replaces one updates the list.
+#
+# Keep this bash 3.2 compatible: the mac release job runs it with macOS's /bin/bash, while the
+# pull request check runs it on ubuntu.
+
+set -euo pipefail
+shopt -s nullglob nocaseglob
+
+crates=(
+ # serde-saphyr's parser. Its LICENSE requires redistributions to include its notice and the ones
+ # it inherited, which it keeps in .licenses/.
+ granit-parser
+ # serde-saphyr decodes reader input with it, through encoding_rs_io. Its WHATWG-derived data is
+ # under BSD-3-Clause, which requires binary redistributions to reproduce that notice.
+ encoding_rs
+)
+
+if [[ $# -ne 1 ]]; then
+ echo "usage: $0 " >&2
+ exit 2
+fi
+out=$1
+if [[ -e $out ]]; then
+ echo "$0: $out already exists" >&2
+ exit 1
+fi
+
+manifest="$(dirname "$0")/../Cargo.toml"
+metadata=$(cargo metadata --format-version 1 --locked --manifest-path "$manifest")
+mkdir -p "$out"
+
+for crate in "${crates[@]}"; do
+ # One "\t" line per version of the crate in Cargo.lock.
+ packages=$(jq -r --arg name "$crate" \
+ '.packages[] | select(.name == $name) | "\(.version)\t\(.manifest_path)"' <<<"$metadata")
+ if [[ -z $packages ]]; then
+ echo "$0: $crate is no longer a dependency. Update the crate list in $0." >&2
+ exit 1
+ fi
+
+ while IFS=$'\t' read -r version manifest_path; do
+ src=$(dirname "$manifest_path")
+ # The license files at the crate root, plus the .licenses directory that granit-parser's LICENSE
+ # links to. Copying keeps their relative paths, so those links still resolve.
+ notices=("$src"/{LICENSE,LICENCE,COPYRIGHT,NOTICE,COPYING}*)
+ if [[ -d $src/.licenses ]]; then
+ notices+=("$src/.licenses")
+ fi
+ if [[ ${#notices[@]} -eq 0 ]]; then
+ echo "$0: $src has no license files. Check $crate's license and update $0." >&2
+ exit 1
+ fi
+
+ mkdir "$out/$crate-$version"
+ cp -RL "${notices[@]}" "$out/$crate-$version"
+ done <<<"$packages"
+done
diff --git a/src/common_test.rs b/src/common_test.rs
index 57a8bd8..fdb0ffa 100644
--- a/src/common_test.rs
+++ b/src/common_test.rs
@@ -1,10 +1,6 @@
#[cfg(test)]
pub mod tests {
- use std::{
- error::Error,
- fs::{self, File},
- path::PathBuf,
- };
+ use std::{error::Error, fs, path::PathBuf};
use indoc::indoc;
@@ -111,8 +107,7 @@ pub mod tests {
fs::write(test_config.temp_dir_path.join(&project_file.relative_path), &project_file.content)?;
}
- let config_file = File::open(test_config.temp_dir_path.join(test_config.relative_code_ownership_config_yml_path))?;
- let config: Config = serde_yaml::from_reader(config_file)?;
+ let config = Config::load_from_path(&test_config.temp_dir_path.join(test_config.relative_code_ownership_config_yml_path))?;
let codeowners_file_path = &test_config.temp_dir_path.join(".github/CODEOWNERS");
let cache: Cache = NoopCache::default().into();
diff --git a/src/config.rs b/src/config.rs
index 620185f..4894dac 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,36 +1,56 @@
use serde::Deserialize;
-use std::{fs::File, path::Path};
+use std::path::{Path, PathBuf};
+
+/// `Config.path` for a config not read with `load_from_path`: the usual location, relative to the project root.
+pub(crate) const DEFAULT_PATH: &str = "config/code_ownership.yml";
#[derive(Deserialize, Debug, Clone)]
pub struct Config {
+ #[serde(deserialize_with = "crate::yaml::strings_or_nulls")]
pub owned_globs: Vec,
- #[serde(default = "ruby_package_paths")]
+ #[serde(default = "ruby_package_paths", deserialize_with = "crate::yaml::strings_or_nulls")]
pub ruby_package_paths: Vec,
- #[serde(alias = "js_package_paths", default = "javascript_package_paths")]
+ #[serde(
+ alias = "js_package_paths",
+ default = "javascript_package_paths",
+ deserialize_with = "crate::yaml::strings_or_nulls"
+ )]
pub javascript_package_paths: Vec,
- #[serde(default = "team_file_glob")]
+ #[serde(default = "team_file_glob", deserialize_with = "crate::yaml::strings_or_nulls")]
pub team_file_glob: Vec,
- #[serde(default = "unowned_globs")]
+ #[serde(default = "unowned_globs", deserialize_with = "crate::yaml::strings_or_nulls")]
pub unowned_globs: Vec,
- #[serde(alias = "unbuilt_gems_path", default = "vendored_gems_path")]
+ #[serde(
+ alias = "unbuilt_gems_path",
+ default = "vendored_gems_path",
+ deserialize_with = "crate::yaml::string_or_null"
+ )]
pub vendored_gems_path: String,
- #[serde(default = "default_cache_directory")]
+ #[serde(default = "default_cache_directory", deserialize_with = "crate::yaml::string_or_null")]
pub cache_directory: String,
- #[serde(default = "default_ignore_dirs")]
+ #[serde(default = "default_ignore_dirs", deserialize_with = "crate::yaml::strings_or_nulls")]
pub ignore_dirs: Vec,
- #[serde(default = "default_executable_name")]
+ #[serde(default = "default_executable_name", deserialize_with = "crate::yaml::string_or_null")]
pub executable_name: String,
- #[serde(default = "default_codeowners_path")]
+ #[serde(default = "default_codeowners_path", deserialize_with = "crate::yaml::string_or_null")]
pub codeowners_path: String,
+
+ #[serde(default = "default_yaml_max_nodes")]
+ pub yaml_max_nodes: usize,
+
+ /// The file [`Config::load_from_path`] read this config from, or `config/code_ownership.yml` for a config
+ /// deserialized another way. The hint on a file over `yaml_max_nodes` names it.
+ #[serde(skip_deserializing, default = "default_path")]
+ pub path: PathBuf,
}
#[allow(dead_code)]
@@ -91,19 +111,38 @@ fn default_codeowners_path() -> String {
".github".to_string()
}
+fn default_yaml_max_nodes() -> usize {
+ crate::yaml::DEFAULT_MAX_NODES
+}
+
+fn default_path() -> PathBuf {
+ PathBuf::from(DEFAULT_PATH)
+}
+
impl Config {
pub fn load_from_path(path: &Path) -> std::result::Result {
- let file = File::open(path).map_err(|e| format!("Can't open config file: {} ({})", path.to_string_lossy(), e))?;
- serde_yaml::from_reader(file).map_err(|e| format!("Can't parse config file: {} ({})", path.to_string_lossy(), e))
+ let bytes = std::fs::read(path).map_err(|e| format!("Can't open config file: {} ({})", path.to_string_lossy(), e))?;
+ // The config can't raise its own limit, and at a few hundred nodes it never needs to.
+ let config: Self = crate::yaml::from_slice(&bytes, crate::yaml::DEFAULT_MAX_NODES)
+ .map_err(|e| format!("Can't parse config file: {} ({})", path.to_string_lossy(), e))?;
+ Ok(Self {
+ path: path.to_path_buf(),
+ ..config
+ })
+ }
+
+ /// `yaml_max_nodes`, with this config's file relative to `project_root` when it is inside it.
+ pub(crate) fn node_limit(&self, project_root: &Path) -> crate::yaml::NodeLimit {
+ crate::yaml::NodeLimit {
+ max_nodes: self.yaml_max_nodes,
+ config_path: crate::path_utils::relative_to_buf(project_root, &self.path),
+ }
}
}
#[cfg(test)]
mod tests {
- use std::{
- error::Error,
- fs::{self, File},
- };
+ use std::{error::Error, fs};
use indoc::indoc;
use tempfile::tempdir;
@@ -120,8 +159,7 @@ mod tests {
- \"{app,components,config,frontend,lib,packs,spec}/**/*.{rb,rake,js,jsx,ts,tsx,json,yml}\"
"};
fs::write(&config_path, config_str)?;
- let config_file = File::open(&config_path)?;
- let config: Config = serde_yaml::from_reader(config_file)?;
+ let config = Config::load_from_path(&config_path)?;
assert_eq!(
config.owned_globs,
vec!["{app,components,config,frontend,lib,packs,spec}/**/*.{rb,rake,js,jsx,ts,tsx,json,yml}"]
@@ -135,6 +173,53 @@ mod tests {
);
assert_eq!(config.vendored_gems_path, "vendored/");
assert_eq!(config.executable_name, "codeowners generate");
+ assert_eq!(config.yaml_max_nodes, 20_000);
+ Ok(())
+ }
+
+ #[test]
+ fn test_parse_config_with_custom_yaml_max_nodes() -> Result<(), Box> {
+ let temp_dir = tempdir()?;
+ let config_path = temp_dir.path().join("config.yml");
+ let config_str = indoc! {"
+ ---
+ owned_globs:
+ - \"**/*.rb\"
+ yaml_max_nodes: 200000
+ "};
+ fs::write(&config_path, config_str)?;
+ let config = Config::load_from_path(&config_path)?;
+ assert_eq!(config.yaml_max_nodes, 200_000);
+ Ok(())
+ }
+
+ #[test]
+ fn test_node_limit_names_the_config_file() -> Result<(), Box> {
+ let temp_dir = tempdir()?;
+ fs::create_dir(temp_dir.path().join("config"))?;
+ let config_path = temp_dir.path().join("config/alt.yml");
+ let config_str = indoc! {"
+ ---
+ owned_globs:
+ - \"**/*.rb\"
+ yaml_max_nodes: 30000
+ "};
+ fs::write(&config_path, config_str)?;
+ let config = Config::load_from_path(&config_path)?;
+ assert_eq!(
+ config.node_limit(temp_dir.path()),
+ crate::yaml::NodeLimit {
+ max_nodes: 30_000,
+ config_path: PathBuf::from("config/alt.yml"),
+ }
+ );
+ // A config file outside the project root is named in full, and a config not read from a file names the default.
+ assert_eq!(config.node_limit(Path::new("/elsewhere")).config_path, config_path);
+ let unread: Config = crate::yaml::from_slice(b"owned_globs: []\n", crate::yaml::DEFAULT_MAX_NODES)?;
+ assert_eq!(
+ unread.node_limit(temp_dir.path()).config_path,
+ Path::new("config/code_ownership.yml")
+ );
Ok(())
}
@@ -149,8 +234,7 @@ mod tests {
executable_name: my-custom-codeowners
"};
fs::write(&config_path, config_str)?;
- let config_file = File::open(&config_path)?;
- let config: Config = serde_yaml::from_reader(config_file)?;
+ let config = Config::load_from_path(&config_path)?;
assert_eq!(config.executable_name, "my-custom-codeowners");
Ok(())
}
@@ -165,8 +249,7 @@ mod tests {
- \"**/*.rb\"
"};
fs::write(&config_path, config_str)?;
- let config_file = File::open(&config_path)?;
- let config: Config = serde_yaml::from_reader(config_file)?;
+ let config = Config::load_from_path(&config_path)?;
assert_eq!(config.executable_name, "codeowners generate");
Ok(())
}
@@ -181,8 +264,7 @@ mod tests {
- \"**/*.rb\"
"};
fs::write(&config_path, config_str)?;
- let config_file = File::open(&config_path)?;
- let config: Config = serde_yaml::from_reader(config_file)?;
+ let config = Config::load_from_path(&config_path)?;
assert_eq!(config.codeowners_path, ".github");
Ok(())
}
@@ -198,9 +280,67 @@ mod tests {
codeowners_path: docs
"};
fs::write(&config_path, config_str)?;
- let config_file = File::open(&config_path)?;
- let config: Config = serde_yaml::from_reader(config_file)?;
+ let config = Config::load_from_path(&config_path)?;
assert_eq!(config.codeowners_path, "docs");
Ok(())
}
+
+ #[test]
+ fn test_parse_config_with_empty_values() -> Result<(), Box> {
+ let temp_dir = tempdir()?;
+ let config_path = temp_dir.path().join("config.yml");
+ let config_str = indoc! {"
+ ---
+ owned_globs:
+ - \"**/*.rb\"
+ -
+ ruby_package_paths:
+ -
+ javascript_package_paths:
+ -
+ team_file_glob:
+ -
+ unowned_globs:
+ -
+ ignore_dirs:
+ -
+ vendored_gems_path:
+ cache_directory:
+ executable_name:
+ codeowners_path:
+ "};
+ fs::write(&config_path, config_str)?;
+ let config = Config::load_from_path(&config_path)?;
+ assert_eq!(config.owned_globs, vec!["**/*.rb", ""]);
+ for globs in [
+ &config.ruby_package_paths,
+ &config.javascript_package_paths,
+ &config.team_file_glob,
+ &config.unowned_globs,
+ &config.ignore_dirs,
+ ] {
+ assert_eq!(globs, &[""]);
+ }
+ for value in [
+ &config.vendored_gems_path,
+ &config.cache_directory,
+ &config.executable_name,
+ &config.codeowners_path,
+ ] {
+ assert_eq!(value, "");
+ }
+ Ok(())
+ }
+
+ #[test]
+ fn test_load_from_path_reports_parse_error_on_one_line() -> Result<(), Box> {
+ let temp_dir = tempdir()?;
+ let config_path = temp_dir.path().join("config.yml");
+ fs::write(&config_path, "owned_globs: {app: true}\n")?;
+ let error = Config::load_from_path(&config_path).unwrap_err();
+ assert!(error.starts_with("Can't parse config file: "), "{error}");
+ assert!(error.contains("line 1"), "{error}");
+ assert!(!error.contains('\n'), "{error}");
+ Ok(())
+ }
}
diff --git a/src/lib.rs b/src/lib.rs
index 13d53a4..eb35bd2 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -9,3 +9,4 @@ pub mod project_builder;
pub mod project_file_builder;
pub mod runner;
pub(crate) mod tracked_files;
+pub(crate) mod yaml;
diff --git a/src/ownership/codeowners_file_parser.rs b/src/ownership/codeowners_file_parser.rs
index fd25691..a59d855 100644
--- a/src/ownership/codeowners_file_parser.rs
+++ b/src/ownership/codeowners_file_parser.rs
@@ -1,6 +1,7 @@
use crate::{
ownership::{FileGenerator, TeamOwnership},
project::Team,
+ yaml::NodeLimit,
};
use fast_glob::glob_match;
use memoize::memoize;
@@ -14,6 +15,7 @@ pub struct Parser {
pub project_root: PathBuf,
pub codeowners_file_path: PathBuf,
pub team_file_globs: Vec,
+ pub node_limit: NodeLimit,
}
impl Parser {
@@ -40,7 +42,7 @@ impl Parser {
let codeowners_entries = parse_codeowners_entries(self.codeowners_file_path.to_string_lossy().into_owned());
- let teams_by_name = teams_by_github_team_name(self.absolute_team_files_globs());
+ let teams_by_name = teams_by_github_team_name(self.absolute_team_files_globs(), self.node_limit.clone());
let result: HashMap> = file_inputs
.par_iter()
@@ -65,13 +67,13 @@ impl Parser {
}
#[memoize]
-fn teams_by_github_team_name(team_file_glob: Vec) -> HashMap {
+fn teams_by_github_team_name(team_file_glob: Vec, node_limit: NodeLimit) -> HashMap {
let mut teams = HashMap::new();
for glob in team_file_glob {
match glob::glob(&glob) {
Ok(paths) => {
for path in paths.filter_map(Result::ok) {
- let team = match Team::from_team_file_path(path.clone()) {
+ let team = match Team::from_team_file_path(path.clone(), &node_limit) {
Ok(team) => team,
Err(e) => {
eprintln!("Error parsing team file: {e:?}, path: {}", path.display());
@@ -495,6 +497,35 @@ mod tests {
Ok(())
}
+ #[test]
+ fn test_teams_from_files_paths_is_keyed_on_yaml_max_nodes() -> Result<(), Box> {
+ let temp_dir = tempfile::tempdir()?;
+ let root = temp_dir.path();
+ fs::create_dir_all(root.join("config/teams"))?;
+ fs::write(
+ root.join("config/teams/payroll.yml"),
+ "name: Payroll\ngithub:\n team: '@PayrollTeam'\n",
+ )?;
+ fs::write(root.join("CODEOWNERS"), "# Team-specific owned globs\n/app/**/* @PayrollTeam\n")?;
+ let owner_of = |yaml_max_nodes: usize| -> Result