瀏覽代碼

Merge pull request #1381 from Llandy3d/ready_alias

implement the shortcut handler
Niko Matsakis 3 年之前
父節點
當前提交
ef61cb1f30
共有 6 個文件被更改,包括 261 次插入0 次删除
  1. 8 0
      parser/src/command.rs
  2. 94 0
      parser/src/command/shortcut.rs
  3. 10 0
      src/config.rs
  4. 2 0
      src/handlers.rs
  5. 128 0
      src/handlers/shortcut.rs
  6. 19 0
      src/interactions.rs

+ 8 - 0
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<usize> {
     input.to_ascii_lowercase().find(&format!("@{}", bot))
@@ -24,6 +25,7 @@ pub enum Command<'a> {
     Prioritize(Result<prioritize::PrioritizeCommand, Error<'a>>),
     Second(Result<second::SecondCommand, Error<'a>>),
     Glacier(Result<glacier::GlacierCommand, Error<'a>>),
+    Shortcut(Result<shortcut::ShortcutCommand, Error<'a>>),
     Close(Result<close::CloseCommand, Error<'a>>),
 }
 
@@ -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(),
         }
     }

+ 94 - 0
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<Option<Self>, 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<Option<ShortcutCommand>, 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),
+    );
+}

+ 10 - 0
src/config.rs

@@ -29,6 +29,7 @@ pub(crate) struct Config {
     pub(crate) notify_zulip: Option<NotifyZulipConfig>,
     pub(crate) github_releases: Option<GitHubReleasesConfig>,
     pub(crate) review_submitted: Option<ReviewSubmittedConfig>,
+    pub(crate) shortcut: Option<ShortcutConfig>,
 }
 
 #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
@@ -82,6 +83,12 @@ pub(crate) struct RelabelConfig {
     pub(crate) allow_unauthenticated: Vec<String>,
 }
 
+#[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>(&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,

+ 2 - 0
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<HandlerError> {
     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,
 }
 

+ 128 - 0
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<Label>,
+    assign: &str,
+    remove: &str,
+) -> Option<()> {
+    if issue_labels.iter().any(|label| label.name == assign) {
+        return Some(());
+    }
+
+    if let Some(index) = issue_labels.iter().position(|label| label.name == remove) {
+        issue_labels.swap_remove(index);
+    }
+
+    issue_labels.push(Label {
+        name: assign.into(),
+    });
+
+    None
+}
+
+#[cfg(test)]
+mod tests {
+
+    use super::{assign_and_remove_label, Label};
+    fn create_labels(names: Vec<&str>) -> Vec<Label> {
+        names
+            .into_iter()
+            .map(|name| Label { name: name.into() })
+            .collect()
+    }
+
+    #[test]
+    fn test_adds_without_labels() {
+        let expected = create_labels(vec!["assign"]);
+        let mut labels = vec![];
+        assert!(assign_and_remove_label(&mut labels, "assign", "remove").is_none());
+        assert_eq!(labels, expected);
+    }
+
+    #[test]
+    fn test_do_nothing_with_label_already_set() {
+        let expected = create_labels(vec!["assign"]);
+        let mut labels = create_labels(vec!["assign"]);
+        assert!(assign_and_remove_label(&mut labels, "assign", "remove").is_some());
+        assert_eq!(labels, expected);
+    }
+
+    #[test]
+    fn test_other_labels_untouched() {
+        let expected = create_labels(vec!["bug", "documentation", "assign"]);
+        let mut labels = create_labels(vec!["bug", "documentation"]);
+        assert!(assign_and_remove_label(&mut labels, "assign", "remove").is_none());
+        assert_eq!(labels, expected);
+    }
+
+    #[test]
+    fn test_correctly_remove_label() {
+        let expected = create_labels(vec!["bug", "documentation", "assign"]);
+        let mut labels = create_labels(vec!["bug", "documentation", "remove"]);
+        assert!(assign_and_remove_label(&mut labels, "assign", "remove").is_none());
+        assert_eq!(labels, expected);
+    }
+}

+ 19 - 0
src/interactions.rs

@@ -29,6 +29,25 @@ impl<'a> ErrorComment<'a> {
     }
 }
 
+pub struct PingComment<'a> {
+    issue: &'a Issue,
+    users: &'a [&'a str],
+}
+
+impl<'a> PingComment<'a> {
+    pub fn new(issue: &'a Issue, users: &'a [&str]) -> PingComment<'a> {
+        PingComment { issue, users }
+    }
+
+    pub async fn post(&self, client: &GithubClient) -> anyhow::Result<()> {
+        let mut body = String::new();
+        for user in self.users {
+            write!(body, "@{} ", user)?;
+        }
+        self.issue.post_comment(client, &body).await
+    }
+}
+
 pub struct EditIssueBody<'a> {
     issue: &'a Issue,
     id: &'static str,