Skip to content

Commit dabef4e

Browse files
authored
adding for-file (#29)
1 parent 2d877bc commit dabef4e

File tree

11 files changed

+246
-99
lines changed

11 files changed

+246
-99
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Commands:
1313
generate Generate the CODEOWNERS file and save it to '--codeowners-file-path'
1414
validate Validate the validity of the CODEOWNERS file. A validation failure will exit with a failure code and a detailed output of the validation errors
1515
generate-and-validate Chains both 'generate' and 'validate' commands
16+
for-file Print the owners for a given file
1617
help Print this message or the help of the given subcommand(s)
1718
1819
Options:

src/main.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use ownership::Ownership;
1+
use ownership::{FileOwner, Ownership};
22

33
use crate::project::Project;
44
use clap::{Parser, Subcommand};
@@ -18,6 +18,8 @@ mod project;
1818

1919
#[derive(Subcommand, Debug)]
2020
enum Command {
21+
/// Responds with ownership for a given file
22+
ForFile { name: String },
2123
/// Generate the CODEOWNERS file and save it to '--codeowners-file-path'.
2224
Generate,
2325

@@ -113,6 +115,19 @@ fn cli() -> Result<(), Error> {
113115
std::fs::write(codeowners_file_path, ownership.generate_file()).change_context(Error::Io)?;
114116
ownership.validate().change_context(Error::ValidationFailed)?
115117
}
118+
Command::ForFile { name } => {
119+
let file_owners = ownership.for_file(&name).change_context(Error::Io)?;
120+
match file_owners.len() {
121+
0 => println!("{}", FileOwner::default()),
122+
1 => println!("{}", file_owners[0]),
123+
_ => {
124+
println!("Error: file is owned by multiple teams!");
125+
for file_owner in file_owners {
126+
println!("\n{}\n", file_owner);
127+
}
128+
}
129+
}
130+
}
116131
}
117132

118133
Ok(())

src/ownership.rs

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1-
use mapper::TeamName;
2-
use std::sync::Arc;
1+
use file_owner_finder::FileOwnerFinder;
2+
use mapper::{OwnerMatcher, TeamName};
3+
use std::{
4+
fmt::{self, Display},
5+
path::Path,
6+
sync::Arc,
7+
};
38
use tracing::{info, instrument};
49

510
mod file_generator;
11+
mod file_owner_finder;
612
mod mapper;
713
mod validator;
814

9-
#[cfg(test)]
10-
mod tests;
11-
1215
use crate::{ownership::mapper::DirectoryMapper, project::Project};
1316

1417
pub use validator::Errors as ValidatorErrors;
@@ -23,6 +26,26 @@ pub struct Ownership {
2326
project: Arc<Project>,
2427
}
2528

29+
pub struct FileOwner {
30+
pub team_name: TeamName,
31+
pub team_config_file_path: String,
32+
}
33+
34+
impl Display for FileOwner {
35+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36+
write!(f, "Team: {}\nTeam YML: {}", self.team_name, self.team_config_file_path)
37+
}
38+
}
39+
40+
impl Default for FileOwner {
41+
fn default() -> Self {
42+
Self {
43+
team_name: "Unowned".to_string(),
44+
team_config_file_path: "Unowned".to_string(),
45+
}
46+
}
47+
}
48+
2649
#[allow(dead_code)]
2750
#[derive(Debug, PartialEq)]
2851
pub struct Entry {
@@ -62,6 +85,29 @@ impl Ownership {
6285
validator.validate()
6386
}
6487

88+
#[instrument(level = "debug", skip_all)]
89+
pub fn for_file(&self, file_path: &str) -> Result<Vec<FileOwner>, ValidatorErrors> {
90+
info!("getting file ownership for {}", file_path);
91+
let owner_matchers: Vec<OwnerMatcher> = self.mappers().iter().flat_map(|mapper| mapper.owner_matchers()).collect();
92+
let file_owner_finder = FileOwnerFinder {
93+
owner_matchers: &owner_matchers,
94+
};
95+
let owners = file_owner_finder.find(Path::new(file_path));
96+
Ok(owners
97+
.iter()
98+
.map(|owner| match self.project.get_team(&owner.team_name) {
99+
Some(team) => FileOwner {
100+
team_name: owner.team_name.clone(),
101+
team_config_file_path: team
102+
.path
103+
.strip_prefix(&self.project.base_path)
104+
.map_or_else(|_| String::new(), |p| p.to_string_lossy().to_string()),
105+
},
106+
None => FileOwner::default(),
107+
})
108+
.collect())
109+
}
110+
65111
#[instrument(level = "debug", skip_all)]
66112
pub fn generate_file(&self) -> String {
67113
info!("generating codeowners file");
@@ -81,3 +127,26 @@ impl Ownership {
81127
]
82128
}
83129
}
130+
131+
#[cfg(test)]
132+
mod tests {
133+
use crate::common_test::tests::build_ownership_with_all_mappers;
134+
135+
#[test]
136+
fn test_for_file_owner() -> Result<(), Box<dyn std::error::Error>> {
137+
let ownership = build_ownership_with_all_mappers()?;
138+
let file_owners = ownership.for_file("app/consumers/directory_owned.rb").unwrap();
139+
assert_eq!(file_owners.len(), 1);
140+
assert_eq!(file_owners[0].team_name, "Bar");
141+
assert_eq!(file_owners[0].team_config_file_path, "config/teams/bar.yml");
142+
Ok(())
143+
}
144+
145+
#[test]
146+
fn test_for_file_no_owner() -> Result<(), Box<dyn std::error::Error>> {
147+
let ownership = build_ownership_with_all_mappers()?;
148+
let file_owners = ownership.for_file("app/madeup/foo.rb").unwrap();
149+
assert_eq!(file_owners.len(), 0);
150+
Ok(())
151+
}
152+
}

src/ownership/file_owner_finder.rs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
use std::{collections::HashMap, path::Path};
2+
3+
use super::mapper::{directory_mapper::is_directory_mapper_source, OwnerMatcher, Source, TeamName};
4+
5+
#[derive(Debug)]
6+
pub struct Owner {
7+
pub sources: Vec<Source>,
8+
pub team_name: TeamName,
9+
}
10+
11+
pub struct FileOwnerFinder<'a> {
12+
pub owner_matchers: &'a [OwnerMatcher],
13+
}
14+
15+
impl<'a> FileOwnerFinder<'a> {
16+
pub fn find(&self, relative_path: &Path) -> Vec<Owner> {
17+
let mut team_sources_map: HashMap<&TeamName, Vec<Source>> = HashMap::new();
18+
let mut directory_overrider = DirectoryOverrider::default();
19+
20+
for owner_matcher in self.owner_matchers {
21+
let (owner, source) = owner_matcher.owner_for(relative_path);
22+
23+
if let Some(team_name) = owner {
24+
if is_directory_mapper_source(source) {
25+
directory_overrider.process(team_name, source);
26+
} else {
27+
team_sources_map.entry(team_name).or_default().push(source.clone());
28+
}
29+
}
30+
}
31+
32+
// Add most specific directory owner if it exists
33+
if let Some((team_name, source)) = directory_overrider.specific_directory_owner() {
34+
team_sources_map.entry(team_name).or_default().push(source.clone());
35+
}
36+
37+
team_sources_map
38+
.into_iter()
39+
.map(|(team_name, sources)| Owner {
40+
sources,
41+
team_name: team_name.clone(),
42+
})
43+
.collect()
44+
}
45+
}
46+
47+
/// DirectoryOverrider is used to override the owner of a directory if a more specific directory owner is found.
48+
#[derive(Debug, Default)]
49+
pub struct DirectoryOverrider<'a> {
50+
specific_directory_owner: Option<(&'a TeamName, &'a Source)>,
51+
}
52+
53+
impl<'a> DirectoryOverrider<'a> {
54+
fn process(&mut self, team_name: &'a TeamName, source: &'a Source) {
55+
if self
56+
.specific_directory_owner
57+
.map_or(true, |(_, current_source)| current_source.len() < source.len())
58+
{
59+
self.specific_directory_owner = Some((team_name, source));
60+
}
61+
}
62+
63+
fn specific_directory_owner(&self) -> Option<(&TeamName, &Source)> {
64+
self.specific_directory_owner
65+
}
66+
}
67+
68+
#[cfg(test)]
69+
mod tests {
70+
use super::*;
71+
72+
#[test]
73+
fn test_directory_overrider() {
74+
let mut directory_overrider = DirectoryOverrider::default();
75+
assert_eq!(directory_overrider.specific_directory_owner(), None);
76+
let team_name_1 = "team1".to_string();
77+
let source_1 = "src/**".to_string();
78+
directory_overrider.process(&team_name_1, &source_1);
79+
assert_eq!(directory_overrider.specific_directory_owner(), Some((&team_name_1, &source_1)));
80+
81+
let team_name_longest = "team2".to_string();
82+
let source_longest = "source/subdir/**".to_string();
83+
directory_overrider.process(&team_name_longest, &source_longest);
84+
assert_eq!(
85+
directory_overrider.specific_directory_owner(),
86+
Some((&team_name_longest, &source_longest))
87+
);
88+
89+
let team_name_3 = "team3".to_string();
90+
let source_3 = "source/**".to_string();
91+
directory_overrider.process(&team_name_3, &source_3);
92+
assert_eq!(
93+
directory_overrider.specific_directory_owner(),
94+
Some((&team_name_longest, &source_longest))
95+
);
96+
}
97+
}

src/ownership/mapper.rs

Lines changed: 1 addition & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
use directory_mapper::is_directory_mapper_source;
21
use glob_match::glob_match;
32
use std::{
43
collections::HashMap,
54
path::{Path, PathBuf},
65
};
76

8-
mod directory_mapper;
7+
pub(crate) mod directory_mapper;
98
mod package_mapper;
109
mod team_file_mapper;
1110
mod team_gem_mapper;
@@ -51,99 +50,10 @@ impl OwnerMatcher {
5150
}
5251
}
5352

54-
#[derive(Debug)]
55-
pub struct Owner {
56-
pub sources: Vec<Source>,
57-
pub team_name: TeamName,
58-
}
59-
60-
pub struct FileOwnerFinder<'a> {
61-
pub owner_matchers: &'a [OwnerMatcher],
62-
}
63-
64-
impl<'a> FileOwnerFinder<'a> {
65-
pub fn find(&self, relative_path: &Path) -> Vec<Owner> {
66-
let mut team_sources_map: HashMap<&TeamName, Vec<Source>> = HashMap::new();
67-
let mut directory_overrider = DirectoryOverrider::default();
68-
69-
for owner_matcher in self.owner_matchers {
70-
let (owner, source) = owner_matcher.owner_for(relative_path);
71-
72-
if let Some(team_name) = owner {
73-
if is_directory_mapper_source(source) {
74-
directory_overrider.process(team_name, source);
75-
} else {
76-
team_sources_map.entry(team_name).or_default().push(source.clone());
77-
}
78-
}
79-
}
80-
81-
// Add most specific directory owner if it exists
82-
if let Some((team_name, source)) = directory_overrider.specific_directory_owner() {
83-
team_sources_map.entry(team_name).or_default().push(source.clone());
84-
}
85-
86-
team_sources_map
87-
.into_iter()
88-
.map(|(team_name, sources)| Owner {
89-
sources,
90-
team_name: team_name.clone(),
91-
})
92-
.collect()
93-
}
94-
}
95-
96-
/// DirectoryOverrider is used to override the owner of a directory if a more specific directory owner is found.
97-
#[derive(Debug, Default)]
98-
struct DirectoryOverrider<'a> {
99-
specific_directory_owner: Option<(&'a TeamName, &'a Source)>,
100-
}
101-
102-
impl<'a> DirectoryOverrider<'a> {
103-
fn process(&mut self, team_name: &'a TeamName, source: &'a Source) {
104-
if self
105-
.specific_directory_owner
106-
.map_or(true, |(_, current_source)| current_source.len() < source.len())
107-
{
108-
self.specific_directory_owner = Some((team_name, source));
109-
}
110-
}
111-
112-
fn specific_directory_owner(&self) -> Option<(&TeamName, &Source)> {
113-
self.specific_directory_owner
114-
}
115-
}
116-
11753
#[cfg(test)]
11854
mod tests {
11955
use super::*;
12056

121-
#[test]
122-
fn test_directory_overrider() {
123-
let mut directory_overrider = DirectoryOverrider::default();
124-
assert_eq!(directory_overrider.specific_directory_owner(), None);
125-
let team_name_1 = "team1".to_string();
126-
let source_1 = "src/**".to_string();
127-
directory_overrider.process(&team_name_1, &source_1);
128-
assert_eq!(directory_overrider.specific_directory_owner(), Some((&team_name_1, &source_1)));
129-
130-
let team_name_longest = "team2".to_string();
131-
let source_longest = "source/subdir/**".to_string();
132-
directory_overrider.process(&team_name_longest, &source_longest);
133-
assert_eq!(
134-
directory_overrider.specific_directory_owner(),
135-
Some((&team_name_longest, &source_longest))
136-
);
137-
138-
let team_name_3 = "team3".to_string();
139-
let source_3 = "source/**".to_string();
140-
directory_overrider.process(&team_name_3, &source_3);
141-
assert_eq!(
142-
directory_overrider.specific_directory_owner(),
143-
Some((&team_name_longest, &source_longest))
144-
);
145-
}
146-
14757
fn assert_owner_for(glob: &str, relative_path: &str, expect_match: bool) {
14858
let source = "directory_mapper (\"packs/bam\")".to_string();
14959
let team_name = "team1".to_string();

src/ownership/validator.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ use tracing::debug;
1313
use tracing::instrument;
1414

1515
use super::file_generator::FileGenerator;
16-
use super::mapper::FileOwnerFinder;
17-
use super::mapper::Owner;
16+
use super::file_owner_finder::FileOwnerFinder;
17+
use super::file_owner_finder::Owner;
1818
use super::mapper::{Mapper, OwnerMatcher, TeamName};
1919

2020
pub struct Validator {

src/project.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,10 @@ impl Project {
267267
.expect("Could not generate relative path")
268268
}
269269

270+
pub fn get_team(&self, name: &str) -> Option<Team> {
271+
self.team_by_name().get(name).cloned()
272+
}
273+
270274
pub fn team_by_name(&self) -> HashMap<String, Team> {
271275
let mut result: HashMap<String, Team> = HashMap::new();
272276

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Payroll
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# @team Payments

0 commit comments

Comments
 (0)