//! Code from https://github.com/softprops/codeowners //! //! Codeowners provides interfaces for resolving owners of paths within code //! repositories using //! Github [CODEOWNERS](https://help.github.com/articles/about-codeowners/) //! files //! //! # Examples //! //! Typical use involves resolving a CODEOWNERS file, parsing it, //! then querying target paths //! //! ```no_run //! extern crate codeowners; //! use std::env; //! //! fn main() { //! if let (Some(owners_file), Some(path)) = //! (env::args().nth(1), env::args().nth(2)) { //! let owners = codeowners::from_path(owners_file); //! match owners.of(&path) { //! None => println!("{} is up for adoption", path), //! Some(owners) => { //! for owner in owners { //! println!("{}", owner); //! } //! } //! } //! } //! } //! ``` use glob::Pattern; use lazy_static::lazy_static; use regex::Regex; use std::{ fmt, fs::File, io::{BufRead, BufReader, Read}, path::Path, str::FromStr, }; /// Various types of owners /// /// Owners supports parsing from strings as well as displaying as strings /// /// # Examples /// /// ```rust /// let raw = "@org/team"; /// assert_eq!( /// raw.parse::().unwrap().to_string(), /// raw /// ); /// ``` #[derive(Debug, PartialEq, Eq)] pub enum Owner { /// Owner in the form @username Username(String), /// Owner in the form @org/Team Team(String), /// Owner in the form user@domain.com Email(String), } impl fmt::Display for Owner { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let inner = match *self { Self::Username(ref u) => u, Self::Team(ref t) => t, Self::Email(ref e) => e, }; f.write_str(inner.as_str()) } } impl FromStr for Owner { type Err = String; fn from_str(s: &str) -> Result { lazy_static! { static ref TEAM: Regex = Regex::new(r"^@\S+/\S+").unwrap(); static ref USERNAME: Regex = Regex::new(r"^@\S+").unwrap(); static ref EMAIL: Regex = Regex::new(r"^\S+@\S+").unwrap(); } if TEAM.is_match(s) { Ok(Self::Team(s.into())) } else if USERNAME.is_match(s) { Ok(Self::Username(s.into())) } else if EMAIL.is_match(s) { Ok(Self::Email(s.into())) } else { Err(String::from("not an owner")) } } } /// Mappings of owners to path patterns #[derive(Debug, PartialEq, Eq)] pub struct Owners { paths: Vec<(Pattern, Vec)>, } impl Owners { /// Resolve a list of owners matching a given path pub fn of

(&self, path: P) -> Option<&Vec> where P: AsRef, { self.paths.iter().find_map(|mapping| { let (ref pattern, ref owners) = mapping; let opts = glob::MatchOptions { case_sensitive: false, require_literal_separator: pattern.as_str().contains('/'), require_literal_leading_dot: false, }; if pattern.matches_path_with(path.as_ref(), opts) { Some(owners) } else { // this pattern is only meant to match // direct children if pattern.as_str().ends_with("/*") { return None; } // case of implied owned children // foo/bar @owner should indicate that foo/bar/baz.rs is // owned by @owner let mut p = path.as_ref(); while let Some(parent) = p.parent() { if pattern.matches_path_with(parent, opts) { return Some(owners); } p = parent; } None } }) } } /// Parse a CODEOWNERS file existing at a given path pub fn from_path

(path: P) -> Owners where P: AsRef, { from_reader(File::open(path).unwrap()) } /// Parse a CODEOWNERS file from some readable source /// This format is defined in /// [Github's documentation](https://help.github.com/articles/about-codeowners/) /// The syntax is uses gitgnore /// [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(read: R) -> Owners where R: Read, { let mut paths = BufReader::new(read) .lines() .map_while(Result::ok) .filter(|line| !line.is_empty() && !line.starts_with('#')) .fold(Vec::new(), |mut paths, line| { let mut elements = line.split_whitespace(); if let Some(path) = elements.next() { let owners = elements.fold(Vec::new(), |mut result, owner| { if let Ok(owner) = owner.parse() { result.push(owner); } result }); paths.push((pattern(path), owners)); } paths }); // last match takes precedence paths.reverse(); Owners { paths } } fn pattern(path: &str) -> Pattern { // if pattern starts with anchor or explicit wild card, it should // match any prefix let prefixed = if path.starts_with('*') || path.starts_with('/') { path.to_owned() } else { format!("**/{path}") }; // if pattern starts with anchor it should only match paths // relative to root let mut normalized = prefixed.trim_start_matches('/').to_string(); // if pattern ends with /, it should match children of that directory if normalized.ends_with('/') { normalized.push_str("**"); } Pattern::new(&normalized).unwrap() } #[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 "; #[test] fn owner_parses() { assert!("@user".parse() == Ok(Owner::Username("@user".into()))); assert!("@org/team".parse() == Ok(Owner::Team("@org/team".into()))); assert!("user@domain.com".parse() == Ok(Owner::Email("user@domain.com".into()))); assert!("bogus".parse::() == Err("not an owner".into())); } #[test] fn owner_displays() { assert!(Owner::Username("@user".into()).to_string() == "@user"); assert!(Owner::Team("@org/team".into()).to_string() == "@org/team"); assert!(Owner::Email("user@domain.com".into()).to_string() == "user@domain.com"); } #[test] fn from_reader_parses() { let owners = from_reader(EXAMPLE.as_bytes()); assert_eq!( owners, Owners { paths: vec![ (Pattern::new("docs/**").unwrap(), vec![Owner::Username("@doctocat".into())]), (Pattern::new("**/apps/**").unwrap(), vec![Owner::Username("@octocat".into())]), ( Pattern::new("**/docs/*").unwrap(), vec![Owner::Email("docs@example.com".into())] ), ( Pattern::new("build/logs/**").unwrap(), vec![Owner::Username("@doctocat".into())] ), (Pattern::new("*.go").unwrap(), vec![Owner::Email("docs@example.com".into())]), (Pattern::new("*.js").unwrap(), vec![Owner::Username("@js-owner".into())]), ( Pattern::new("*").unwrap(), vec![ Owner::Username("@global-owner1".into()), Owner::Username("@global-owner2".into()), ] ), ], } ); } #[test] fn owners_owns_wildcard() { let owners = from_reader(EXAMPLE.as_bytes()); assert_eq!( owners.of("foo.txt"), Some(&vec![ Owner::Username("@global-owner1".into()), Owner::Username("@global-owner2".into()), ]) ); assert_eq!( owners.of("foo/bar.txt"), Some(&vec![ Owner::Username("@global-owner1".into()), Owner::Username("@global-owner2".into()), ]) ); } #[test] fn owners_owns_js_extension() { let owners = from_reader(EXAMPLE.as_bytes()); assert_eq!(owners.of("foo.js"), Some(&vec![Owner::Username("@js-owner".into())])); assert_eq!(owners.of("foo/bar.js"), Some(&vec![Owner::Username("@js-owner".into())])); } #[test] fn owners_owns_go_extension() { let owners = from_reader(EXAMPLE.as_bytes()); assert_eq!(owners.of("foo.go"), Some(&vec![Owner::Email("docs@example.com".into())])); assert_eq!(owners.of("foo/bar.go"), Some(&vec![Owner::Email("docs@example.com".into())])); } #[test] fn owners_owns_anchored_build_logs() { let owners = from_reader(EXAMPLE.as_bytes()); // relative to root assert_eq!( owners.of("build/logs/foo.go"), Some(&vec![Owner::Username("@doctocat".into())]) ); assert_eq!( owners.of("build/logs/foo/bar.go"), Some(&vec![Owner::Username("@doctocat".into())]) ); // not relative to root assert_eq!( owners.of("foo/build/logs/foo.go"), Some(&vec![Owner::Email("docs@example.com".into())]) ); } #[test] fn owners_owns_unanchored_docs() { let owners = from_reader(EXAMPLE.as_bytes()); // docs anywhere assert_eq!( owners.of("foo/docs/foo.js"), Some(&vec![Owner::Email("docs@example.com".into())]) ); assert_eq!( owners.of("foo/bar/docs/foo.js"), Some(&vec![Owner::Email("docs@example.com".into())]) ); // but not nested assert_eq!( owners.of("foo/bar/docs/foo/foo.js"), Some(&vec![Owner::Username("@js-owner".into())]) ); } #[test] fn owners_owns_unanchored_apps() { let owners = from_reader(EXAMPLE.as_bytes()); assert_eq!(owners.of("foo/apps/foo.js"), Some(&vec![Owner::Username("@octocat".into())])); } #[test] fn owners_owns_anchored_docs() { let owners = from_reader(EXAMPLE.as_bytes()); // relative to root assert_eq!(owners.of("docs/foo.js"), Some(&vec![Owner::Username("@doctocat".into())])); } #[test] fn implied_children_owners() { #[allow(clippy::string_lit_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())])); } }