Skip to content

Commit d419312

Browse files
authored
Unowned team globs (#58)
* DRY iter_team_globs * the behavior we want * introducing concept of subtracted globs * subtracting team globs * bumping version
1 parent 5dae937 commit d419312

File tree

14 files changed

+226
-87
lines changed

14 files changed

+226
-87
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "codeowners"
3-
version = "0.2.3"
3+
version = "0.2.4"
44
edition = "2024"
55

66
[profile.release]

src/common_test.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,13 @@ pub mod tests {
279279
})
280280
}
281281

282+
pub fn build_ownership_with_subtracted_globs_team_glob_codeowners() -> Result<Ownership, Box<dyn Error>> {
283+
ownership!(TestProjectFile {
284+
relative_path: "config/teams/baz.yml".to_owned(),
285+
content: "name: Baz\ngithub:\n team: \"@Baz\"\n members:\n - Baz member\nowned_globs:\n - \"packs/bar/**\"\nunowned_globs:\n - \"packs/bar/excluded/**\"\n".to_owned(),
286+
})
287+
}
288+
282289
pub fn build_ownership_with_team_yml_codeowners() -> Result<Ownership, Box<dyn Error>> {
283290
let temp_dir = tempdir()?;
284291

src/ownership/mapper.rs

Lines changed: 115 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -67,19 +67,56 @@ impl Source {
6767
#[derive(Debug, PartialEq)]
6868
pub enum OwnerMatcher {
6969
ExactMatches(HashMap<PathBuf, TeamName>, Source),
70-
Glob { glob: String, team_name: TeamName, source: Source },
70+
Glob {
71+
glob: String,
72+
subtracted_globs: Vec<String>,
73+
team_name: TeamName,
74+
source: Source,
75+
},
7176
}
7277

7378
impl OwnerMatcher {
79+
pub fn new_glob_with_candidate_subtracted_globs(
80+
glob: String,
81+
candidate_subtracted_globs: &[String],
82+
team_name: TeamName,
83+
source: Source,
84+
) -> Self {
85+
let subtracted_globs = candidate_subtracted_globs
86+
.iter()
87+
.filter(|candidate_subtracted_glob| {
88+
glob_match(candidate_subtracted_glob, &glob) || glob_match(&glob, candidate_subtracted_glob)
89+
})
90+
.cloned()
91+
.collect();
92+
OwnerMatcher::Glob {
93+
glob,
94+
subtracted_globs,
95+
team_name,
96+
source,
97+
}
98+
}
99+
100+
pub fn new_glob(glob: String, team_name: TeamName, source: Source) -> Self {
101+
OwnerMatcher::Glob {
102+
glob,
103+
subtracted_globs: vec![],
104+
team_name,
105+
source,
106+
}
107+
}
108+
74109
pub fn owner_for(&self, relative_path: &Path) -> (Option<&TeamName>, &Source) {
75110
match self {
76-
OwnerMatcher::Glob { glob, team_name, source } => {
77-
if glob_match(glob, relative_path.to_str().unwrap()) {
78-
(Some(team_name), source)
79-
} else {
80-
(None, source)
81-
}
82-
}
111+
OwnerMatcher::Glob {
112+
glob,
113+
subtracted_globs,
114+
team_name,
115+
source,
116+
} => relative_path
117+
.to_str()
118+
.filter(|path| glob_match(glob, path) && !subtracted_globs.iter().any(|subtracted| glob_match(subtracted, path)))
119+
.map_or((None, source), |_| (Some(team_name), source)),
83120
OwnerMatcher::ExactMatches(path_to_team, source) => (path_to_team.get(relative_path), source),
84121
}
85122
}
@@ -89,14 +126,15 @@ impl OwnerMatcher {
89126
mod tests {
90127
use super::*;
91128

92-
fn assert_owner_for(glob: &str, relative_path: &str, expect_match: bool) {
129+
fn assert_owner_for(glob: &str, subtracted_globs: &[&str], relative_path: &str, expect_match: bool) {
93130
let source = Source::Directory("packs/bam".to_string());
94131
let team_name = "team1".to_string();
95-
let owner_matcher = OwnerMatcher::Glob {
96-
glob: glob.to_string(),
97-
team_name: team_name.clone(),
98-
source: source.clone(),
99-
};
132+
let owner_matcher = OwnerMatcher::new_glob_with_candidate_subtracted_globs(
133+
glob.to_string(),
134+
&subtracted_globs.iter().map(|s| s.to_string()).collect::<Vec<String>>(),
135+
team_name.clone(),
136+
source.clone(),
137+
);
100138
let response = owner_matcher.owner_for(Path::new(relative_path));
101139
if expect_match {
102140
assert_eq!(response, (Some(&team_name), &source));
@@ -107,26 +145,50 @@ mod tests {
107145

108146
#[test]
109147
fn owner_for_without_brackets_in_glob() {
110-
assert_owner_for("packs/bam/**/**", "packs/bam/app/components/sidebar.jsx", true);
111-
assert_owner_for("packs/bam/**/**", "packs/baz/app/components/sidebar.jsx", false);
112-
assert_owner_for("packs/bam/**/**", "packs/bam/app/[components]/gadgets/sidebar.jsx", true);
113-
assert_owner_for("packs/bam/**/**", "packs/bam/app/sidebar_[component].jsx", true);
148+
assert_owner_for("packs/bam/**/**", &[], "packs/bam/app/components/sidebar.jsx", true);
149+
assert_owner_for("packs/bam/**/**", &[], "packs/baz/app/components/sidebar.jsx", false);
150+
assert_owner_for("packs/bam/**/**", &[], "packs/bam/app/[components]/gadgets/sidebar.jsx", true);
151+
assert_owner_for("packs/bam/**/**", &[], "packs/bam/app/sidebar_[component].jsx", true);
152+
assert_owner_for(
153+
"packs/bam/**/**",
154+
&["packs/bam/app/excluded/**"],
155+
"packs/bam/app/excluded/sidebar_[component].jsx",
156+
false,
157+
);
158+
}
159+
160+
#[test]
161+
fn subtracted_globs() {
162+
assert_owner_for(
163+
"packs/bam/**/**",
164+
&["packs/bam/app/excluded/**"],
165+
"packs/bam/app/excluded/some_file.rb",
166+
false,
167+
);
168+
assert_owner_for(
169+
"packs/bam/**/**",
170+
&["packs/bam/app/excluded/**"],
171+
"packs/bam/app/not_excluded/some_file.rb",
172+
true,
173+
);
114174
}
115175

116176
#[test]
117177
fn owner_for_with_brackets_in_glob() {
118178
assert_owner_for(
119179
"packs/bam/app/\\[components\\]/**/**",
180+
&[],
120181
"packs/bam/app/[components]/gadgets/sidebar.jsx",
121182
true,
122183
);
123-
assert_owner_for("packs/\\[bam\\]/**/**", "packs/[bam]/app/components/sidebar.jsx", true);
184+
assert_owner_for("packs/\\[bam\\]/**/**", &[], "packs/[bam]/app/components/sidebar.jsx", true);
124185
}
125186

126187
#[test]
127188
fn owner_for_with_multiple_brackets_in_glob() {
128189
assert_owner_for(
129190
"packs/\\[bam\\]/bar/\\[foo\\]/**/**",
191+
&[],
130192
"packs/[bam]/bar/[foo]/app/components/sidebar.jsx",
131193
true,
132194
);
@@ -150,4 +212,38 @@ mod tests {
150212
);
151213
assert_eq!(Source::TeamYml.to_string(), "Teams own their configuration files");
152214
}
215+
216+
#[test]
217+
fn test_new_glob_with_candidate_subtracted_globs() {
218+
assert_new_glob_with_candidate_subtracted_globs("packs/bam/**/**", &[], &[]);
219+
assert_new_glob_with_candidate_subtracted_globs("packs/bam/**/**", &["packs/bam/app/**/**"], &["packs/bam/app/**/**"]);
220+
assert_new_glob_with_candidate_subtracted_globs(
221+
"packs/bam/**/**",
222+
&["packs/bam/app/an/exceptional/path/it.rb"],
223+
&["packs/bam/app/an/exceptional/path/it.rb"],
224+
);
225+
assert_new_glob_with_candidate_subtracted_globs("packs/bam/**/**", &["packs/bam.rb"], &[]);
226+
assert_new_glob_with_candidate_subtracted_globs("packs/bam/**/**", &["packs/nope/app/**/**"], &[]);
227+
assert_new_glob_with_candidate_subtracted_globs("packs/**", &["packs/yep/app/**/**"], &["packs/yep/app/**/**"]);
228+
assert_new_glob_with_candidate_subtracted_globs("packs/foo.yml", &["packs/foo/**/**"], &[]);
229+
}
230+
231+
fn assert_new_glob_with_candidate_subtracted_globs(
232+
glob: &str,
233+
candidate_subtracted_globs: &[&str],
234+
expected_subtracted_globs: &[&str],
235+
) {
236+
let owner_matcher = OwnerMatcher::new_glob_with_candidate_subtracted_globs(
237+
glob.to_string(),
238+
&candidate_subtracted_globs.iter().map(|s| s.to_string()).collect::<Vec<String>>(),
239+
"team1".to_string(),
240+
Source::TeamGlob(glob.to_string()),
241+
);
242+
243+
if let OwnerMatcher::Glob { subtracted_globs, .. } = owner_matcher {
244+
assert_eq!(subtracted_globs, expected_subtracted_globs);
245+
} else {
246+
panic!("Expected a Glob matcher");
247+
}
248+
}
153249
}

src/ownership/mapper/directory_mapper.rs

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,11 @@ impl Mapper for DirectoryMapper {
4040
let mut owner_matchers = Vec::new();
4141

4242
for file in &self.project.directory_codeowner_files {
43-
owner_matchers.push(OwnerMatcher::Glob {
44-
glob: format!("{}/**/**", escape_brackets(&file.directory_root().to_string_lossy())),
45-
team_name: file.owner.to_owned(),
46-
source: Source::Directory(file.directory_root().to_string_lossy().to_string()),
47-
});
43+
owner_matchers.push(OwnerMatcher::new_glob(
44+
format!("{}/**/**", escape_brackets(&file.directory_root().to_string_lossy())),
45+
file.owner.to_owned(),
46+
Source::Directory(file.directory_root().to_string_lossy().to_string()),
47+
));
4848
}
4949

5050
owner_matchers
@@ -125,21 +125,21 @@ mod tests {
125125
vecs_match(
126126
&mapper.owner_matchers(),
127127
&vec![
128-
OwnerMatcher::Glob {
129-
glob: "app/consumers/**/**".to_owned(),
130-
team_name: "Bar".to_owned(),
131-
source: Source::Directory("app/consumers".to_string()),
132-
},
133-
OwnerMatcher::Glob {
134-
glob: "app/services/**/**".to_owned(),
135-
team_name: "Foo".to_owned(),
136-
source: Source::Directory("app/services".to_owned()),
137-
},
138-
OwnerMatcher::Glob {
139-
glob: "app/services/exciting/**/**".to_owned(),
140-
team_name: "Bar".to_owned(),
141-
source: Source::Directory("app/services/exciting".to_owned()),
142-
},
128+
OwnerMatcher::new_glob(
129+
"app/consumers/**/**".to_owned(),
130+
"Bar".to_owned(),
131+
Source::Directory("app/consumers".to_string()),
132+
),
133+
OwnerMatcher::new_glob(
134+
"app/services/**/**".to_owned(),
135+
"Foo".to_owned(),
136+
Source::Directory("app/services".to_owned()),
137+
),
138+
OwnerMatcher::new_glob(
139+
"app/services/exciting/**/**".to_owned(),
140+
"Bar".to_owned(),
141+
Source::Directory("app/services/exciting".to_owned()),
142+
),
143143
],
144144
);
145145
Ok(())
@@ -152,16 +152,16 @@ mod tests {
152152
vecs_match(
153153
&mapper.owner_matchers(),
154154
&vec![
155-
OwnerMatcher::Glob {
156-
glob: "app/\\[consumers\\]/**/**".to_string(),
157-
team_name: "Bar".to_string(),
158-
source: Source::Directory("app/[consumers]".to_string()),
159-
},
160-
OwnerMatcher::Glob {
161-
glob: "app/\\[consumers\\]/deep/nesting/\\[nestdir\\]/**/**".to_string(),
162-
team_name: "Foo".to_string(),
163-
source: Source::Directory("app/[consumers]/deep/nesting/[nestdir]".to_string()),
164-
},
155+
OwnerMatcher::new_glob(
156+
"app/\\[consumers\\]/**/**".to_string(),
157+
"Bar".to_string(),
158+
Source::Directory("app/[consumers]".to_string()),
159+
),
160+
OwnerMatcher::new_glob(
161+
"app/\\[consumers\\]/deep/nesting/\\[nestdir\\]/**/**".to_string(),
162+
"Foo".to_string(),
163+
Source::Directory("app/[consumers]/deep/nesting/[nestdir]".to_string()),
164+
),
165165
],
166166
);
167167
Ok(())

src/ownership/mapper/package_mapper.rs

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,11 @@ impl PackageMapper {
101101
let team = team_by_name.get(&package.owner);
102102

103103
if let Some(team) = team {
104-
owner_matchers.push(OwnerMatcher::Glob {
105-
glob: format!("{}/**/**", package_root),
106-
team_name: team.name.to_owned(),
107-
source: Source::Package(package.path.to_string_lossy().to_string(), format!("{}/**/**", package_root)),
108-
});
104+
owner_matchers.push(OwnerMatcher::new_glob(
105+
format!("{}/**/**", package_root),
106+
team.name.to_owned(),
107+
Source::Package(package.path.to_string_lossy().to_string(), format!("{}/**/**", package_root)),
108+
));
109109
}
110110
}
111111

@@ -198,16 +198,16 @@ mod tests {
198198
vecs_match(
199199
&mapper.owner_matchers(&PackageType::Ruby),
200200
&vec![
201-
OwnerMatcher::Glob {
202-
glob: "packs/bam/**/**".to_owned(),
203-
team_name: "Bam".to_owned(),
204-
source: Source::Package("packs/bam/package.yml".to_owned(), "packs/bam/**/**".to_owned()),
205-
},
206-
OwnerMatcher::Glob {
207-
glob: "packs/foo/**/**".to_owned(),
208-
team_name: "Baz".to_owned(),
209-
source: Source::Package("packs/foo/package.yml".to_owned(), "packs/foo/**/**".to_owned()),
210-
},
201+
OwnerMatcher::new_glob(
202+
"packs/bam/**/**".to_owned(),
203+
"Bam".to_owned(),
204+
Source::Package("packs/bam/package.yml".to_owned(), "packs/bam/**/**".to_owned()),
205+
),
206+
OwnerMatcher::new_glob(
207+
"packs/foo/**/**".to_owned(),
208+
"Baz".to_owned(),
209+
Source::Package("packs/foo/package.yml".to_owned(), "packs/foo/**/**".to_owned()),
210+
),
211211
],
212212
);
213213
Ok(())

src/ownership/mapper/team_gem_mapper.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,11 @@ impl Mapper for TeamGemMapper {
4646
let vendored_gem = vendored_gem_by_name.get(owned_gem);
4747

4848
if let Some(vendored_gem) = vendored_gem {
49-
owner_matchers.push(OwnerMatcher::Glob {
50-
glob: format!("{}/**/*", self.project.relative_path(&vendored_gem.path).to_string_lossy()),
51-
team_name: team.name.clone(),
52-
source: Source::TeamGem,
53-
});
49+
owner_matchers.push(OwnerMatcher::new_glob(
50+
format!("{}/**/*", self.project.relative_path(&vendored_gem.path).to_string_lossy()),
51+
team.name.clone(),
52+
Source::TeamGem,
53+
));
5454
}
5555
}
5656
}
@@ -92,11 +92,11 @@ mod tests {
9292
let mapper = TeamGemMapper::build(ownership.project.clone());
9393
vecs_match(
9494
&mapper.owner_matchers(),
95-
&vec![OwnerMatcher::Glob {
96-
glob: "gems/globbing/**/*".to_owned(),
97-
team_name: "Bam".to_owned(),
98-
source: Source::TeamGem,
99-
}],
95+
&vec![OwnerMatcher::new_glob(
96+
"gems/globbing/**/*".to_owned(),
97+
"Bam".to_owned(),
98+
Source::TeamGem,
99+
)],
100100
);
101101
Ok(())
102102
}

0 commit comments

Comments
 (0)