diff --git a/parser/src/command.rs b/parser/src/command.rs index 3a643dd2e..7edde0f60 100644 --- a/parser/src/command.rs +++ b/parser/src/command.rs @@ -10,6 +10,7 @@ pub mod ping; pub mod prioritize; pub mod relabel; pub mod second; +pub mod shortcut; pub fn find_command_start(input: &str, bot: &str) -> Option { input.to_ascii_lowercase().find(&format!("@{}", bot)) @@ -24,6 +25,7 @@ pub enum Command<'a> { Prioritize(Result>), Second(Result>), Glacier(Result>), + Shortcut(Result>), Close(Result>), } @@ -119,6 +121,11 @@ impl<'a> Input<'a> { Command::Glacier, &original_tokenizer, )); + success.extend(parse_single_command( + shortcut::ShortcutCommand::parse, + Command::Shortcut, + &original_tokenizer, + )); success.extend(parse_single_command( close::CloseCommand::parse, Command::Close, @@ -182,6 +189,7 @@ impl<'a> Command<'a> { Command::Prioritize(r) => r.is_ok(), Command::Second(r) => r.is_ok(), Command::Glacier(r) => r.is_ok(), + Command::Shortcut(r) => r.is_ok(), Command::Close(r) => r.is_ok(), } } diff --git a/parser/src/command/shortcut.rs b/parser/src/command/shortcut.rs new file mode 100644 index 000000000..78d4a9c65 --- /dev/null +++ b/parser/src/command/shortcut.rs @@ -0,0 +1,94 @@ +//! The shortcut command parser. +//! +//! This can parse predefined shortcut input, single word commands. +//! +//! The grammar is as follows: +//! +//! ```text +//! Command: `@bot ready`, or `@bot author`. +//! ``` + +use crate::error::Error; +use crate::token::{Token, Tokenizer}; +use std::collections::HashMap; +use std::fmt; + +#[derive(PartialEq, Eq, Debug, Copy, Clone)] +pub enum ShortcutCommand { + Ready, + Author, +} + +#[derive(PartialEq, Eq, Debug)] +pub enum ParseError { + ExpectedEnd, +} + +impl std::error::Error for ParseError {} + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ParseError::ExpectedEnd => write!(f, "expected end of command"), + } + } +} + +impl ShortcutCommand { + pub fn parse<'a>(input: &mut Tokenizer<'a>) -> Result, Error<'a>> { + let mut shortcuts = HashMap::new(); + shortcuts.insert("ready", ShortcutCommand::Ready); + shortcuts.insert("author", ShortcutCommand::Author); + + let mut toks = input.clone(); + if let Some(Token::Word(word)) = toks.peek_token()? { + if !shortcuts.contains_key(word) { + return Ok(None); + } + toks.next_token()?; + if let Some(Token::Dot) | Some(Token::EndOfLine) = toks.peek_token()? { + toks.next_token()?; + *input = toks; + let command = shortcuts.get(word).unwrap(); + return Ok(Some(*command)); + } else { + return Err(toks.error(ParseError::ExpectedEnd)); + } + } + Ok(None) + } +} + +#[cfg(test)] +fn parse(input: &str) -> Result, Error<'_>> { + let mut toks = Tokenizer::new(input); + Ok(ShortcutCommand::parse(&mut toks)?) +} + +#[test] +fn test_1() { + assert_eq!(parse("ready."), Ok(Some(ShortcutCommand::Ready)),); +} + +#[test] +fn test_2() { + assert_eq!(parse("ready"), Ok(Some(ShortcutCommand::Ready)),); +} + +#[test] +fn test_3() { + assert_eq!(parse("author"), Ok(Some(ShortcutCommand::Author)),); +} + +#[test] +fn test_4() { + use std::error::Error; + assert_eq!( + parse("ready word") + .unwrap_err() + .source() + .unwrap() + .downcast_ref(), + Some(&ParseError::ExpectedEnd), + ); +} diff --git a/src/config.rs b/src/config.rs index 248ac546c..d261f0477 100644 --- a/src/config.rs +++ b/src/config.rs @@ -29,6 +29,7 @@ pub(crate) struct Config { pub(crate) notify_zulip: Option, pub(crate) github_releases: Option, pub(crate) review_submitted: Option, + pub(crate) shortcut: Option, } #[derive(PartialEq, Eq, Debug, serde::Deserialize)] @@ -82,6 +83,12 @@ pub(crate) struct RelabelConfig { pub(crate) allow_unauthenticated: Vec, } +#[derive(PartialEq, Eq, Debug, serde::Deserialize)] +pub(crate) struct ShortcutConfig { + #[serde(default)] + _empty: (), +} + #[derive(PartialEq, Eq, Debug, serde::Deserialize)] pub(crate) struct PrioritizeConfig { pub(crate) label: String, @@ -255,6 +262,8 @@ mod tests { release = "T-release" core = "T-core" infra = "T-infra" + + [shortcut] "#; let config = toml::from_str::(&config).unwrap(); let mut ping_teams = HashMap::new(); @@ -290,6 +299,7 @@ mod tests { nominate: Some(NominateConfig { teams: nominate_teams }), + shortcut: Some(ShortcutConfig { _empty: () }), prioritize: None, major_change: None, glacier: None, diff --git a/src/handlers.rs b/src/handlers.rs index e1100fcd2..be6002a82 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -38,6 +38,7 @@ mod prioritize; mod relabel; mod review_submitted; mod rustc_commits; +mod shortcut; pub async fn handle(ctx: &Context, event: &Event) -> Vec { let config = config::get(&ctx.github, event.repo_name()).await; @@ -240,6 +241,7 @@ command_handlers! { prioritize: Prioritize, relabel: Relabel, major_change: Second, + shortcut: Shortcut, close: Close, } diff --git a/src/handlers/shortcut.rs b/src/handlers/shortcut.rs new file mode 100644 index 000000000..4c5ef6b0b --- /dev/null +++ b/src/handlers/shortcut.rs @@ -0,0 +1,128 @@ +//! Purpose: Allow the use of single words shortcut to do specific actions on GitHub via comments. +//! +//! Parsing is done in the `parser::command::shortcut` module. + +use crate::{ + config::ShortcutConfig, + github::{Event, Label}, + handlers::Context, + interactions::{ErrorComment, PingComment}, +}; +use parser::command::shortcut::ShortcutCommand; + +pub(super) async fn handle_command( + ctx: &Context, + _config: &ShortcutConfig, + event: &Event, + input: ShortcutCommand, +) -> anyhow::Result<()> { + let issue = event.issue().unwrap(); + // NOTE: if shortcuts available to issues are created, they need to be allowed here + if !issue.is_pr() { + let msg = format!("The \"{:?}\" shortcut only works on pull requests.", input); + let cmnt = ErrorComment::new(&issue, msg); + cmnt.post(&ctx.github).await?; + return Ok(()); + } + + let mut issue_labels = issue.labels().to_owned(); + let waiting_on_review = "S-waiting-on-review"; + let waiting_on_author = "S-waiting-on-author"; + + match input { + ShortcutCommand::Ready => { + if assign_and_remove_label(&mut issue_labels, waiting_on_review, waiting_on_author) + .is_some() + { + return Ok(()); + } + issue.set_labels(&ctx.github, issue_labels).await?; + + let to_ping: Vec<_> = issue + .assignees + .iter() + .map(|user| user.login.as_str()) + .collect(); + let cmnt = PingComment::new(&issue, &to_ping); + cmnt.post(&ctx.github).await?; + } + ShortcutCommand::Author => { + if assign_and_remove_label(&mut issue_labels, waiting_on_author, waiting_on_review) + .is_some() + { + return Ok(()); + } + issue.set_labels(&ctx.github, issue_labels).await?; + + let to_ping = vec![issue.user.login.as_str()]; + let cmnt = PingComment::new(&issue, &to_ping); + cmnt.post(&ctx.github).await?; + } + } + + Ok(()) +} + +fn assign_and_remove_label( + issue_labels: &mut Vec