Skip to content

Commit c8a5d53

Browse files
authored
Allow to specify CODEOWNERS location in config or env var (#95)
1 parent 68a5e19 commit c8a5d53

File tree

18 files changed

+235
-30
lines changed

18 files changed

+235
-30
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,9 +253,10 @@ use codeowners::runner::{RunConfig, for_file, teams_for_files_from_codeowners};
253253
fn main() {
254254
let run_config = RunConfig {
255255
project_root: std::path::PathBuf::from("."),
256-
codeowners_file_path: std::path::PathBuf::from(".github/CODEOWNERS"),
256+
codeowners_file_path: Some(std::path::PathBuf::from(".github/CODEOWNERS")), // optional, if None provided, will be resolved from config/env
257257
config_path: std::path::PathBuf::from("config/code_ownership.yml"),
258258
no_cache: true, // set false to enable on-disk caching
259+
executable_name: None,
259260
};
260261
261262
// Find owner for a single file using the optimized path (not just CODEOWNERS)

src/cli.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,9 @@ struct Args {
6565
#[command(subcommand)]
6666
command: Command,
6767

68-
/// Path for the CODEOWNERS file.
69-
#[arg(long, default_value = "./.github/CODEOWNERS")]
70-
codeowners_file_path: PathBuf,
68+
/// Path for the CODEOWNERS file (overrides codeowners_path from the config) [default: .github/CODEOWNERS]
69+
#[arg(long)]
70+
codeowners_file_path: Option<PathBuf>,
7171
/// Path for the configuration file
7272
#[arg(long, default_value = "./config/code_ownership.yml")]
7373
config_path: PathBuf,
@@ -93,8 +93,11 @@ impl Args {
9393
Ok(self.absolute_path(&self.config_path)?.clean())
9494
}
9595

96-
fn absolute_codeowners_path(&self) -> Result<PathBuf, RunnerError> {
97-
Ok(self.absolute_path(&self.codeowners_file_path)?.clean())
96+
fn absolute_codeowners_path(&self) -> Result<Option<PathBuf>, RunnerError> {
97+
match &self.codeowners_file_path {
98+
Some(path) => Ok(Some(self.absolute_path(path)?.clean())),
99+
None => Ok(None),
100+
}
98101
}
99102

100103
fn absolute_path(&self, path: &Path) -> Result<PathBuf, RunnerError> {

src/config.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ pub struct Config {
2828

2929
#[serde(default = "default_executable_name")]
3030
pub executable_name: String,
31+
32+
#[serde(default = "default_codeowners_path")]
33+
pub codeowners_path: String,
3134
}
3235

3336
#[allow(dead_code)]
@@ -84,6 +87,10 @@ fn default_ignore_dirs() -> Vec<String> {
8487
]
8588
}
8689

90+
fn default_codeowners_path() -> String {
91+
".github".to_string()
92+
}
93+
8794
impl Config {
8895
pub fn load_from_path(path: &Path) -> std::result::Result<Self, String> {
8996
let file = File::open(path).map_err(|e| format!("Can't open config file: {} ({})", path.to_string_lossy(), e))?;
@@ -163,4 +170,37 @@ mod tests {
163170
assert_eq!(config.executable_name, "codeowners");
164171
Ok(())
165172
}
173+
174+
#[test]
175+
fn test_codeowners_path_defaults_when_not_specified() -> Result<(), Box<dyn Error>> {
176+
let temp_dir = tempdir()?;
177+
let config_path = temp_dir.path().join("config.yml");
178+
let config_str = indoc! {"
179+
---
180+
owned_globs:
181+
- \"**/*.rb\"
182+
"};
183+
fs::write(&config_path, config_str)?;
184+
let config_file = File::open(&config_path)?;
185+
let config: Config = serde_yaml::from_reader(config_file)?;
186+
assert_eq!(config.codeowners_path, ".github");
187+
Ok(())
188+
}
189+
190+
#[test]
191+
fn test_codeowners_path_can_be_customized() -> Result<(), Box<dyn Error>> {
192+
let temp_dir = tempdir()?;
193+
let config_path = temp_dir.path().join("config.yml");
194+
let config_str = indoc! {"
195+
---
196+
owned_globs:
197+
- \"**/*.rb\"
198+
codeowners_path: docs
199+
"};
200+
fs::write(&config_path, config_str)?;
201+
let config_file = File::open(&config_path)?;
202+
let config: Config = serde_yaml::from_reader(config_file)?;
203+
assert_eq!(config.codeowners_path, "docs");
204+
Ok(())
205+
}
166206
}

src/crosscheck.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,9 @@ fn load_config(run_config: &RunConfig) -> Result<Config, String> {
4747
}
4848

4949
fn build_project(config: &Config, run_config: &RunConfig, cache: &Cache) -> Result<Project, String> {
50-
let mut project_builder = ProjectBuilder::new(
51-
config,
52-
run_config.project_root.clone(),
53-
run_config.codeowners_file_path.clone(),
54-
cache,
55-
);
50+
use crate::runner::resolve_codeowners_file_path;
51+
let codeowners_file_path = resolve_codeowners_file_path(run_config, config);
52+
let mut project_builder = ProjectBuilder::new(config, run_config.project_root.clone(), codeowners_file_path, cache);
5653
project_builder.build().map_err(|e| e.to_string())
5754
}
5855

src/ownership/file_owner_resolver.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ mod tests {
294294
cache_directory: "tmp/cache/codeowners".to_string(),
295295
ignore_dirs: vec![],
296296
executable_name: "codeowners".to_string(),
297+
codeowners_path: ".github".to_string(),
297298
}
298299
}
299300

src/ownership/mapper/package_mapper.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ mod tests {
147147
use std::{error::Error, path::Path};
148148
#[test]
149149
fn test_remove_nested_packages() {
150-
let packages = vec![
150+
let packages = [
151151
Package {
152152
path: Path::new("packs/a/package.yml").to_owned(),
153153
package_type: PackageType::Ruby,

src/runner.rs

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::path::PathBuf;
12
use std::process::Command;
23

34
use error_stack::{Result, ResultExt};
@@ -20,6 +21,7 @@ pub struct Runner {
2021
ownership: Ownership,
2122
cache: Cache,
2223
config: Config,
24+
codeowners_file_path: PathBuf,
2325
}
2426

2527
pub fn version() -> String {
@@ -55,9 +57,30 @@ pub(crate) fn config_from_run_config(run_config: &RunConfig) -> Result<Config, E
5557
Err(msg) => Err(error_stack::Report::new(Error::Io(msg))),
5658
}
5759
}
60+
61+
/// Resolves the CODEOWNERS file path with the following priority:
62+
/// 1. Explicit `codeowners_file_path` in `RunConfig` (if provided from e.g. CLI flag)
63+
/// 2. `CODEOWNERS_PATH` environment variable (if set and not empty)
64+
/// 3. Computed from `codeowners_path` directory path in config + "CODEOWNERS" filename
65+
/// 4. Default fallback to `.github/CODEOWNERS` (using default codeowners_path from config)
66+
pub(crate) fn resolve_codeowners_file_path(run_config: &RunConfig, config: &Config) -> PathBuf {
67+
if let Some(ref path) = run_config.codeowners_file_path {
68+
return path.clone();
69+
}
70+
71+
if let Ok(env_path) = std::env::var("CODEOWNERS_PATH")
72+
&& !env_path.is_empty()
73+
{
74+
return run_config.project_root.join(env_path);
75+
}
76+
77+
run_config.project_root.join(&config.codeowners_path).join("CODEOWNERS")
78+
}
79+
5880
impl Runner {
5981
pub fn new(run_config: &RunConfig) -> Result<Self, Error> {
6082
let config = config_from_run_config(run_config)?;
83+
let codeowners_file_path = resolve_codeowners_file_path(run_config, &config);
6184

6285
let cache: Cache = if run_config.no_cache {
6386
NoopCache::default().into()
@@ -71,12 +94,7 @@ impl Runner {
7194
.into()
7295
};
7396

74-
let mut project_builder = ProjectBuilder::new(
75-
&config,
76-
run_config.project_root.clone(),
77-
run_config.codeowners_file_path.clone(),
78-
&cache,
79-
);
97+
let mut project_builder = ProjectBuilder::new(&config, run_config.project_root.clone(), codeowners_file_path.clone(), &cache);
8098
let project = project_builder.build().change_context(Error::Io(format!(
8199
"Can't build project: {}",
82100
&run_config.config_path.to_string_lossy()
@@ -93,6 +111,7 @@ impl Runner {
93111
ownership,
94112
cache,
95113
config,
114+
codeowners_file_path,
96115
})
97116
}
98117

@@ -150,10 +169,10 @@ impl Runner {
150169

151170
pub fn generate(&self, git_stage: bool) -> RunResult {
152171
let content = self.ownership.generate_file();
153-
if let Some(parent) = &self.run_config.codeowners_file_path.parent() {
172+
if let Some(parent) = &self.codeowners_file_path.parent() {
154173
let _ = std::fs::create_dir_all(parent);
155174
}
156-
match std::fs::write(&self.run_config.codeowners_file_path, content) {
175+
match std::fs::write(&self.codeowners_file_path, content) {
157176
Ok(_) => {
158177
if git_stage {
159178
self.git_stage();
@@ -178,7 +197,7 @@ impl Runner {
178197
fn git_stage(&self) {
179198
let _ = Command::new("git")
180199
.arg("add")
181-
.arg(&self.run_config.codeowners_file_path)
200+
.arg(&self.codeowners_file_path)
182201
.current_dir(&self.run_config.project_root)
183202
.output();
184203
}

src/runner/api.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,10 @@ pub fn teams_for_files_from_codeowners(
6161
file_paths: &[String],
6262
) -> error_stack::Result<HashMap<String, Option<Team>>, Error> {
6363
let config = config_from_run_config(run_config)?;
64+
let codeowners_file_path = super::resolve_codeowners_file_path(run_config, &config);
6465
let res = crate::ownership::codeowners_query::teams_for_files_from_codeowners(
6566
&run_config.project_root,
66-
&run_config.codeowners_file_path,
67+
&codeowners_file_path,
6768
&config.team_file_glob,
6869
file_paths,
6970
)

src/runner/types.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ pub struct RunResult {
1414
#[derive(Debug, Clone)]
1515
pub struct RunConfig {
1616
pub project_root: PathBuf,
17-
pub codeowners_file_path: PathBuf,
17+
pub codeowners_file_path: Option<PathBuf>,
1818
pub config_path: PathBuf,
1919
pub no_cache: bool,
2020
pub executable_name: Option<String>,

tests/codeowners_path_test.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
use predicates::prelude::predicate;
2+
use std::{error::Error, fs, path::Path};
3+
4+
mod common;
5+
6+
use common::OutputStream;
7+
use common::git_add_all_files;
8+
use common::run_codeowners;
9+
use common::setup_fixture_repo;
10+
11+
#[test]
12+
fn test_generate_uses_codeowners_path_from_config() -> Result<(), Box<dyn Error>> {
13+
let fixture_root = Path::new("tests/fixtures/custom_codeowners_path");
14+
let temp_dir = setup_fixture_repo(fixture_root);
15+
let project_root = temp_dir.path();
16+
git_add_all_files(project_root);
17+
18+
let mut cmd = assert_cmd::Command::cargo_bin("codeowners")?;
19+
cmd.arg("--project-root")
20+
.arg(project_root)
21+
.arg("--no-cache")
22+
.arg("generate")
23+
.assert()
24+
.success();
25+
26+
let expected_codeowners: String = std::fs::read_to_string(Path::new("tests/fixtures/custom_codeowners_path/expected/CODEOWNERS"))?;
27+
let actual_codeowners: String = std::fs::read_to_string(project_root.join("docs/CODEOWNERS"))?;
28+
29+
assert_eq!(expected_codeowners, actual_codeowners);
30+
31+
Ok(())
32+
}
33+
34+
#[test]
35+
fn test_cli_overrides_codeowners_path_from_config() -> Result<(), Box<dyn Error>> {
36+
fs::create_dir_all("tmp")?;
37+
let codeowners_abs = std::env::current_dir()?.join("tmp/CODEOWNERS");
38+
let codeowners_str = codeowners_abs.to_str().unwrap();
39+
40+
run_codeowners(
41+
"custom_codeowners_path",
42+
&["--codeowners-file-path", codeowners_str, "generate"],
43+
true,
44+
OutputStream::Stdout,
45+
predicate::eq(""),
46+
)?;
47+
48+
let expected_codeowners: String = std::fs::read_to_string(Path::new("tests/fixtures/custom_codeowners_path/expected/CODEOWNERS"))?;
49+
let actual_codeowners: String = std::fs::read_to_string(Path::new("tmp/CODEOWNERS"))?;
50+
51+
assert_eq!(expected_codeowners, actual_codeowners);
52+
53+
Ok(())
54+
}
55+
56+
#[test]
57+
fn test_validate_uses_codeowners_path_from_config() -> Result<(), Box<dyn Error>> {
58+
run_codeowners(
59+
"custom_codeowners_path",
60+
&["validate"],
61+
true,
62+
OutputStream::Stdout,
63+
predicate::eq(""),
64+
)?;
65+
66+
Ok(())
67+
}
68+
69+
#[test]
70+
fn test_validate_uses_cli_override() -> Result<(), Box<dyn Error>> {
71+
fs::create_dir_all("tmp")?;
72+
let codeowners_abs = std::env::current_dir()?.join("tmp/CODEOWNERS");
73+
let codeowners_str = codeowners_abs.to_str().unwrap();
74+
75+
run_codeowners(
76+
"custom_codeowners_path",
77+
&["--codeowners-file-path", codeowners_str, "generate"],
78+
true,
79+
OutputStream::Stdout,
80+
predicate::eq(""),
81+
)?;
82+
83+
run_codeowners(
84+
"custom_codeowners_path",
85+
&["--codeowners-file-path", codeowners_str, "validate"],
86+
true,
87+
OutputStream::Stdout,
88+
predicate::eq(""),
89+
)?;
90+
91+
Ok(())
92+
}

0 commit comments

Comments
 (0)