Skip to content

Add triage command #12

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 33 additions & 18 deletions parser/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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,
}

Expand Down Expand Up @@ -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!(
Expand Down Expand Up @@ -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,
}
}
Expand Down
77 changes: 77 additions & 0 deletions parser/src/command/triage.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
9 changes: 9 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/github.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ impl User {
}
}

#[derive(Debug, Clone, serde::Deserialize)]
#[derive(Debug, Clone, serde::Deserialize, PartialEq, Eq)]
pub struct Label {
pub name: String,
}
Expand Down
1 change: 1 addition & 0 deletions src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ macro_rules! handlers {
handlers! {
assign = assign::AssignmentHandler,
relabel = relabel::RelabelHandler,
triage = triage::TriageHandler,
//tracking_issue = tracking_issue::TrackingIssueHandler,
}

Expand Down
82 changes: 82 additions & 0 deletions src/handlers/triage.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
}