Skip to content

Commit 405cb6c

Browse files
committed
for-file absolute path
1 parent 2b2130a commit 405cb6c

File tree

6 files changed

+235
-32
lines changed

6 files changed

+235
-32
lines changed

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 & 6 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,
@@ -86,13 +89,67 @@ fn build_codeowners_lines_in_priority(codeowners_file_path: String) -> Vec<Strin
8689
stripped_lines_by_priority(&codeowners_file)
8790
}
8891

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+
89145
fn stripped_lines_by_priority(codeowners_file: &str) -> Vec<String> {
90-
codeowners_file
91-
.lines()
92-
.filter(|line| !line.trim().is_empty() && !line.trim().starts_with("#"))
93-
.map(|line| line.trim().to_string())
94-
.rev()
95-
.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
96153
}
97154

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

134191
#[cfg(test)]
135192
mod tests {
193+
136194
use crate::common_test::tests::vecs_match;
137195

138196
use super::*;
@@ -333,4 +391,90 @@ mod tests {
333391
assert_eq!(stripped_lines, vec!["/another/path/to/owned @Bar", "/path/to/owned @Foo"]);
334392
Ok(())
335393
}
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+
}
336480
}

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: 8 additions & 2 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

@@ -220,7 +223,10 @@ impl Runner {
220223
}
221224

222225
pub fn for_file(&self, file_path: &str) -> RunResult {
223-
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) {
224230
Ok(file_owners) => file_owners,
225231
Err(err) => {
226232
return RunResult {

tests/valid_project_test.rs

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use assert_cmd::prelude::*;
22
use indoc::indoc;
33
use predicates::prelude::predicate;
4-
use std::{error::Error, path::Path, process::Command};
4+
use std::{error::Error, fs, path::Path, process::Command};
55

66
#[test]
77
fn test_validate() -> Result<(), Box<dyn Error>> {
@@ -56,6 +56,29 @@ fn test_for_file() -> Result<(), Box<dyn Error>> {
5656
Ok(())
5757
}
5858

59+
#[test]
60+
fn test_for_file_full_path() -> Result<(), Box<dyn Error>> {
61+
let project_root = Path::new("tests/fixtures/valid_project");
62+
let for_file_absolute_path = fs::canonicalize(project_root.join("ruby/app/models/payroll.rb"))?;
63+
64+
Command::cargo_bin("codeowners")?
65+
.arg("--project-root")
66+
.arg(project_root)
67+
.arg("--no-cache")
68+
.arg("for-file")
69+
.arg(&for_file_absolute_path.to_str().unwrap())
70+
.assert()
71+
.success()
72+
.stdout(predicate::eq(indoc! {"
73+
Team: Payroll
74+
Github Team: @PayrollTeam
75+
Team YML: config/teams/payroll.yml
76+
Description:
77+
- Owner annotation at the top of the file
78+
"}));
79+
Ok(())
80+
}
81+
5982
#[test]
6083
fn test_fast_for_file() -> Result<(), Box<dyn Error>> {
6184
Command::cargo_bin("codeowners")?
@@ -74,6 +97,27 @@ fn test_fast_for_file() -> Result<(), Box<dyn Error>> {
7497
Ok(())
7598
}
7699

100+
#[test]
101+
fn test_fast_for_file_full_path() -> Result<(), Box<dyn Error>> {
102+
let project_root = Path::new("tests/fixtures/valid_project");
103+
let for_file_absolute_path = fs::canonicalize(project_root.join("ruby/app/models/payroll.rb"))?;
104+
105+
Command::cargo_bin("codeowners")?
106+
.arg("--project-root")
107+
.arg(project_root)
108+
.arg("--no-cache")
109+
.arg("for-file")
110+
.arg("--fast")
111+
.arg(&for_file_absolute_path.to_str().unwrap())
112+
.assert()
113+
.success()
114+
.stdout(predicate::eq(indoc! {"
115+
Team: Payroll
116+
Team YML: config/teams/payroll.yml
117+
"}));
118+
Ok(())
119+
}
120+
77121
#[test]
78122
fn test_for_file_same_team_multiple_ownerships() -> Result<(), Box<dyn Error>> {
79123
Command::cargo_bin("codeowners")?

0 commit comments

Comments
 (0)