Skip to content

Commit 5dae937

Browse files
authored
for-file --fast fixes (#57)
* create codeowners parent dir * for-file absolute path
1 parent 77a0b73 commit 5dae937

File tree

7 files changed

+241
-37
lines changed

7 files changed

+241
-37
lines changed

rust-toolchain.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
[toolchain]
2-
channel = "1.85.0"
2+
channel = "1.86.0"
33
components = ["clippy", "rustfmt"]
44
targets = ["x86_64-apple-darwin", "aarch64-apple-darwin", "x86_64-unknown-linux-gnu"]

src/ownership.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,8 @@ impl Ownership {
125125
}
126126

127127
#[instrument(level = "debug", skip_all)]
128-
pub fn for_file(&self, file_path: &str) -> Result<Vec<FileOwner>, ValidatorErrors> {
129-
info!("getting file ownership for {}", file_path);
128+
pub fn for_file(&self, file_path: &Path) -> Result<Vec<FileOwner>, ValidatorErrors> {
129+
info!("getting file ownership for {}", file_path.display());
130130
let owner_matchers: Vec<OwnerMatcher> = self.mappers().iter().flat_map(|mapper| mapper.owner_matchers()).collect();
131131
let file_owner_finder = FileOwnerFinder {
132132
owner_matchers: &owner_matchers,
@@ -186,7 +186,7 @@ mod tests {
186186
#[test]
187187
fn test_for_file_owner() -> Result<(), Box<dyn Error>> {
188188
let ownership = build_ownership_with_all_mappers()?;
189-
let file_owners = ownership.for_file("app/consumers/directory_owned.rb").unwrap();
189+
let file_owners = ownership.for_file(Path::new("app/consumers/directory_owned.rb")).unwrap();
190190
assert_eq!(file_owners.len(), 1);
191191
assert_eq!(file_owners[0].team.name, "Bar");
192192
assert_eq!(file_owners[0].team_config_file_path, "config/teams/bar.yml");
@@ -196,7 +196,7 @@ mod tests {
196196
#[test]
197197
fn test_for_file_no_owner() -> Result<(), Box<dyn Error>> {
198198
let ownership = build_ownership_with_all_mappers()?;
199-
let file_owners = ownership.for_file("app/madeup/foo.rb").unwrap();
199+
let file_owners = ownership.for_file(Path::new("app/madeup/foo.rb")).unwrap();
200200
assert_eq!(file_owners.len(), 0);
201201
Ok(())
202202
}

src/ownership/file_generator.rs

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,23 +43,25 @@ impl FileGenerator {
4343

4444
fn to_sorted_lines(entries: &[Entry]) -> Vec<String> {
4545
let mut lines: Vec<String> = entries.iter().map(|entry| entry.to_row()).collect();
46-
lines.sort_by(|a, b| {
47-
if let Some((prefix, _)) = a.split_once("**") {
48-
if b.starts_with(prefix) {
49-
return Ordering::Less;
50-
}
51-
}
52-
if let Some((prefix, _)) = b.split_once("**") {
53-
if a.starts_with(prefix) {
54-
return Ordering::Greater;
55-
}
56-
}
57-
a.cmp(b)
58-
});
46+
lines.sort_by(compare_lines);
5947
lines
6048
}
6149
}
6250

51+
pub fn compare_lines(a: &String, b: &String) -> Ordering {
52+
if let Some((prefix, _)) = a.split_once("**") {
53+
if b.starts_with(prefix) {
54+
return Ordering::Less;
55+
}
56+
}
57+
if let Some((prefix, _)) = b.split_once("**") {
58+
if a.starts_with(prefix) {
59+
return Ordering::Greater;
60+
}
61+
}
62+
a.cmp(b)
63+
}
64+
6365
#[cfg(test)]
6466
mod tests {
6567
use super::*;

src/ownership/parser.rs

Lines changed: 150 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use crate::{
44
};
55
use fast_glob::glob_match;
66
use memoize::memoize;
7+
use regex::Regex;
78
use std::{
89
collections::HashMap,
910
error::Error,
@@ -12,6 +13,8 @@ use std::{
1213
path::{Path, PathBuf},
1314
};
1415

16+
use super::file_generator::compare_lines;
17+
1518
pub struct Parser {
1619
pub project_root: PathBuf,
1720
pub codeowners_file_path: PathBuf,
@@ -54,7 +57,6 @@ impl Parser {
5457

5558
#[memoize]
5659
fn teams_by_github_team_name(team_file_glob: Vec<String>) -> HashMap<String, Team> {
57-
dbg!("in teams_by_github_team_name");
5860
let mut teams = HashMap::new();
5961
for glob in team_file_glob {
6062
let paths = glob::glob(&glob).expect("Failed to read glob pattern").filter_map(Result::ok);
@@ -76,8 +78,6 @@ fn teams_by_github_team_name(team_file_glob: Vec<String>) -> HashMap<String, Tea
7678

7779
#[memoize]
7880
fn build_codeowners_lines_in_priority(codeowners_file_path: String) -> Vec<String> {
79-
dbg!("in build_codeowners_lines_in_priority");
80-
dbg!(&codeowners_file_path);
8181
let codeowners_file = match fs::read_to_string(codeowners_file_path) {
8282
Ok(codeowners_file) => codeowners_file,
8383
Err(e) => {
@@ -89,13 +89,67 @@ fn build_codeowners_lines_in_priority(codeowners_file_path: String) -> Vec<Strin
8989
stripped_lines_by_priority(&codeowners_file)
9090
}
9191

92+
#[derive(Debug, Clone, PartialEq, Eq)]
93+
struct Section {
94+
heading: String,
95+
lines: Vec<String>,
96+
}
97+
98+
impl Section {
99+
fn new(heading: String, lines: Vec<String>) -> Self {
100+
let mut sorted_lines = lines.clone();
101+
sorted_lines.sort_by(compare_lines);
102+
Self {
103+
heading,
104+
lines: sorted_lines,
105+
}
106+
}
107+
}
108+
109+
fn codeowner_sections(codeowners_file: &str) -> Result<Vec<Section>, Box<dyn Error>> {
110+
let un_ignore = Regex::new(r"^# \/")?;
111+
let mut iter = codeowners_file.lines().peekable();
112+
let mut sections = Vec::new();
113+
let mut current_section = None;
114+
let mut current_lines = Vec::new();
115+
116+
while let Some(line) = iter.next() {
117+
let line = un_ignore.replace(line, "/").to_string();
118+
if line.is_empty() {
119+
continue;
120+
}
121+
122+
if line.starts_with('#') {
123+
if iter
124+
.peek()
125+
.map(|next| next.starts_with('/') || next.starts_with("# /"))
126+
.unwrap_or(false)
127+
{
128+
if let Some(section_name) = current_section.take() {
129+
sections.push(Section::new(section_name, std::mem::take(&mut current_lines)));
130+
}
131+
current_section = Some(line);
132+
}
133+
} else {
134+
current_lines.push(line);
135+
}
136+
}
137+
138+
if let Some(section_name) = current_section {
139+
sections.push(Section::new(section_name, current_lines));
140+
}
141+
142+
Ok(sections)
143+
}
144+
92145
fn stripped_lines_by_priority(codeowners_file: &str) -> Vec<String> {
93-
codeowners_file
94-
.lines()
95-
.filter(|line| !line.trim().is_empty() && !line.trim().starts_with("#"))
96-
.map(|line| line.trim().to_string())
97-
.rev()
98-
.collect()
146+
let mut lines = Vec::new();
147+
let sections = codeowner_sections(codeowners_file).unwrap_or_default();
148+
for section in sections {
149+
lines.extend(section.lines);
150+
}
151+
lines.reverse();
152+
lines
99153
}
100154

101155
pub fn parse_for_team(team_name: String, codeowners_file: &str) -> Result<Vec<TeamOwnership>, Box<dyn Error>> {
@@ -136,6 +190,7 @@ pub fn parse_for_team(team_name: String, codeowners_file: &str) -> Result<Vec<Te
136190

137191
#[cfg(test)]
138192
mod tests {
193+
139194
use crate::common_test::tests::vecs_match;
140195

141196
use super::*;
@@ -336,4 +391,90 @@ mod tests {
336391
assert_eq!(stripped_lines, vec!["/another/path/to/owned @Bar", "/path/to/owned @Foo"]);
337392
Ok(())
338393
}
394+
395+
#[test]
396+
fn test_stripped_lines_by_priority_with_ignored_teams() -> Result<(), Box<dyn Error>> {
397+
let codeownership_file = indoc! {"
398+
# STOP! - DO NOT EDIT THIS FILE MANUALLY
399+
# This file was automatically generated by \"bin/codeownership validate\".
400+
#
401+
# CODEOWNERS is used for GitHub to suggest code/file owners to various GitHub
402+
# teams. This is useful when developers create Pull Requests since the
403+
# code/file owner is notified. Reference GitHub docs for more details:
404+
# https://help.github.com/en/articles/about-code-owners
405+
406+
407+
# Annotations at the top of file
408+
# /app/assets/config/manifest.js @Prefix/team-foo
409+
# /config/application.rb @Prefix/team-bar
410+
/config/i18n-tasks.yml.erb @Prefix/language-team
411+
412+
# Team-specific owned globs
413+
# /.github/workflows/pull-translations.yml @Prefix/infra
414+
# /.github/workflows/push-sources.yml @Prefix/infra
415+
# /Dockerfile @Prefix/docker-team
416+
# /components/create.rb @Prefix/component-team
417+
/.codeclimate.yml @Prefix/climate-team
418+
/.vscode/extensions/z/**/* @Prefix/zteam
419+
/bin/brakeman @Prefix/psecurity
420+
/config/brakeman.ignore @Prefix/security
421+
"};
422+
423+
// build up each sections lines
424+
// resort the lines without the '#'
425+
// re-assemble the sections
426+
// reverse sort
427+
let codeowner_sections = codeowner_sections(codeownership_file)?;
428+
assert_eq!(
429+
codeowner_sections,
430+
vec![
431+
Section {
432+
heading: "# Annotations at the top of file".to_string(),
433+
lines: vec![
434+
"/app/assets/config/manifest.js @Prefix/team-foo".to_string(),
435+
"/config/application.rb @Prefix/team-bar".to_string(),
436+
"/config/i18n-tasks.yml.erb @Prefix/language-team".to_string()
437+
]
438+
},
439+
Section {
440+
heading: "# Team-specific owned globs".to_string(),
441+
lines: vec![
442+
"/.codeclimate.yml @Prefix/climate-team".to_string(),
443+
"/.github/workflows/pull-translations.yml @Prefix/infra".to_string(),
444+
"/.github/workflows/push-sources.yml @Prefix/infra".to_string(),
445+
"/.vscode/extensions/z/**/* @Prefix/zteam".to_string(),
446+
"/Dockerfile @Prefix/docker-team".to_string(),
447+
"/bin/brakeman @Prefix/psecurity".to_string(),
448+
"/components/create.rb @Prefix/component-team".to_string(),
449+
"/config/brakeman.ignore @Prefix/security".to_string()
450+
]
451+
},
452+
]
453+
);
454+
let stripped_lines = stripped_lines_by_priority(codeownership_file);
455+
assert_eq!(
456+
stripped_lines,
457+
vec![
458+
"/config/brakeman.ignore @Prefix/security",
459+
"/components/create.rb @Prefix/component-team",
460+
"/bin/brakeman @Prefix/psecurity",
461+
"/Dockerfile @Prefix/docker-team",
462+
"/.vscode/extensions/z/**/* @Prefix/zteam",
463+
"/.github/workflows/push-sources.yml @Prefix/infra",
464+
"/.github/workflows/pull-translations.yml @Prefix/infra",
465+
"/.codeclimate.yml @Prefix/climate-team",
466+
"/config/i18n-tasks.yml.erb @Prefix/language-team",
467+
"/config/application.rb @Prefix/team-bar",
468+
"/app/assets/config/manifest.js @Prefix/team-foo"
469+
]
470+
);
471+
Ok(())
472+
}
473+
474+
#[test]
475+
fn test_unignore_regex() -> Result<(), Box<dyn Error>> {
476+
let un_ignore = Regex::new(r"^# \/")?;
477+
assert_eq!(un_ignore.replace("# /path/to/owned", "/"), "/path/to/owned");
478+
Ok(())
479+
}
339480
}

src/project_builder.rs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use error_stack::{Result, ResultExt};
77
use fast_glob::glob_match;
88
use ignore::WalkBuilder;
99
use rayon::iter::{IntoParallelIterator, ParallelIterator};
10-
use tracing::instrument;
10+
use tracing::{instrument, warn};
1111

1212
use crate::{
1313
cache::Cache,
@@ -149,10 +149,14 @@ impl<'a> ProjectBuilder<'a> {
149149
owner,
150150
});
151151
}
152-
EntryType::TeamFile(absolute_path, _relative_path) => {
153-
let team = Team::from_team_file_path(absolute_path).unwrap();
154-
team_files.push(team);
155-
}
152+
EntryType::TeamFile(absolute_path, _relative_path) => match Team::from_team_file_path(absolute_path) {
153+
Ok(team) => {
154+
team_files.push(team);
155+
}
156+
Err(e) => {
157+
warn!("Error building team from team file path: {}", e);
158+
}
159+
},
156160
EntryType::NullEntry() => {}
157161
}
158162
(project_files, pkgs, gems, codeowners, team_files)
@@ -169,7 +173,10 @@ impl<'a> ProjectBuilder<'a> {
169173
acc
170174
},
171175
);
172-
let teams_by_name = teams.iter().map(|team| (team.name.clone(), team.clone())).collect();
176+
let teams_by_name = teams
177+
.iter()
178+
.flat_map(|team| vec![(team.name.clone(), team.clone()), (team.github_team.clone(), team.clone())])
179+
.collect();
173180

174181
Ok(Project {
175182
base_path: self.base_path.to_owned(),

src/runner.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,17 @@ fn for_file_from_codeowners(run_config: &RunConfig, file_path: &str) -> RunResul
6969

7070
pub fn team_for_file_from_codeowners(run_config: &RunConfig, file_path: &str) -> Result<Option<Team>, Error> {
7171
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));
7275

7376
let parser = crate::ownership::parser::Parser {
7477
project_root: run_config.project_root.clone(),
7578
codeowners_file_path: run_config.codeowners_file_path.clone(),
7679
team_file_globs: config.team_file_glob.clone(),
7780
};
7881
Ok(parser
79-
.team_from_file_path(Path::new(file_path))
82+
.team_from_file_path(Path::new(relative_file_path))
8083
.map_err(|e| Error::Io(e.to_string()))?)
8184
}
8285

@@ -198,7 +201,11 @@ impl Runner {
198201
}
199202

200203
pub fn generate(&self) -> RunResult {
201-
match std::fs::write(&self.run_config.codeowners_file_path, self.ownership.generate_file()) {
204+
let content = self.ownership.generate_file();
205+
if let Some(parent) = &self.run_config.codeowners_file_path.parent() {
206+
let _ = std::fs::create_dir_all(parent);
207+
}
208+
match std::fs::write(&self.run_config.codeowners_file_path, content) {
202209
Ok(_) => RunResult::default(),
203210
Err(err) => RunResult {
204211
io_errors: vec![err.to_string()],
@@ -216,7 +223,10 @@ impl Runner {
216223
}
217224

218225
pub fn for_file(&self, file_path: &str) -> RunResult {
219-
let file_owners = match self.ownership.for_file(file_path) {
226+
let relative_file_path = Path::new(file_path)
227+
.strip_prefix(&self.run_config.project_root)
228+
.unwrap_or(Path::new(file_path));
229+
let file_owners = match self.ownership.for_file(relative_file_path) {
220230
Ok(file_owners) => file_owners,
221231
Err(err) => {
222232
return RunResult {

0 commit comments

Comments
 (0)