Skip to content

capture where codeowners was loaded from as well as line numbers for … #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 44 additions & 58 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
//! then querying target paths
//!
//! ```no_run
//! extern crate codeowners;
//! use std::env;
//!
//! fn main() {
Expand Down Expand Up @@ -104,13 +103,20 @@ impl FromStr for Owner {
}
}

/// Mappings of owners to path patterns
/// Pasrsed result of a GitHub `CODEOWNERS` file
#[derive(Debug, PartialEq)]
pub struct Owners {
paths: Vec<(Pattern, Vec<Owner>)>,
location: String,
paths: Vec<(usize, Pattern, Vec<Owner>)>,
}

impl Owners {
/// Path location of where this instance was loaded from.
/// This may return `-` in cases where not loaded directly from a file
pub fn location(&self) -> &str {
self.location.as_str()
}

/// Resolve a list of owners matching a given path
pub fn of<P>(
&self,
Expand All @@ -122,7 +128,7 @@ impl Owners {
self.paths
.iter()
.filter_map(|mapping| {
let &(ref pattern, ref owners) = mapping;
let &(_, ref pattern, ref owners) = mapping;
let opts = glob::MatchOptions {
case_sensitive: false,
require_literal_separator: pattern.as_str().contains('/'),
Expand Down Expand Up @@ -193,7 +199,10 @@ pub fn from_path<P>(path: P) -> Owners
where
P: AsRef<Path>,
{
crate::from_reader(File::open(path).unwrap())
crate::from_reader(
path.as_ref().display().to_string(),
File::open(path).unwrap(),
)
}

/// Parse a CODEOWNERS file from some readable source
Expand All @@ -203,15 +212,20 @@ where
/// [patterns](https://www.kernel.org/pub/software/scm/git/docs/gitignore.html#_pattern_format)
/// followed by an identifier for an owner. More information can be found
/// [here](https://help.github.com/articles/about-codeowners/#codeowners-syntax)
pub fn from_reader<R>(read: R) -> Owners
pub fn from_reader<L, R>(
location: L,
read: R,
) -> Owners
where
L: AsRef<str>,
R: Read,
{
let mut paths = BufReader::new(read)
.lines()
.filter_map(Result::ok)
.filter(|line| !line.is_empty() && !line.starts_with('#'))
.fold(Vec::new(), |mut paths, line| {
.enumerate()
.filter(|(_, line)| !line.is_empty() && !line.starts_with('#'))
.fold(Vec::new(), |mut paths, (idx, line)| {
let mut elements = line.split_whitespace();
if let Some(path) = elements.next() {
let owners = elements.fold(Vec::new(), |mut result, owner| {
Expand All @@ -220,13 +234,15 @@ where
}
result
});
paths.push((pattern(path), owners))
let line_number = idx + 1;
paths.push((line_number, pattern(path), owners))
}
paths
});
// last match takes precedence
paths.reverse();
Owners { paths }
let location = location.as_ref().into();
Owners { location, paths }
}

fn pattern(path: &str) -> Pattern {
Expand All @@ -250,44 +266,7 @@ fn pattern(path: &str) -> Pattern {
#[cfg(test)]
mod tests {
use super::*;
const EXAMPLE: &str = r"# This is a comment.
# Each line is a file pattern followed by one or more owners.

# These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence,
# @global-owner1 and @global-owner2 will be requested for
# review when someone opens a pull request.
* @global-owner1 @global-owner2

# Order is important; the last matching pattern takes the most
# precedence. When someone opens a pull request that only
# modifies JS files, only @js-owner and not the global
# owner(s) will be requested for a review.
*.js @js-owner

# You can also use email addresses if you prefer. They'll be
# used to look up users just like we do for commit author
# emails.
*.go docs@example.com

# In this example, @doctocat owns any files in the build/logs
# directory at the root of the repository and any of its
# subdirectories.
/build/logs/ @doctocat

# The `docs/*` pattern will match files like
# `docs/getting-started.md` but not further nested files like
# `docs/build-app/troubleshooting.md`.
docs/* docs@example.com

# In this example, @octocat owns any file in an apps directory
# anywhere in your repository.
apps/ @octocat

# In this example, @doctocat owns any file in the `/docs`
# directory in the root of your repository.
/docs/ @doctocat
";
const EXAMPLE: &str = include_str!("../tests/data/CODEOWNERS");

#[test]
fn owner_parses() {
Expand All @@ -306,36 +285,44 @@ apps/ @octocat

#[test]
fn from_reader_parses() {
let owners = from_reader(EXAMPLE.as_bytes());
let owners = from_reader("-", EXAMPLE.as_bytes());
assert_eq!(
owners,
Owners {
location: "-".into(),
paths: vec![
(
37,
Pattern::new("docs/**").unwrap(),
vec![Owner::Username("@doctocat".into())]
),
(
33,
Pattern::new("**/apps/**").unwrap(),
vec![Owner::Username("@octocat".into())]
),
(
29,
Pattern::new("**/docs/*").unwrap(),
vec![Owner::Email("docs@example.com".into())]
),
(
24,
Pattern::new("build/logs/**").unwrap(),
vec![Owner::Username("@doctocat".into())]
),
(
19,
Pattern::new("*.go").unwrap(),
vec![Owner::Email("docs@example.com".into())]
),
(
14,
Pattern::new("*.js").unwrap(),
vec![Owner::Username("@js-owner".into())]
),
(
8,
Pattern::new("*").unwrap(),
vec![
Owner::Username("@global-owner1".into()),
Expand All @@ -349,7 +336,7 @@ apps/ @octocat

#[test]
fn owners_owns_wildcard() {
let owners = from_reader(EXAMPLE.as_bytes());
let owners = from_reader("-", EXAMPLE.as_bytes());
assert_eq!(
owners.of("foo.txt"),
Some(&vec![
Expand All @@ -368,7 +355,7 @@ apps/ @octocat

#[test]
fn owners_owns_js_extention() {
let owners = from_reader(EXAMPLE.as_bytes());
let owners = from_reader("-", EXAMPLE.as_bytes());
assert_eq!(
owners.of("foo.js"),
Some(&vec![Owner::Username("@js-owner".into())])
Expand All @@ -381,7 +368,7 @@ apps/ @octocat

#[test]
fn owners_owns_go_extention() {
let owners = from_reader(EXAMPLE.as_bytes());
let owners = from_reader("-", EXAMPLE.as_bytes());
assert_eq!(
owners.of("foo.go"),
Some(&vec![Owner::Email("docs@example.com".into())])
Expand All @@ -394,7 +381,7 @@ apps/ @octocat

#[test]
fn owners_owns_anchored_build_logs() {
let owners = from_reader(EXAMPLE.as_bytes());
let owners = from_reader("-", EXAMPLE.as_bytes());
// relative to root
assert_eq!(
owners.of("build/logs/foo.go"),
Expand All @@ -413,7 +400,7 @@ apps/ @octocat

#[test]
fn owners_owns_unanchored_docs() {
let owners = from_reader(EXAMPLE.as_bytes());
let owners = from_reader("-", EXAMPLE.as_bytes());
// docs anywhere
assert_eq!(
owners.of("foo/docs/foo.js"),
Expand All @@ -432,7 +419,7 @@ apps/ @octocat

#[test]
fn owners_owns_unanchored_apps() {
let owners = from_reader(EXAMPLE.as_bytes());
let owners = from_reader("-", EXAMPLE.as_bytes());
assert_eq!(
owners.of("foo/apps/foo.js"),
Some(&vec![Owner::Username("@octocat".into())])
Expand All @@ -441,7 +428,7 @@ apps/ @octocat

#[test]
fn owners_owns_anchored_docs() {
let owners = from_reader(EXAMPLE.as_bytes());
let owners = from_reader("-", EXAMPLE.as_bytes());
// relative to root
assert_eq!(
owners.of("docs/foo.js"),
Expand All @@ -451,11 +438,10 @@ apps/ @octocat

#[test]
fn implied_children_owners() {
let owners = from_reader("foo/bar @doug".as_bytes());
let owners = from_reader("-", "foo/bar @doug".as_bytes());
assert_eq!(
owners.of("foo/bar/baz.rs"),
Some(&vec![Owner::Username("@doug".into())])
)
}

}
37 changes: 37 additions & 0 deletions tests/data/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# This is a comment.
# Each line is a file pattern followed by one or more owners.

# These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence,
# @global-owner1 and @global-owner2 will be requested for
# review when someone opens a pull request.
* @global-owner1 @global-owner2

# Order is important; the last matching pattern takes the most
# precedence. When someone opens a pull request that only
# modifies JS files, only @js-owner and not the global
# owner(s) will be requested for a review.
*.js @js-owner

# You can also use email addresses if you prefer. They'll be
# used to look up users just like we do for commit author
# emails.
*.go docs@example.com

# In this example, @doctocat owns any files in the build/logs
# directory at the root of the repository and any of its
# subdirectories.
/build/logs/ @doctocat

# The `docs/*` pattern will match files like
# `docs/getting-started.md` but not further nested files like
# `docs/build-app/troubleshooting.md`.
docs/* docs@example.com

# In this example, @octocat owns any file in an apps directory
# anywhere in your repository.
apps/ @octocat

# In this example, @doctocat owns any file in the `/docs`
# directory in the root of your repository.
/docs/ @doctocat