Skip to content

Commit

Permalink
adding for-file (#29)
Browse files Browse the repository at this point in the history
  • Loading branch information
perryqh authored Nov 5, 2024
1 parent 2d877bc commit dabef4e
Show file tree
Hide file tree
Showing 11 changed files with 246 additions and 99 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Commands:
generate Generate the CODEOWNERS file and save it to '--codeowners-file-path'
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
generate-and-validate Chains both 'generate' and 'validate' commands
for-file Print the owners for a given file
help Print this message or the help of the given subcommand(s)
Options:
Expand Down
17 changes: 16 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use ownership::Ownership;
use ownership::{FileOwner, Ownership};

use crate::project::Project;
use clap::{Parser, Subcommand};
Expand All @@ -18,6 +18,8 @@ mod project;

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

Expand Down Expand Up @@ -113,6 +115,19 @@ fn cli() -> Result<(), Error> {
std::fs::write(codeowners_file_path, ownership.generate_file()).change_context(Error::Io)?;
ownership.validate().change_context(Error::ValidationFailed)?
}
Command::ForFile { name } => {
let file_owners = ownership.for_file(&name).change_context(Error::Io)?;
match file_owners.len() {
0 => println!("{}", FileOwner::default()),
1 => println!("{}", file_owners[0]),
_ => {
println!("Error: file is owned by multiple teams!");
for file_owner in file_owners {
println!("\n{}\n", file_owner);
}
}
}
}
}

Ok(())
Expand Down
79 changes: 74 additions & 5 deletions src/ownership.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
use mapper::TeamName;
use std::sync::Arc;
use file_owner_finder::FileOwnerFinder;
use mapper::{OwnerMatcher, TeamName};
use std::{
fmt::{self, Display},
path::Path,
sync::Arc,
};
use tracing::{info, instrument};

mod file_generator;
mod file_owner_finder;
mod mapper;
mod validator;

#[cfg(test)]
mod tests;

use crate::{ownership::mapper::DirectoryMapper, project::Project};

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

pub struct FileOwner {
pub team_name: TeamName,
pub team_config_file_path: String,
}

impl Display for FileOwner {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Team: {}\nTeam YML: {}", self.team_name, self.team_config_file_path)
}
}

impl Default for FileOwner {
fn default() -> Self {
Self {
team_name: "Unowned".to_string(),
team_config_file_path: "Unowned".to_string(),
}
}
}

#[allow(dead_code)]
#[derive(Debug, PartialEq)]
pub struct Entry {
Expand Down Expand Up @@ -62,6 +85,29 @@ impl Ownership {
validator.validate()
}

#[instrument(level = "debug", skip_all)]
pub fn for_file(&self, file_path: &str) -> Result<Vec<FileOwner>, ValidatorErrors> {
info!("getting file ownership for {}", file_path);
let owner_matchers: Vec<OwnerMatcher> = self.mappers().iter().flat_map(|mapper| mapper.owner_matchers()).collect();
let file_owner_finder = FileOwnerFinder {
owner_matchers: &owner_matchers,
};
let owners = file_owner_finder.find(Path::new(file_path));
Ok(owners
.iter()
.map(|owner| match self.project.get_team(&owner.team_name) {
Some(team) => FileOwner {
team_name: owner.team_name.clone(),
team_config_file_path: team
.path
.strip_prefix(&self.project.base_path)
.map_or_else(|_| String::new(), |p| p.to_string_lossy().to_string()),
},
None => FileOwner::default(),
})
.collect())
}

#[instrument(level = "debug", skip_all)]
pub fn generate_file(&self) -> String {
info!("generating codeowners file");
Expand All @@ -81,3 +127,26 @@ impl Ownership {
]
}
}

#[cfg(test)]
mod tests {
use crate::common_test::tests::build_ownership_with_all_mappers;

#[test]
fn test_for_file_owner() -> Result<(), Box<dyn std::error::Error>> {
let ownership = build_ownership_with_all_mappers()?;
let file_owners = ownership.for_file("app/consumers/directory_owned.rb").unwrap();
assert_eq!(file_owners.len(), 1);
assert_eq!(file_owners[0].team_name, "Bar");
assert_eq!(file_owners[0].team_config_file_path, "config/teams/bar.yml");
Ok(())
}

#[test]
fn test_for_file_no_owner() -> Result<(), Box<dyn std::error::Error>> {
let ownership = build_ownership_with_all_mappers()?;
let file_owners = ownership.for_file("app/madeup/foo.rb").unwrap();
assert_eq!(file_owners.len(), 0);
Ok(())
}
}
97 changes: 97 additions & 0 deletions src/ownership/file_owner_finder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
use std::{collections::HashMap, path::Path};

use super::mapper::{directory_mapper::is_directory_mapper_source, OwnerMatcher, Source, TeamName};

#[derive(Debug)]
pub struct Owner {
pub sources: Vec<Source>,
pub team_name: TeamName,
}

pub struct FileOwnerFinder<'a> {
pub owner_matchers: &'a [OwnerMatcher],
}

impl<'a> FileOwnerFinder<'a> {
pub fn find(&self, relative_path: &Path) -> Vec<Owner> {
let mut team_sources_map: HashMap<&TeamName, Vec<Source>> = HashMap::new();
let mut directory_overrider = DirectoryOverrider::default();

for owner_matcher in self.owner_matchers {
let (owner, source) = owner_matcher.owner_for(relative_path);

if let Some(team_name) = owner {
if is_directory_mapper_source(source) {
directory_overrider.process(team_name, source);
} else {
team_sources_map.entry(team_name).or_default().push(source.clone());
}
}
}

// Add most specific directory owner if it exists
if let Some((team_name, source)) = directory_overrider.specific_directory_owner() {
team_sources_map.entry(team_name).or_default().push(source.clone());
}

team_sources_map
.into_iter()
.map(|(team_name, sources)| Owner {
sources,
team_name: team_name.clone(),
})
.collect()
}
}

/// DirectoryOverrider is used to override the owner of a directory if a more specific directory owner is found.
#[derive(Debug, Default)]
pub struct DirectoryOverrider<'a> {
specific_directory_owner: Option<(&'a TeamName, &'a Source)>,
}

impl<'a> DirectoryOverrider<'a> {
fn process(&mut self, team_name: &'a TeamName, source: &'a Source) {
if self
.specific_directory_owner
.map_or(true, |(_, current_source)| current_source.len() < source.len())
{
self.specific_directory_owner = Some((team_name, source));
}
}

fn specific_directory_owner(&self) -> Option<(&TeamName, &Source)> {
self.specific_directory_owner
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_directory_overrider() {
let mut directory_overrider = DirectoryOverrider::default();
assert_eq!(directory_overrider.specific_directory_owner(), None);
let team_name_1 = "team1".to_string();
let source_1 = "src/**".to_string();
directory_overrider.process(&team_name_1, &source_1);
assert_eq!(directory_overrider.specific_directory_owner(), Some((&team_name_1, &source_1)));

let team_name_longest = "team2".to_string();
let source_longest = "source/subdir/**".to_string();
directory_overrider.process(&team_name_longest, &source_longest);
assert_eq!(
directory_overrider.specific_directory_owner(),
Some((&team_name_longest, &source_longest))
);

let team_name_3 = "team3".to_string();
let source_3 = "source/**".to_string();
directory_overrider.process(&team_name_3, &source_3);
assert_eq!(
directory_overrider.specific_directory_owner(),
Some((&team_name_longest, &source_longest))
);
}
}
92 changes: 1 addition & 91 deletions src/ownership/mapper.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
use directory_mapper::is_directory_mapper_source;
use glob_match::glob_match;
use std::{
collections::HashMap,
path::{Path, PathBuf},
};

mod directory_mapper;
pub(crate) mod directory_mapper;
mod package_mapper;
mod team_file_mapper;
mod team_gem_mapper;
Expand Down Expand Up @@ -51,99 +50,10 @@ impl OwnerMatcher {
}
}

#[derive(Debug)]
pub struct Owner {
pub sources: Vec<Source>,
pub team_name: TeamName,
}

pub struct FileOwnerFinder<'a> {
pub owner_matchers: &'a [OwnerMatcher],
}

impl<'a> FileOwnerFinder<'a> {
pub fn find(&self, relative_path: &Path) -> Vec<Owner> {
let mut team_sources_map: HashMap<&TeamName, Vec<Source>> = HashMap::new();
let mut directory_overrider = DirectoryOverrider::default();

for owner_matcher in self.owner_matchers {
let (owner, source) = owner_matcher.owner_for(relative_path);

if let Some(team_name) = owner {
if is_directory_mapper_source(source) {
directory_overrider.process(team_name, source);
} else {
team_sources_map.entry(team_name).or_default().push(source.clone());
}
}
}

// Add most specific directory owner if it exists
if let Some((team_name, source)) = directory_overrider.specific_directory_owner() {
team_sources_map.entry(team_name).or_default().push(source.clone());
}

team_sources_map
.into_iter()
.map(|(team_name, sources)| Owner {
sources,
team_name: team_name.clone(),
})
.collect()
}
}

/// DirectoryOverrider is used to override the owner of a directory if a more specific directory owner is found.
#[derive(Debug, Default)]
struct DirectoryOverrider<'a> {
specific_directory_owner: Option<(&'a TeamName, &'a Source)>,
}

impl<'a> DirectoryOverrider<'a> {
fn process(&mut self, team_name: &'a TeamName, source: &'a Source) {
if self
.specific_directory_owner
.map_or(true, |(_, current_source)| current_source.len() < source.len())
{
self.specific_directory_owner = Some((team_name, source));
}
}

fn specific_directory_owner(&self) -> Option<(&TeamName, &Source)> {
self.specific_directory_owner
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_directory_overrider() {
let mut directory_overrider = DirectoryOverrider::default();
assert_eq!(directory_overrider.specific_directory_owner(), None);
let team_name_1 = "team1".to_string();
let source_1 = "src/**".to_string();
directory_overrider.process(&team_name_1, &source_1);
assert_eq!(directory_overrider.specific_directory_owner(), Some((&team_name_1, &source_1)));

let team_name_longest = "team2".to_string();
let source_longest = "source/subdir/**".to_string();
directory_overrider.process(&team_name_longest, &source_longest);
assert_eq!(
directory_overrider.specific_directory_owner(),
Some((&team_name_longest, &source_longest))
);

let team_name_3 = "team3".to_string();
let source_3 = "source/**".to_string();
directory_overrider.process(&team_name_3, &source_3);
assert_eq!(
directory_overrider.specific_directory_owner(),
Some((&team_name_longest, &source_longest))
);
}

fn assert_owner_for(glob: &str, relative_path: &str, expect_match: bool) {
let source = "directory_mapper (\"packs/bam\")".to_string();
let team_name = "team1".to_string();
Expand Down
4 changes: 2 additions & 2 deletions src/ownership/validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ use tracing::debug;
use tracing::instrument;

use super::file_generator::FileGenerator;
use super::mapper::FileOwnerFinder;
use super::mapper::Owner;
use super::file_owner_finder::FileOwnerFinder;
use super::file_owner_finder::Owner;
use super::mapper::{Mapper, OwnerMatcher, TeamName};

pub struct Validator {
Expand Down
4 changes: 4 additions & 0 deletions src/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,10 @@ impl Project {
.expect("Could not generate relative path")
}

pub fn get_team(&self, name: &str) -> Option<Team> {
self.team_by_name().get(name).cloned()
}

pub fn team_by_name(&self) -> HashMap<String, Team> {
let mut result: HashMap<String, Team> = HashMap::new();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Payroll
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# @team Payments
Loading

0 comments on commit dabef4e

Please sign in to comment.