diff --git a/parser/src/command.rs b/parser/src/command.rs index fa0e23940..2072276e0 100644 --- a/parser/src/command.rs +++ b/parser/src/command.rs @@ -4,6 +4,7 @@ use crate::token::{Token, Tokenizer}; pub mod assign; pub mod relabel; +pub mod triage; pub fn find_commmand_start(input: &str, bot: &str) -> Option<usize> { input.find(&format!("@{}", bot)) @@ -13,6 +14,7 @@ pub fn find_commmand_start(input: &str, bot: &str) -> Option<usize> { pub enum Command<'a> { Relabel(Result<relabel::RelabelCommand, Error<'a>>), Assign(Result<assign::AssignCommand, Error<'a>>), + Triage(Result<triage::TriageCommand, Error<'a>>), None, } @@ -50,33 +52,45 @@ impl<'a> Input<'a> { let original_tokenizer = tok.clone(); + fn attempt_parse<'a, F, E, T>( + original: &Tokenizer<'a>, + success: &mut Vec<(Tokenizer<'a>, Command<'a>)>, + cmd: F, + transform: E, + ) where + F: Fn(&mut Tokenizer<'a>) -> Result<Option<T>, Error<'a>>, + E: Fn(Result<T, Error<'a>>) -> Command<'a>, { - let mut tok = original_tokenizer.clone(); - let res = relabel::RelabelCommand::parse(&mut tok); + let mut tok = original.clone(); + let res = cmd(&mut tok); match res { Ok(None) => {} Ok(Some(cmd)) => { - success.push((tok, Command::Relabel(Ok(cmd)))); + success.push((tok, transform(Ok(cmd)))); } Err(err) => { - success.push((tok, Command::Relabel(Err(err)))); - } - } - } - - { - let mut tok = original_tokenizer.clone(); - let res = assign::AssignCommand::parse(&mut tok); - match res { - Ok(None) => {} - Ok(Some(cmd)) => { - success.push((tok, Command::Assign(Ok(cmd)))); - } - Err(err) => { - success.push((tok, Command::Assign(Err(err)))); + success.push((tok, transform(Err(err)))); } } } + attempt_parse( + &original_tokenizer, + &mut success, + relabel::RelabelCommand::parse, + Command::Relabel, + ); + attempt_parse( + &original_tokenizer, + &mut success, + assign::AssignCommand::parse, + Command::Assign, + ); + attempt_parse( + &original_tokenizer, + &mut success, + triage::TriageCommand::parse, + Command::Triage, + ); if success.len() > 1 { panic!( @@ -112,6 +126,7 @@ impl<'a> Command<'a> { match self { Command::Relabel(r) => r.is_ok(), Command::Assign(r) => r.is_ok(), + Command::Triage(r) => r.is_ok(), Command::None => true, } } diff --git a/parser/src/command/triage.rs b/parser/src/command/triage.rs new file mode 100644 index 000000000..d0175e9e9 --- /dev/null +++ b/parser/src/command/triage.rs @@ -0,0 +1,77 @@ +//! The triage command parser. +//! +//! Gives the priority to be changed to. +//! +//! The grammar is as follows: +//! +//! ```text +//! Command: `@bot triage {high,medium,low,P-high,P-medium,P-low}` +//! ``` + +use crate::error::Error; +use crate::token::{Token, Tokenizer}; +use std::fmt; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Priority { + High, + Low, + Medium, +} + +#[derive(PartialEq, Eq, Debug)] +pub struct TriageCommand { + pub priority: Priority, +} + +#[derive(PartialEq, Eq, Debug)] +pub enum ParseError { + ExpectedPriority, + ExpectedEnd, +} + +impl std::error::Error for ParseError {} + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ParseError::ExpectedPriority => write!(f, "expected priority (high, medium, low)"), + ParseError::ExpectedEnd => write!(f, "expected end of command"), + } + } +} + +impl TriageCommand { + pub fn parse<'a>(input: &mut Tokenizer<'a>) -> Result<Option<Self>, Error<'a>> { + let mut toks = input.clone(); + if let Some(Token::Word("triage")) = toks.peek_token()? { + toks.next_token()?; + let priority = match toks.peek_token()? { + Some(Token::Word("high")) | Some(Token::Word("P-high")) => { + toks.next_token()?; + Priority::High + } + Some(Token::Word("medium")) | Some(Token::Word("P-medium")) => { + toks.next_token()?; + Priority::Medium + } + Some(Token::Word("low")) | Some(Token::Word("P-low")) => { + toks.next_token()?; + Priority::Low + } + _ => { + return Err(toks.error(ParseError::ExpectedPriority)); + } + }; + if let Some(Token::Dot) | Some(Token::EndOfLine) = toks.peek_token()? { + toks.next_token()?; + *input = toks; + return Ok(Some(TriageCommand { priority: priority })); + } else { + return Err(toks.error(ParseError::ExpectedEnd)); + } + } else { + return Ok(None); + } + } +} diff --git a/src/config.rs b/src/config.rs index a60830419..c0322b2a4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -16,6 +16,7 @@ lazy_static::lazy_static! { pub(crate) struct Config { pub(crate) relabel: Option<RelabelConfig>, pub(crate) assign: Option<AssignConfig>, + pub(crate) triage: Option<TriageConfig>, } #[derive(serde::Deserialize)] @@ -31,6 +32,14 @@ pub(crate) struct RelabelConfig { pub(crate) allow_unauthenticated: Vec<String>, } +#[derive(serde::Deserialize)] +pub(crate) struct TriageConfig { + pub(crate) remove: Vec<String>, + pub(crate) high: String, + pub(crate) medium: String, + pub(crate) low: String, +} + pub(crate) fn get(gh: &GithubClient, repo: &str) -> Result<Arc<Config>, Error> { if let Some(config) = get_cached_config(repo) { Ok(config) diff --git a/src/github.rs b/src/github.rs index 38b57c62c..650d8ad83 100644 --- a/src/github.rs +++ b/src/github.rs @@ -31,7 +31,7 @@ impl User { } } -#[derive(Debug, Clone, serde::Deserialize)] +#[derive(Debug, Clone, serde::Deserialize, PartialEq, Eq)] pub struct Label { pub name: String, } diff --git a/src/handlers.rs b/src/handlers.rs index f0aa30e48..114eb22e0 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -27,6 +27,7 @@ macro_rules! handlers { handlers! { assign = assign::AssignmentHandler, relabel = relabel::RelabelHandler, + triage = triage::TriageHandler, //tracking_issue = tracking_issue::TrackingIssueHandler, } diff --git a/src/handlers/triage.rs b/src/handlers/triage.rs new file mode 100644 index 000000000..3a3f2d7e9 --- /dev/null +++ b/src/handlers/triage.rs @@ -0,0 +1,82 @@ +//! Triage command +//! +//! Removes I-nominated tag and adds priority label. + +use crate::{ + config::TriageConfig, + github::{self, Event}, + handlers::{Context, Handler}, +}; +use failure::Error; +use parser::command::triage::{Priority, TriageCommand}; +use parser::command::{Command, Input}; + +pub(super) struct TriageHandler; + +impl Handler for TriageHandler { + type Input = TriageCommand; + type Config = TriageConfig; + + fn parse_input(&self, ctx: &Context, event: &Event) -> Result<Option<Self::Input>, Error> { + #[allow(irrefutable_let_patterns)] + let event = if let Event::IssueComment(e) = event { + e + } else { + // not interested in other events + return Ok(None); + }; + + let mut input = Input::new(&event.comment.body, &ctx.username); + match input.parse_command() { + Command::Triage(Ok(command)) => Ok(Some(command)), + Command::Triage(Err(err)) => { + failure::bail!( + "Parsing triage command in [comment]({}) failed: {}", + event.comment.html_url, + err + ); + } + _ => Ok(None), + } + } + + fn handle_input( + &self, + ctx: &Context, + config: &TriageConfig, + event: &Event, + cmd: TriageCommand, + ) -> Result<(), Error> { + #[allow(irrefutable_let_patterns)] + let event = if let Event::IssueComment(e) = event { + e + } else { + // not interested in other events + return Ok(()); + }; + + let is_team_member = + if let Err(_) | Ok(false) = event.comment.user.is_team_member(&ctx.github) { + false + } else { + true + }; + if !is_team_member { + failure::bail!("Cannot use triage command as non-team-member"); + } + + let mut labels = event.issue.labels().to_owned(); + let add = match cmd.priority { + Priority::High => &config.high, + Priority::Medium => &config.medium, + Priority::Low => &config.low, + }; + labels.push(github::Label { name: add.clone() }); + labels.retain(|label| !config.remove.contains(&label.name)); + if &labels[..] != event.issue.labels() { + event.issue.set_labels(&ctx.github, labels)?; + } + + Ok(()) + } +}