Skip to content

Run commands on just-opened issue bodies #18

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

Merged
Merged
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
54 changes: 54 additions & 0 deletions src/github.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ pub struct Issue {
pub number: u64,
pub body: String,
title: String,
html_url: String,
user: User,
labels: Vec<Label>,
assignees: Vec<User>,
Expand Down Expand Up @@ -280,6 +281,34 @@ pub struct IssueCommentEvent {
pub repository: Repository,
}

#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum IssuesAction {
Opened,
Edited,
Deleted,
Transferred,
Pinned,
Unpinned,
Closed,
Reopened,
Assigned,
Unassigned,
Labeled,
Unlabeled,
Locked,
Unlocked,
Milestoned,
Demilestoned,
}

#[derive(Debug, serde::Deserialize)]
pub struct IssuesEvent {
pub action: IssuesAction,
pub issue: Issue,
pub repository: Repository,
}

#[derive(Debug, serde::Deserialize)]
pub struct Repository {
pub full_name: String,
Expand All @@ -288,18 +317,43 @@ pub struct Repository {
#[derive(Debug)]
pub enum Event {
IssueComment(IssueCommentEvent),
Issue(IssuesEvent),
}

impl Event {
pub fn repo_name(&self) -> &str {
match self {
Event::IssueComment(event) => &event.repository.full_name,
Event::Issue(event) => &event.repository.full_name,
}
}

pub fn issue(&self) -> Option<&Issue> {
match self {
Event::IssueComment(event) => Some(&event.issue),
Event::Issue(event) => Some(&event.issue),
}
}

/// This will both extract from IssueComment events but also Issue events
pub fn comment_body(&self) -> Option<&str> {
match self {
Event::Issue(e) => Some(&e.issue.body),
Event::IssueComment(e) => Some(&e.comment.body),
}
}

pub fn html_url(&self) -> Option<&str> {
match self {
Event::Issue(e) => Some(&e.issue.html_url),
Event::IssueComment(e) => Some(&e.comment.html_url),
}
}

pub fn user(&self) -> &User {
match self {
Event::Issue(e) => &e.issue.user,
Event::IssueComment(e) => &e.comment.user,
}
}
}
Expand Down
60 changes: 32 additions & 28 deletions src/handlers/assign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,21 +33,28 @@ impl Handler for AssignmentHandler {
type Config = AssignConfig;

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
let body = if let Some(b) = event.comment_body() {
b
} else {
// not interested in other events
return Ok(None);
};

let mut input = Input::new(&event.comment.body, &ctx.username);
if let Event::Issue(e) = event {
if e.action != github::IssuesAction::Opened {
// skip events other than opening the issue to avoid retriggering commands in the
// issue body
return Ok(None);
}
}

let mut input = Input::new(&body, &ctx.username);
match input.parse_command() {
Command::Assign(Ok(command)) => Ok(Some(command)),
Command::Assign(Err(err)) => {
failure::bail!(
"Parsing assign command in [comment]({}) failed: {}",
event.comment.html_url,
event.html_url().expect("has html url"),
err
);
}
Expand All @@ -62,27 +69,18 @@ impl Handler for AssignmentHandler {
event: &Event,
cmd: AssignCommand,
) -> Result<(), Error> {
#[allow(irrefutable_let_patterns)]
let event = if let Event::IssueComment(e) = event {
e
let is_team_member = if let Err(_) | Ok(false) = event.user().is_team_member(&ctx.github) {
false
} else {
// not interested in other events
return Ok(());
true
};

let is_team_member =
if let Err(_) | Ok(false) = event.comment.user.is_team_member(&ctx.github) {
false
} else {
true
};

let e = EditIssueBody::new(&event.issue, "ASSIGN");
let e = EditIssueBody::new(&event.issue().unwrap(), "ASSIGN");

let to_assign = match cmd {
AssignCommand::Own => event.comment.user.login.clone(),
AssignCommand::Own => event.user().login.clone(),
AssignCommand::User { username } => {
if !is_team_member && username != event.comment.user.login {
if !is_team_member && username != event.user().login {
failure::bail!("Only Rust team members can assign other users");
}
username.clone()
Expand All @@ -92,18 +90,22 @@ impl Handler for AssignmentHandler {
user: Some(current),
}) = e.current_data()
{
if current == event.comment.user.login || is_team_member {
event.issue.remove_assignees(&ctx.github, Selection::All)?;
if current == event.user().login || is_team_member {
event
.issue()
.unwrap()
.remove_assignees(&ctx.github, Selection::All)?;
e.apply(&ctx.github, String::new(), AssignData { user: None })?;
return Ok(());
} else {
failure::bail!("Cannot release another user's assignment");
}
} else {
let current = &event.comment.user;
if event.issue.contain_assignee(current) {
let current = &event.user();
if event.issue().unwrap().contain_assignee(current) {
event
.issue
.issue()
.unwrap()
.remove_assignees(&ctx.github, Selection::One(&current))?;
e.apply(&ctx.github, String::new(), AssignData { user: None })?;
return Ok(());
Expand All @@ -119,18 +121,20 @@ impl Handler for AssignmentHandler {

e.apply(&ctx.github, String::new(), &data)?;

match event.issue.set_assignee(&ctx.github, &to_assign) {
match event.issue().unwrap().set_assignee(&ctx.github, &to_assign) {
Ok(()) => return Ok(()), // we are done
Err(github::AssignmentError::InvalidAssignee) => {
event
.issue
.issue()
.unwrap()
.set_assignee(&ctx.github, &ctx.username)
.context("self-assignment failed")?;
e.apply(
&ctx.github,
format!(
"This issue has been assigned to @{} via [this comment]({}).",
to_assign, event.comment.html_url
to_assign,
event.html_url().unwrap()
),
&data,
)?;
Expand Down
39 changes: 21 additions & 18 deletions src/handlers/relabel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use crate::{
interactions::ErrorComment,
};
use failure::Error;
use parser::command::relabel::{RelabelCommand, LabelDelta};
use parser::command::relabel::{LabelDelta, RelabelCommand};
use parser::command::{Command, Input};

pub(super) struct RelabelHandler;
Expand All @@ -25,21 +25,29 @@ impl Handler for RelabelHandler {
type Config = RelabelConfig;

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
let body = if let Some(b) = event.comment_body() {
b
} else {
// not interested in other events
return Ok(None);
};

let mut input = Input::new(&event.comment.body, &ctx.username);
if let Event::Issue(e) = event {
if e.action != github::IssuesAction::Opened {
// skip events other than opening the issue to avoid retriggering commands in the
// issue body
return Ok(None);
}
}

let mut input = Input::new(&body, &ctx.username);
match input.parse_command() {
Command::Relabel(Ok(command)) => Ok(Some(command)),
Command::Relabel(Err(err)) => {
failure::bail!(
"Parsing label command in [comment]({}) failed: {}",
event.comment.html_url, err
event.html_url().expect("has html url"),
err
);
}
_ => Ok(None),
Expand All @@ -53,20 +61,12 @@ impl Handler for RelabelHandler {
event: &Event,
input: RelabelCommand,
) -> Result<(), Error> {
#[allow(irrefutable_let_patterns)]
let event = if let Event::IssueComment(e) = event {
e
} else {
// not interested in other events
return Ok(());
};

let mut issue_labels = event.issue.labels().to_owned();
let mut issue_labels = event.issue().unwrap().labels().to_owned();
let mut changed = false;
for delta in &input.0 {
let name = delta.label().as_str();
if let Err(msg) = check_filter(name, config, &event.comment.user, &ctx.github) {
ErrorComment::new(&event.issue, msg.to_string()).post(&ctx.github)?;
if let Err(msg) = check_filter(name, config, &event.user(), &ctx.github) {
ErrorComment::new(&event.issue().unwrap(), msg.to_string()).post(&ctx.github)?;
return Ok(());
}
match delta {
Expand All @@ -88,7 +88,10 @@ impl Handler for RelabelHandler {
}

if changed {
event.issue.set_labels(&ctx.github, issue_labels)?;
event
.issue()
.unwrap()
.set_labels(&ctx.github, issue_labels)?;
}

Ok(())
Expand Down
13 changes: 3 additions & 10 deletions src/handlers/tracking_issue.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#![cfg(empty)]
use crate::{
github::GithubClient,
registry::{Event, Handler},
Expand Down Expand Up @@ -84,16 +85,8 @@ impl TrackingIssueHandler {

impl Handler for TrackingIssueHandler {
fn handle_event(&self, event: &Event) -> Result<(), Error> {
#[allow(irrefutable_let_patterns)]
let event = if let Event::IssueComment(e) = event {
e
} else {
// not interested in other events
return Ok(());
};

self.handle_create(&event)?;
self.handle_link(&event)?;
//self.handle_create(&event)?;
//self.handle_link(&event)?;
Ok(())
}
}
16 changes: 16 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use payload::SignedPayload;

enum EventName {
IssueComment,
Issue,
Other,
}

Expand All @@ -36,6 +37,7 @@ impl<'a, 'r> request::FromRequest<'a, 'r> for EventName {
};
let ev = match ev {
"issue_comment" => EventName::IssueComment,
"issues" => EventName::Issue,
_ => EventName::Other,
};
Outcome::Success(ev)
Expand Down Expand Up @@ -83,6 +85,20 @@ fn webhook(
return Err(err.into());
}
}
EventName::Issue => {
let payload = payload
.deserialize::<github::IssuesEvent>()
.context("IssueCommentEvent failed to deserialize")
.map_err(Error::from)?;

let event = github::Event::Issue(payload);
if let Err(err) = handlers::handle(&ctx, &event) {
if let Some(issue) = event.issue() {
ErrorComment::new(issue, err.to_string()).post(&ctx.github)?;
}
return Err(err.into());
}
}
// Other events need not be handled
EventName::Other => {}
}
Expand Down