Skip to content

Commit 9aabccd

Browse files
committed
adding verify compare for file
1 parent 27dc33c commit 9aabccd

File tree

7 files changed

+151
-2
lines changed

7 files changed

+151
-2
lines changed

prompts/owner-overrides.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Feature Request
2+
3+
## Owner overrides
4+
5+
### Context
6+
7+
Today, if more than one mapper claims ownership of a file, we return an error. There are limited exceptions for directory ownership where multiple directories may apply, and we conceptually pick the nearest directory.
8+
9+
We have seen frequent confusion and repeated requests for a way to explicitly override ownership.
10+
11+
### Proposal
12+
13+
Allow overrides, resolving conflicts by choosing the most specific claim.
14+
15+
#### Priority (most specific to least specific)
16+
17+
Ownership for a file is determined by the _closest_ applicable claim:
18+
19+
1. File annotations (inline annotations in the file)
20+
1. *Directory ownership (the nearest ancestor directory claim)
21+
1. Package ownership (`package.yml`, `package.json`)
22+
1. Team glob patterns
23+
24+
If multiple teams match at the same priority level (e.g., multiple team glob patterns match the same path), this remains an error.
25+
26+
#### Tooling to reduce confusion
27+
28+
Update the `for-file` command to list all matching teams in descending priority, so users can see which owner would win and why.
29+
30+
31+
* If directory ownership is declared _above_ a package file, it would be lower priority

src/cli.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ enum Command {
4646

4747
#[clap(about = "Delete the cache file.", visible_alias = "d")]
4848
DeleteCache,
49+
50+
#[clap(about = "Compare the CODEOWNERS file to the for-file command.")]
51+
VerifyCompareForFile,
4952
}
5053

5154
/// A CLI to validate and generate Github's CODEOWNERS file.
@@ -113,6 +116,7 @@ pub fn cli() -> Result<RunResult, RunnerError> {
113116
Command::ForFile { name, fast: _ } => runner::for_file(&run_config, &name),
114117
Command::ForTeam { name } => runner::for_team(&run_config, &name),
115118
Command::DeleteCache => runner::delete_cache(&run_config),
119+
Command::VerifyCompareForFile => runner::verify_compare_for_file(&run_config),
116120
};
117121

118122
Ok(runner_result)

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ pub(crate) mod project;
66
pub mod project_builder;
77
pub mod project_file_builder;
88
pub mod runner;
9+
pub mod verify;

src/runner.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,10 @@ pub fn delete_cache(run_config: &RunConfig) -> RunResult {
132132
run_with_runner(run_config, |runner| runner.delete_cache())
133133
}
134134

135+
pub fn verify_compare_for_file(run_config: &RunConfig) -> RunResult {
136+
run_with_runner(run_config, |runner| runner.verify_compare_for_file())
137+
}
138+
135139
pub type Runnable = fn(Runner) -> RunResult;
136140

137141
pub fn run_with_runner<F>(run_config: &RunConfig, runnable: F) -> RunResult
@@ -172,7 +176,7 @@ impl fmt::Display for Error {
172176
}
173177
}
174178

175-
fn config_from_path(path: &PathBuf) -> Result<Config, Error> {
179+
pub(crate) fn config_from_path(path: &PathBuf) -> Result<Config, Error> {
176180
let config_file = File::open(path)
177181
.change_context(Error::Io(format!("Can't open config file: {}", &path.to_string_lossy())))
178182
.attach_printable(format!("Can't open config file: {}", &path.to_string_lossy()))?;
@@ -299,6 +303,10 @@ impl Runner {
299303
},
300304
}
301305
}
306+
307+
pub fn verify_compare_for_file(&self) -> RunResult {
308+
crate::verify::verify_compare_for_file(&self.run_config, &self.cache)
309+
}
302310
}
303311

304312
#[cfg(test)]

src/verify.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
use std::path::Path;
2+
3+
use crate::{
4+
cache::Cache,
5+
config::Config,
6+
ownership::for_file_fast::find_file_owners,
7+
project::Project,
8+
project_builder::ProjectBuilder,
9+
runner::{RunConfig, RunResult, config_from_path, team_for_file_from_codeowners},
10+
};
11+
12+
pub fn verify_compare_for_file(run_config: &RunConfig, cache: &Cache) -> RunResult {
13+
match do_verify_compare_for_file(run_config, cache) {
14+
Ok(mismatches) if mismatches.is_empty() => RunResult {
15+
info_messages: vec!["Success! All files match between CODEOWNERS and for-file command.".to_string()],
16+
..Default::default()
17+
},
18+
Ok(mismatches) => RunResult {
19+
validation_errors: mismatches,
20+
..Default::default()
21+
},
22+
Err(err) => RunResult {
23+
io_errors: vec![err],
24+
..Default::default()
25+
},
26+
}
27+
}
28+
29+
fn do_verify_compare_for_file(run_config: &RunConfig, cache: &Cache) -> Result<Vec<String>, String> {
30+
let config = load_config(run_config)?;
31+
let project = build_project(&config, run_config, cache)?;
32+
33+
let mut mismatches: Vec<String> = Vec::new();
34+
for file in &project.files {
35+
let (codeowners_team, fast_display) = owners_for_file(&file.path, run_config, &config)?;
36+
let codeowners_display = codeowners_team.clone().unwrap_or_else(|| "Unowned".to_string());
37+
if !is_match(codeowners_team.as_deref(), &fast_display) {
38+
mismatches.push(format_mismatch(&project, &file.path, &codeowners_display, &fast_display));
39+
}
40+
}
41+
42+
Ok(mismatches)
43+
}
44+
45+
fn load_config(run_config: &RunConfig) -> Result<Config, String> {
46+
config_from_path(&run_config.config_path).map_err(|e| e.to_string())
47+
}
48+
49+
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+
);
56+
project_builder.build().map_err(|e| e.to_string())
57+
}
58+
59+
fn owners_for_file(path: &Path, run_config: &RunConfig, config: &Config) -> Result<(Option<String>, String), String> {
60+
let file_path_str = path.to_string_lossy().to_string();
61+
62+
let codeowners_team = team_for_file_from_codeowners(run_config, &file_path_str)
63+
.map_err(|e| e.to_string())?
64+
.map(|t| t.name);
65+
66+
let fast_owners = find_file_owners(&run_config.project_root, config, Path::new(&file_path_str))?;
67+
let fast_display = match fast_owners.len() {
68+
0 => "Unowned".to_string(),
69+
1 => fast_owners[0].team.name.clone(),
70+
_ => {
71+
let names: Vec<String> = fast_owners.into_iter().map(|fo| fo.team.name).collect();
72+
format!("Multiple: {}", names.join(", "))
73+
}
74+
};
75+
76+
Ok((codeowners_team, fast_display))
77+
}
78+
79+
fn is_match(codeowners_team: Option<&str>, fast_display: &str) -> bool {
80+
match (codeowners_team, fast_display) {
81+
(None, "Unowned") => true,
82+
(Some(t), fd) if fd == t => true,
83+
_ => false,
84+
}
85+
}
86+
87+
fn format_mismatch(project: &Project, file_path: &Path, codeowners_display: &str, fast_display: &str) -> String {
88+
let rel = project.relative_path(file_path).to_string_lossy().to_string();
89+
format!("- {}: CODEOWNERS={} fast={}", rel, codeowners_display, fast_display)
90+
}

tests/fixtures/valid_project_with_overrides/tmp/cache/codeowners/project-file-cache.json

Lines changed: 0 additions & 1 deletion
This file was deleted.

tests/valid_project_test.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,22 @@ fn test_generate() -> Result<(), Box<dyn Error>> {
3636
Ok(())
3737
}
3838

39+
#[test]
40+
fn test_verify_compare_for_file() -> Result<(), Box<dyn Error>> {
41+
Command::cargo_bin("codeowners")?
42+
.arg("--project-root")
43+
.arg("tests/fixtures/valid_project")
44+
.arg("--no-cache")
45+
.arg("verify-compare-for-file")
46+
.assert()
47+
.success()
48+
.stdout(predicate::eq(indoc! {"
49+
Success! All files match between CODEOWNERS and for-file command.
50+
"}));
51+
52+
Ok(())
53+
}
54+
3955
#[test]
4056
fn test_for_file() -> Result<(), Box<dyn Error>> {
4157
Command::cargo_bin("codeowners")?

0 commit comments

Comments
 (0)