Skip to content

Commit f3badee

Browse files
authored
New file_owners_for_file providing reason for ownership (#75)
* new file_owners_for_file so consumers can view reason for ownership * mentioning code_ownership in readme
1 parent 94c9bdf commit f3badee

File tree

6 files changed

+148
-107
lines changed

6 files changed

+148
-107
lines changed

README.md

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Codeowners
22

3-
**Codeowners** is a fast, Rust-based CLI for generating and validating [GitHub `CODEOWNERS` files](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners) in large repositories. It supports conventions for Ruby and JavaScript projects, and is a high-performance reimplementation of the [original Ruby CLI](https://github.com/rubyatscale/code_ownership).
3+
**Codeowners** is a fast, Rust-based CLI for generating and validating [GitHub `CODEOWNERS` files](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners) in large repositories.
4+
5+
Note: For Ruby application, it's usually easier to use `codeowners-rs` via the [code_ownership](https://github.com/rubyatscale/code_ownership) gem.
46

57
## 🚀 Quick Start: Generate & Validate
68

@@ -15,20 +17,6 @@ codeowners gv
1517
- Validate that all files are properly owned and that the file is up to date
1618
- Exit with a nonzero code and detailed errors if validation fails
1719

18-
**Why use this tool?**
19-
On large projects, `codeowners gv` is _over 10x faster_ than the legacy Ruby implementation:
20-
21-
```
22-
$ hyperfine 'codeownership validate' 'codeowners validate'
23-
Benchmark 1: codeownership validate (ruby gem)
24-
Time (mean ± σ): 47.991 s ± 1.220 s
25-
Benchmark 2: codeowners gv (this repo)
26-
Time (mean ± σ): 4.263 s ± 0.025 s
27-
28-
Summary
29-
codeowners gv ran 11.26 ± 0.29 times faster than codeownership validate
30-
```
31-
3220
## Table of Contents
3321

3422
- [Quick Start: Generate & Validate](#-quick-start-generate--validate)

src/common_test.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ pub mod tests {
2828
}};
2929
}
3030

31-
const DEFAULT_CODE_OWNERSHIP_YML: &str = indoc! {"
31+
pub const DEFAULT_CODE_OWNERSHIP_YML: &str = indoc! {"
3232
---
3333
owned_globs:
3434
- \"{app,components,config,frontend,lib,packs,spec,ruby}/**/*.{rb,rake,js,jsx,ts,tsx,json,yml,erb}\"

src/ownership/for_file_fast.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ pub fn find_file_owners(project_root: &Path, config: &Config, file_path: &Path)
3636
&& !is_config_unowned
3737
&& let Some(team) = teams_by_name.get(&team_name)
3838
{
39-
sources_by_team.entry(team.name.clone()).or_default().push(Source::TeamFile);
39+
sources_by_team.entry(team.name.clone()).or_default().push(Source::AnnotatedFile);
4040
}
4141
}
4242
}
@@ -277,7 +277,7 @@ fn vendored_gem_owner(relative_file_path: &Path, config: &Config, teams: &[Team]
277277
fn source_priority(source: &Source) -> u8 {
278278
match source {
279279
// Highest confidence first
280-
Source::TeamFile => 0,
280+
Source::AnnotatedFile => 0,
281281
Source::Directory(_) => 1,
282282
Source::Package(_, _) => 2,
283283
Source::TeamGlob(_) => 3,

src/ownership/mapper.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ pub type TeamName = String;
3333
#[derive(Debug, PartialEq, Clone)]
3434
pub enum Source {
3535
Directory(String),
36-
TeamFile,
36+
AnnotatedFile,
3737
TeamGem,
3838
TeamGlob(String),
3939
Package(String, String),
@@ -44,7 +44,7 @@ impl Display for Source {
4444
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
4545
match self {
4646
Source::Directory(path) => write!(f, "Owner specified in `{}/.codeowner`", path),
47-
Source::TeamFile => write!(f, "Owner annotation at the top of the file"),
47+
Source::AnnotatedFile => write!(f, "Owner annotation at the top of the file"),
4848
Source::TeamGem => write!(f, "Owner specified in Team YML's `owned_gems`"),
4949
Source::TeamGlob(glob) => write!(f, "Owner specified in Team YML as an owned_glob `{}`", glob),
5050
Source::Package(package_path, glob) => {
@@ -200,7 +200,7 @@ mod tests {
200200
Source::Directory("packs/bam".to_string()).to_string(),
201201
"Owner specified in `packs/bam/.codeowner`"
202202
);
203-
assert_eq!(Source::TeamFile.to_string(), "Owner annotation at the top of the file");
203+
assert_eq!(Source::AnnotatedFile.to_string(), "Owner annotation at the top of the file");
204204
assert_eq!(Source::TeamGem.to_string(), "Owner specified in Team YML's `owned_gems`");
205205
assert_eq!(
206206
Source::TeamGlob("a/glob/**".to_string()).to_string(),

src/ownership/mapper/team_file_mapper.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ impl Mapper for TeamFileMapper {
5959
}
6060
}
6161

62-
vec![OwnerMatcher::ExactMatches(path_to_team, Source::TeamFile)]
62+
vec![OwnerMatcher::ExactMatches(path_to_team, Source::AnnotatedFile)]
6363
}
6464

6565
fn name(&self) -> String {
@@ -150,7 +150,7 @@ mod tests {
150150
(PathBuf::from("ruby/app/views/foos/show.html.erb"), "Bar".to_owned()),
151151
(PathBuf::from("ruby/app/views/foos/_row.html.erb"), "Bam".to_owned()),
152152
]),
153-
Source::TeamFile,
153+
Source::AnnotatedFile,
154154
)];
155155
assert_eq!(owner_matchers, expected_owner_matchers);
156156
Ok(())

src/runner.rs

Lines changed: 137 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -43,96 +43,17 @@ pub fn for_file(run_config: &RunConfig, file_path: &str, from_codeowners: bool)
4343
for_file_optimized(run_config, file_path)
4444
}
4545

46-
fn for_file_codeowners_only(run_config: &RunConfig, file_path: &str) -> RunResult {
47-
match team_for_file_from_codeowners(run_config, file_path) {
48-
Ok(Some(team)) => {
49-
let relative_team_path = team
50-
.path
51-
.strip_prefix(&run_config.project_root)
52-
.unwrap_or(team.path.as_path())
53-
.to_string_lossy()
54-
.to_string();
55-
RunResult {
56-
info_messages: vec![format!(
57-
"Team: {}\nGithub Team: {}\nTeam YML: {}\nDescription:\n- Owner inferred from codeowners file",
58-
team.name, team.github_team, relative_team_path
59-
)],
60-
..Default::default()
61-
}
62-
}
63-
Ok(None) => RunResult::default(),
64-
Err(err) => RunResult {
65-
io_errors: vec![err.to_string()],
66-
..Default::default()
67-
},
68-
}
69-
}
70-
pub fn team_for_file_from_codeowners(run_config: &RunConfig, file_path: &str) -> Result<Option<Team>, Error> {
71-
let config = config_from_path(&run_config.config_path)?;
72-
let relative_file_path = Path::new(file_path)
73-
.strip_prefix(&run_config.project_root)
74-
.unwrap_or(Path::new(file_path));
75-
76-
let parser = crate::ownership::parser::Parser {
77-
project_root: run_config.project_root.clone(),
78-
codeowners_file_path: run_config.codeowners_file_path.clone(),
79-
team_file_globs: config.team_file_glob.clone(),
80-
};
81-
Ok(parser
82-
.team_from_file_path(Path::new(relative_file_path))
83-
.map_err(|e| Error::Io(e.to_string()))?)
84-
}
85-
86-
pub fn team_for_file(run_config: &RunConfig, file_path: &str) -> Result<Option<Team>, Error> {
46+
pub fn file_owners_for_file(run_config: &RunConfig, file_path: &str) -> Result<Vec<FileOwner>, Error> {
8747
let config = config_from_path(&run_config.config_path)?;
8848
use crate::ownership::for_file_fast::find_file_owners;
8949
let owners = find_file_owners(&run_config.project_root, &config, std::path::Path::new(file_path)).map_err(Error::Io)?;
9050

91-
Ok(owners.first().map(|fo| fo.team.clone()))
51+
Ok(owners)
9252
}
9353

94-
// (imports below intentionally trimmed after refactor)
95-
96-
fn for_file_optimized(run_config: &RunConfig, file_path: &str) -> RunResult {
97-
let config = match config_from_path(&run_config.config_path) {
98-
Ok(c) => c,
99-
Err(err) => {
100-
return RunResult {
101-
io_errors: vec![err.to_string()],
102-
..Default::default()
103-
};
104-
}
105-
};
106-
107-
use crate::ownership::for_file_fast::find_file_owners;
108-
let file_owners = match find_file_owners(&run_config.project_root, &config, std::path::Path::new(file_path)) {
109-
Ok(v) => v,
110-
Err(err) => {
111-
return RunResult {
112-
io_errors: vec![err],
113-
..Default::default()
114-
};
115-
}
116-
};
117-
118-
let info_messages: Vec<String> = match file_owners.len() {
119-
0 => vec![format!("{}", FileOwner::default())],
120-
1 => vec![format!("{}", file_owners[0])],
121-
_ => {
122-
let mut error_messages = vec!["Error: file is owned by multiple teams!".to_string()];
123-
for file_owner in file_owners {
124-
error_messages.push(format!("\n{}", file_owner));
125-
}
126-
return RunResult {
127-
validation_errors: error_messages,
128-
..Default::default()
129-
};
130-
}
131-
};
132-
RunResult {
133-
info_messages,
134-
..Default::default()
135-
}
54+
pub fn team_for_file(run_config: &RunConfig, file_path: &str) -> Result<Option<Team>, Error> {
55+
let owners = file_owners_for_file(run_config, file_path)?;
56+
Ok(owners.first().map(|fo| fo.team.clone()))
13657
}
13758

13859
pub fn version() -> String {
@@ -336,12 +257,144 @@ impl Runner {
336257
}
337258
}
338259

260+
fn for_file_codeowners_only(run_config: &RunConfig, file_path: &str) -> RunResult {
261+
match team_for_file_from_codeowners(run_config, file_path) {
262+
Ok(Some(team)) => {
263+
let relative_team_path = team
264+
.path
265+
.strip_prefix(&run_config.project_root)
266+
.unwrap_or(team.path.as_path())
267+
.to_string_lossy()
268+
.to_string();
269+
RunResult {
270+
info_messages: vec![format!(
271+
"Team: {}\nGithub Team: {}\nTeam YML: {}\nDescription:\n- Owner inferred from codeowners file",
272+
team.name, team.github_team, relative_team_path
273+
)],
274+
..Default::default()
275+
}
276+
}
277+
Ok(None) => RunResult::default(),
278+
Err(err) => RunResult {
279+
io_errors: vec![err.to_string()],
280+
..Default::default()
281+
},
282+
}
283+
}
284+
pub fn team_for_file_from_codeowners(run_config: &RunConfig, file_path: &str) -> Result<Option<Team>, Error> {
285+
let config = config_from_path(&run_config.config_path)?;
286+
let relative_file_path = Path::new(file_path)
287+
.strip_prefix(&run_config.project_root)
288+
.unwrap_or(Path::new(file_path));
289+
290+
let parser = crate::ownership::parser::Parser {
291+
project_root: run_config.project_root.clone(),
292+
codeowners_file_path: run_config.codeowners_file_path.clone(),
293+
team_file_globs: config.team_file_glob.clone(),
294+
};
295+
Ok(parser
296+
.team_from_file_path(Path::new(relative_file_path))
297+
.map_err(|e| Error::Io(e.to_string()))?)
298+
}
299+
300+
fn for_file_optimized(run_config: &RunConfig, file_path: &str) -> RunResult {
301+
let config = match config_from_path(&run_config.config_path) {
302+
Ok(c) => c,
303+
Err(err) => {
304+
return RunResult {
305+
io_errors: vec![err.to_string()],
306+
..Default::default()
307+
};
308+
}
309+
};
310+
311+
use crate::ownership::for_file_fast::find_file_owners;
312+
let file_owners = match find_file_owners(&run_config.project_root, &config, std::path::Path::new(file_path)) {
313+
Ok(v) => v,
314+
Err(err) => {
315+
return RunResult {
316+
io_errors: vec![err],
317+
..Default::default()
318+
};
319+
}
320+
};
321+
322+
let info_messages: Vec<String> = match file_owners.len() {
323+
0 => vec![format!("{}", FileOwner::default())],
324+
1 => vec![format!("{}", file_owners[0])],
325+
_ => {
326+
let mut error_messages = vec!["Error: file is owned by multiple teams!".to_string()];
327+
for file_owner in file_owners {
328+
error_messages.push(format!("\n{}", file_owner));
329+
}
330+
return RunResult {
331+
validation_errors: error_messages,
332+
..Default::default()
333+
};
334+
}
335+
};
336+
RunResult {
337+
info_messages,
338+
..Default::default()
339+
}
340+
}
341+
339342
#[cfg(test)]
340343
mod tests {
344+
use tempfile::tempdir;
345+
346+
use crate::{common_test, ownership::mapper::Source};
347+
341348
use super::*;
342349

343350
#[test]
344351
fn test_version() {
345352
assert_eq!(version(), env!("CARGO_PKG_VERSION").to_string());
346353
}
354+
fn write_file(temp_dir: &Path, file_path: &str, content: &str) {
355+
let file_path = temp_dir.join(file_path);
356+
let _ = std::fs::create_dir_all(file_path.parent().unwrap());
357+
std::fs::write(file_path, content).unwrap();
358+
}
359+
360+
#[test]
361+
fn test_file_owners_for_file() {
362+
let temp_dir = tempdir().unwrap();
363+
write_file(
364+
temp_dir.path(),
365+
"config/code_ownership.yml",
366+
common_test::tests::DEFAULT_CODE_OWNERSHIP_YML,
367+
);
368+
["a", "b", "c"].iter().for_each(|name| {
369+
let team_yml = format!("name: {}\ngithub:\n team: \"@{}\"\n members:\n - {}member\n", name, name, name);
370+
write_file(temp_dir.path(), &format!("config/teams/{}.yml", name), &team_yml);
371+
});
372+
write_file(
373+
temp_dir.path(),
374+
"app/consumers/deep/nesting/nestdir/deep_file.rb",
375+
"# @team b\nclass DeepFile end;",
376+
);
377+
378+
let run_config = RunConfig {
379+
project_root: temp_dir.path().to_path_buf(),
380+
codeowners_file_path: temp_dir.path().join(".github/CODEOWNERS").to_path_buf(),
381+
config_path: temp_dir.path().join("config/code_ownership.yml").to_path_buf(),
382+
no_cache: false,
383+
};
384+
385+
let file_owners = file_owners_for_file(&run_config, "app/consumers/deep/nesting/nestdir/deep_file.rb").unwrap();
386+
assert_eq!(file_owners.len(), 1);
387+
assert_eq!(file_owners[0].team.name, "b");
388+
assert_eq!(file_owners[0].team.github_team, "@b");
389+
assert!(file_owners[0].team.path.to_string_lossy().ends_with("config/teams/b.yml"));
390+
assert_eq!(file_owners[0].sources.len(), 1);
391+
assert_eq!(file_owners[0].sources, vec![Source::AnnotatedFile]);
392+
393+
let team = team_for_file(&run_config, "app/consumers/deep/nesting/nestdir/deep_file.rb")
394+
.unwrap()
395+
.unwrap();
396+
assert_eq!(team.name, "b");
397+
assert_eq!(team.github_team, "@b");
398+
assert!(team.path.to_string_lossy().ends_with("config/teams/b.yml"));
399+
}
347400
}

0 commit comments

Comments
 (0)