Explorar o código

Major change issues: seconding and creation

Mark Rousskov %!s(int64=5) %!d(string=hai) anos
pai
achega
c979787c1e
Modificáronse 5 ficheiros con 172 adicións e 1 borrados
  1. 9 1
      parser/src/command.rs
  2. 15 0
      parser/src/command/second.rs
  3. 8 0
      src/config.rs
  4. 1 0
      src/handlers.rs
  5. 139 0
      src/handlers/major_change.rs

+ 9 - 1
parser/src/command.rs

@@ -5,8 +5,9 @@ use crate::token::{Token, Tokenizer};
 pub mod assign;
 pub mod nominate;
 pub mod ping;
-pub mod relabel;
 pub mod prioritize;
+pub mod relabel;
+pub mod second;
 
 pub fn find_commmand_start(input: &str, bot: &str) -> Option<usize> {
     input.find(&format!("@{}", bot))
@@ -19,6 +20,7 @@ pub enum Command<'a> {
     Ping(Result<ping::PingCommand, Error<'a>>),
     Nominate(Result<nominate::NominateCommand, Error<'a>>),
     Prioritize(Result<prioritize::PrioritizeCommand, Error<'a>>),
+    Second(Result<second::SecondCommand, Error<'a>>),
     None,
 }
 
@@ -102,6 +104,11 @@ impl<'a> Input<'a> {
             Command::Prioritize,
             &original_tokenizer,
         ));
+        success.extend(parse_single_command(
+            second::SecondCommand::parse,
+            Command::Second,
+            &original_tokenizer,
+        ));
 
         if success.len() > 1 {
             panic!(
@@ -141,6 +148,7 @@ impl<'a> Command<'a> {
             Command::Ping(r) => r.is_ok(),
             Command::Nominate(r) => r.is_ok(),
             Command::Prioritize(r) => r.is_ok(),
+            Command::Second(r) => r.is_ok(),
             Command::None => true,
         }
     }

+ 15 - 0
parser/src/command/second.rs

@@ -0,0 +1,15 @@
+use crate::error::Error;
+use crate::token::{Token, Tokenizer};
+
+#[derive(PartialEq, Eq, Debug)]
+pub struct SecondCommand;
+
+impl SecondCommand {
+    pub fn parse<'a>(input: &mut Tokenizer<'a>) -> Result<Option<Self>, Error<'a>> {
+        if let Some(Token::Word("second")) = input.peek_token()? {
+            Ok(Some(Self))
+        } else {
+            Ok(None)
+        }
+    }
+}

+ 8 - 0
src/config.rs

@@ -20,6 +20,7 @@ pub(crate) struct Config {
     pub(crate) ping: Option<PingConfig>,
     pub(crate) nominate: Option<NominateConfig>,
     pub(crate) prioritize: Option<PrioritizeConfig>,
+    pub(crate) major_change: Option<MajorChangeConfig>,
 }
 
 #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
@@ -79,6 +80,13 @@ pub(crate) struct PrioritizeConfig {
     pub(crate) zulip_stream: u64,
 }
 
+#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
+pub(crate) struct MajorChangeConfig {
+    pub(crate) second_label: String,
+    pub(crate) meeting_label: String,
+    pub(crate) zulip_stream: u64,
+}
+
 pub(crate) async fn get(gh: &GithubClient, repo: &str) -> Result<Arc<Config>, ConfigurationError> {
     if let Some(config) = get_cached_config(repo) {
         log::trace!("returning config for {} from cache", repo);

+ 1 - 0
src/handlers.rs

@@ -69,6 +69,7 @@ handlers! {
     ping = ping::PingHandler,
     nominate = nominate::NominateHandler,
     prioritize = prioritize::PrioritizeHandler,
+    major_change = major_change::MajorChangeHandler,
     //tracking_issue = tracking_issue::TrackingIssueHandler,
 }
 

+ 139 - 0
src/handlers/major_change.rs

@@ -0,0 +1,139 @@
+use crate::{
+    config::MajorChangeConfig,
+    github::{self, Event},
+    handlers::{Context, Handler},
+    interactions::ErrorComment,
+};
+use futures::future::{BoxFuture, FutureExt};
+use parser::command::second::SecondCommand;
+use parser::command::{Command, Input};
+
+pub(super) enum Invocation {
+    Second,
+    NewProposal,
+}
+
+pub(super) struct MajorChangeHandler;
+
+impl Handler for MajorChangeHandler {
+    type Input = Invocation;
+    type Config = MajorChangeConfig;
+
+    fn parse_input(&self, ctx: &Context, event: &Event) -> Result<Option<Self::Input>, String> {
+        let body = if let Some(b) = event.comment_body() {
+            b
+        } else {
+            // not interested in other events
+            return Ok(None);
+        };
+
+        match event {
+            Event::Issue(e) => {
+                if e.action != github::IssuesAction::Opened {
+                    return Ok(None);
+                }
+            }
+            Event::IssueComment(e) => {
+                if e.action != github::IssueCommentAction::Created {
+                    return Ok(None);
+                }
+            }
+        }
+
+        if let Event::Issue(e) = event {
+            if e.issue.labels().iter().any(|l| l.name == "major-change") {
+                return Ok(Some(Invocation::NewProposal));
+            }
+        }
+
+        let mut input = Input::new(&body, &ctx.username);
+        match input.parse_command() {
+            Command::Second(Ok(SecondCommand)) => Ok(Some(Invocation::Second)),
+            _ => Ok(None),
+        }
+    }
+
+    fn handle_input<'a>(
+        &self,
+        ctx: &'a Context,
+        config: &'a Self::Config,
+        event: &'a Event,
+        cmd: Self::Input,
+    ) -> BoxFuture<'a, anyhow::Result<()>> {
+        handle_input(ctx, config, event, cmd).boxed()
+    }
+}
+
+async fn handle_input(
+    ctx: &Context,
+    config: &MajorChangeConfig,
+    event: &Event,
+    cmd: Invocation,
+) -> anyhow::Result<()> {
+    let issue = event.issue().unwrap();
+
+    let (zulip_msg, label_to_add) = match cmd {
+        Invocation::Second => {
+            if !issue.labels().iter().any(|l| l.name == "major-change") {
+                let cmnt = ErrorComment::new(
+                    &issue,
+                    "This is not a major change (it lacks the `major-change` label).",
+                );
+                cmnt.post(&ctx.github).await?;
+                return Ok(());
+            }
+
+            if issue.labels().iter().any(|l| l.name == config.second_label) {
+                let cmnt = ErrorComment::new(
+                    &issue,
+                    "This issue has already been seconded! There is currently no need to third an issue.",
+                );
+                cmnt.post(&ctx.github).await?;
+                return Ok(());
+            }
+
+            (format!(
+                "@*T-compiler*: Proposal [#{}]({}) has been seconded, and will be approved in 10 days if no objections are raised.",
+                issue.number,
+                event.html_url().unwrap()
+            ), config.second_label.clone())
+        }
+        Invocation::NewProposal => {
+            if !issue.labels().iter().any(|l| l.name == "major-change") {
+                let cmnt = ErrorComment::new(
+                    &issue,
+                    "This is not a major change (it lacks the `major-change` label).",
+                );
+                cmnt.post(&ctx.github).await?;
+                return Ok(());
+            }
+            (format!(
+                "A new proposal has been announced [#{}]({}). It will be brought up at the next meeting.",
+                issue.number,
+                event.html_url().unwrap()
+            ), config.meeting_label.clone())
+        }
+    };
+
+    let mut labels = issue.labels().to_owned();
+    labels.push(github::Label { name: label_to_add });
+    let github_req = issue.set_labels(&ctx.github, labels);
+
+    let mut zulip_topic = format!("#{} {}", issue.number, issue.title);
+    zulip_topic.truncate(60); // Zulip limitation
+
+    let zulip_stream = config.zulip_stream.to_string();
+    let zulip_req = crate::zulip::MessageApiRequest {
+        type_: "stream",
+        to: &zulip_stream,
+        topic: Some(&zulip_topic),
+        content: &zulip_msg,
+    };
+
+    let zulip_req = zulip_req.send(&ctx.github.raw());
+
+    let (gh_res, zulip_res) = futures::join!(github_req, zulip_req);
+    gh_res?;
+    zulip_res?;
+    Ok(())
+}