Skip to content

Commit 6c515b8

Browse files
authored
Support directory ownership (#14)
* write failing test * first attempt to make tests pass * add dependency * use pretty assertions * fix failing test * bump version * fix merge conflict * clippy
1 parent c4d0517 commit 6c515b8

File tree

10 files changed

+136
-6
lines changed

10 files changed

+136
-6
lines changed

Cargo.lock

Lines changed: 24 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "codeowners"
3-
version = "0.1.0"
3+
version = "0.1.1"
44
edition = "2021"
55

66
[profile.release]
@@ -26,3 +26,4 @@ tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
2626
assert_cmd = "2.0.10"
2727
rusty-hook = "^0.11.2"
2828
predicates = "3.0.2"
29+
pretty_assertions = "1.3.0" # Shows a more readable diff when comparing objects

src/ownership.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ mod validator;
88
#[cfg(test)]
99
mod tests;
1010

11-
use crate::project::Project;
11+
use crate::{ownership::mapper::DirectoryMapper, project::Project};
1212

1313
pub use validator::Errors as ValidatorErrors;
1414

@@ -70,6 +70,7 @@ impl Ownership {
7070
vec![
7171
Box::new(TeamFileMapper::build(self.project.clone())),
7272
Box::new(TeamGlobMapper::build(self.project.clone())),
73+
Box::new(DirectoryMapper::build(self.project.clone())),
7374
Box::new(RubyPackageMapper::build(self.project.clone())),
7475
Box::new(JavascriptPackageMapper::build(self.project.clone())),
7576
Box::new(TeamYmlMapper::build(self.project.clone())),

src/ownership/mapper.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ use std::{
44
path::{Path, PathBuf},
55
};
66

7+
mod directory_mapper;
78
mod package_mapper;
89
mod team_file_mapper;
910
mod team_gem_mapper;
1011
mod team_glob_mapper;
1112
mod team_yml_mapper;
1213

14+
pub use directory_mapper::DirectoryMapper;
1315
pub use package_mapper::JavascriptPackageMapper;
1416
pub use package_mapper::RubyPackageMapper;
1517
pub use team_file_mapper::TeamFileMapper;
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
use std::sync::Arc;
2+
3+
use super::Entry;
4+
use super::{Mapper, OwnerMatcher};
5+
use crate::project::Project;
6+
7+
pub struct DirectoryMapper {
8+
project: Arc<Project>,
9+
}
10+
11+
impl DirectoryMapper {
12+
pub fn build(project: Arc<Project>) -> Self {
13+
Self { project }
14+
}
15+
}
16+
17+
impl Mapper for DirectoryMapper {
18+
fn entries(&self) -> Vec<Entry> {
19+
let mut entries: Vec<Entry> = Vec::new();
20+
let team_by_name = self.project.team_by_name();
21+
22+
for directory_codeowner_file in &self.project.directory_codeowner_files {
23+
let dir_root = directory_codeowner_file.directory_root().to_string_lossy();
24+
let team = team_by_name.get(&directory_codeowner_file.owner);
25+
if let Some(team) = team {
26+
entries.push(Entry {
27+
path: format!("{}/**/**", dir_root),
28+
github_team: team.github_team.to_owned(),
29+
team_name: team.name.to_owned(),
30+
disabled: team.avoid_ownership,
31+
});
32+
}
33+
}
34+
35+
entries
36+
}
37+
38+
fn owner_matchers(&self) -> Vec<OwnerMatcher> {
39+
let mut owner_matchers = Vec::new();
40+
41+
for file in &self.project.directory_codeowner_files {
42+
owner_matchers.push(OwnerMatcher::Glob {
43+
glob: format!("{}/**/**", file.directory_root().to_string_lossy()),
44+
team_name: file.owner.to_owned(),
45+
source: format!("directory_mapper ({:?})", &file.directory_root()),
46+
});
47+
}
48+
49+
owner_matchers
50+
}
51+
52+
fn name(&self) -> String {
53+
"Owner in .codeowner".to_owned()
54+
}
55+
}

src/ownership/tests.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::path::Path;
33
use crate::project::{Package, PackageType, Project, ProjectFile, Team, VendoredGem};
44

55
use super::Ownership;
6+
use pretty_assertions::assert_eq;
67

78
fn build_payroll_team() -> Team {
89
Team {
@@ -48,6 +49,7 @@ fn build_project_with_annotated_file() -> Project {
4849
teams: vec![build_payroll_team()],
4950
vendored_gems: vec![],
5051
codeowners_file: "".to_owned(),
52+
directory_codeowner_files: vec![],
5153
}
5254
}
5355

@@ -70,6 +72,7 @@ fn build_project_with_team_specific_owned_globs() -> Project {
7072
teams: vec![build_payroll_team_with_owned_glob()],
7173
vendored_gems: vec![],
7274
codeowners_file: "".to_owned(),
75+
directory_codeowner_files: vec![],
7376
}
7477
}
7578

@@ -92,6 +95,7 @@ fn build_project_with_packages() -> Project {
9295
teams: vec![build_payroll_team()],
9396
vendored_gems: vec![],
9497
codeowners_file: "".to_owned(),
98+
directory_codeowner_files: vec![],
9599
}
96100
}
97101

@@ -106,6 +110,7 @@ fn build_project_with_team_owned_gems() -> Project {
106110
name: "payroll_calculator".to_owned(),
107111
}],
108112
codeowners_file: "".to_owned(),
113+
directory_codeowner_files: vec![],
109114
}
110115
}
111116

@@ -121,6 +126,8 @@ fn test_annotations_at_the_top_of_file() {
121126
"",
122127
"# Team-specific owned globs",
123128
"",
129+
"# Owner in .codeowner",
130+
"",
124131
"# Owner metadata key in package.yml",
125132
"",
126133
"# Owner metadata key in package.json",
@@ -147,6 +154,8 @@ fn test_team_specific_owned_globs() {
147154
"# Team-specific owned globs",
148155
"/packs/payroll/** @Payroll-Eng",
149156
"",
157+
"# Owner in .codeowner",
158+
"",
150159
"# Owner metadata key in package.yml",
151160
"",
152161
"# Owner metadata key in package.json",
@@ -172,6 +181,8 @@ fn test_owner_metadata_in_package() {
172181
"",
173182
"# Team-specific owned globs",
174183
"",
184+
"# Owner in .codeowner",
185+
"",
175186
"# Owner metadata key in package.yml",
176187
"/packs/payroll_package/**/** @Payroll-Eng",
177188
"",
@@ -199,6 +210,8 @@ fn test_team_owned_gems() {
199210
"",
200211
"# Team-specific owned globs",
201212
"",
213+
"# Owner in .codeowner",
214+
"",
202215
"# Owner metadata key in package.yml",
203216
"",
204217
"# Owner metadata key in package.json",

src/project.rs

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use std::{
88

99
use error_stack::{Context, Result};
1010

11-
use ignore::Walk;
11+
use ignore::WalkBuilder;
1212
use rayon::prelude::{IntoParallelIterator, ParallelIterator};
1313
use regex::Regex;
1414
use tracing::{info, instrument};
@@ -23,6 +23,7 @@ pub struct Project {
2323
pub vendored_gems: Vec<VendoredGem>,
2424
pub teams: Vec<Team>,
2525
pub codeowners_file: String,
26+
pub directory_codeowner_files: Vec<DirectoryCodeownersFile>,
2627
}
2728

2829
#[derive(Clone)]
@@ -37,7 +38,7 @@ pub struct ProjectFile {
3738
pub path: PathBuf,
3839
}
3940

40-
#[derive(Clone)]
41+
#[derive(Clone, Debug)]
4142
pub struct Team {
4243
pub path: PathBuf,
4344
pub name: String,
@@ -59,6 +60,18 @@ impl Package {
5960
}
6061
}
6162

63+
#[derive(Clone, Debug)]
64+
pub struct DirectoryCodeownersFile {
65+
pub path: PathBuf,
66+
pub owner: String,
67+
}
68+
69+
impl DirectoryCodeownersFile {
70+
pub fn directory_root(&self) -> &Path {
71+
self.path.parent().unwrap()
72+
}
73+
}
74+
6275
#[derive(PartialEq, Eq, Debug)]
6376
pub enum PackageType {
6477
Ruby,
@@ -143,8 +156,13 @@ impl Project {
143156
let mut packages: Vec<Package> = Vec::new();
144157
let mut teams: Vec<Team> = Vec::new();
145158
let mut vendored_gems: Vec<VendoredGem> = Vec::new();
159+
let mut directory_codeowner_files: Vec<DirectoryCodeownersFile> = Vec::new();
146160

147-
for entry in Walk::new(base_path) {
161+
let mut builder = WalkBuilder::new(base_path);
162+
builder.hidden(false);
163+
let walkdir = builder.build();
164+
165+
for entry in walkdir {
148166
let entry = entry.into_context(Error::Io)?;
149167

150168
let absolute_path = entry.path();
@@ -186,6 +204,17 @@ impl Project {
186204
}
187205
}
188206

207+
if file_name.eq_ignore_ascii_case(".codeowner") {
208+
let owner = std::fs::read_to_string(absolute_path).into_context(Error::Io)?;
209+
let owner = owner.trim().to_owned();
210+
211+
let relative_path = relative_path.to_owned();
212+
directory_codeowner_files.push(DirectoryCodeownersFile {
213+
path: relative_path,
214+
owner,
215+
})
216+
}
217+
189218
if matches_globs(&relative_path, &config.team_file_glob) {
190219
let file = File::open(absolute_path).into_context(Error::Io)?;
191220
let deserializer: deserializers::Team = serde_yaml::from_reader(file).into_context(Error::SerdeYaml)?;
@@ -228,6 +257,7 @@ impl Project {
228257
teams,
229258
packages,
230259
codeowners_file,
260+
directory_codeowner_files,
231261
})
232262
}
233263

tests/fixtures/valid_project/.github/CODEOWNERS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
# Team-specific owned globs
1616
/ruby/app/payments/**/* @PaymentsTeam
1717

18+
# Owner in .codeowner
19+
/ruby/app/payroll/**/** @PayrollTeam
20+
1821
# Owner metadata key in package.yml
1922
/ruby/packages/payroll_flow/**/** @PayrollTeam
2023

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+
class Payroll; end

0 commit comments

Comments
 (0)