Skip to content

Commit 76a9a45

Browse files
Merge pull request #708 from kellda/multicmd
Allow multiple commands in a single comment
2 parents 72ead8c + aea4015 commit 76a9a45

File tree

3 files changed

+122
-104
lines changed

3 files changed

+122
-104
lines changed

parser/src/command.rs

Lines changed: 31 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ pub enum Command<'a> {
2323
Prioritize(Result<prioritize::PrioritizeCommand, Error<'a>>),
2424
Second(Result<second::SecondCommand, Error<'a>>),
2525
Glacier(Result<glacier::GlacierCommand, Error<'a>>),
26-
None,
2726
}
2827

2928
#[derive(Debug)]
@@ -64,12 +63,7 @@ impl<'a> Input<'a> {
6463
}
6564
}
6665

67-
pub fn parse_command(&mut self) -> Command<'a> {
68-
let start = match find_commmand_start(&self.all[self.parsed..], self.bot) {
69-
Some(pos) => pos,
70-
None => return Command::None,
71-
};
72-
self.parsed += start;
66+
fn parse_command(&mut self) -> Option<Command<'a>> {
7367
let mut tok = Tokenizer::new(&self.all[self.parsed..]);
7468
assert_eq!(
7569
tok.next_token().unwrap(),
@@ -131,18 +125,31 @@ impl<'a> Input<'a> {
131125
.is_some()
132126
{
133127
log::info!("command overlaps code; code: {:?}", self.code);
134-
return Command::None;
128+
return None;
135129
}
136130

137-
match success.pop() {
138-
Some((mut tok, c)) => {
139-
// if we errored out while parsing the command do not move the input forwards
140-
if c.is_ok() {
141-
self.parsed += tok.position();
142-
}
143-
c
131+
let (mut tok, c) = success.pop()?;
132+
// if we errored out while parsing the command do not move the input forwards
133+
self.parsed += if c.is_ok() {
134+
tok.position()
135+
} else {
136+
self.bot.len() + 1
137+
};
138+
Some(c)
139+
}
140+
}
141+
142+
impl<'a> Iterator for Input<'a> {
143+
type Item = Command<'a>;
144+
145+
fn next(&mut self) -> Option<Command<'a>> {
146+
loop {
147+
let start = find_commmand_start(&self.all[self.parsed..], self.bot)?;
148+
self.parsed += start;
149+
if let Some(command) = self.parse_command() {
150+
return Some(command);
144151
}
145-
None => Command::None,
152+
self.parsed += self.bot.len() + 1;
146153
}
147154
}
148155
}
@@ -157,35 +164,27 @@ impl<'a> Command<'a> {
157164
Command::Prioritize(r) => r.is_ok(),
158165
Command::Second(r) => r.is_ok(),
159166
Command::Glacier(r) => r.is_ok(),
160-
Command::None => true,
161167
}
162168
}
163169

164170
pub fn is_err(&self) -> bool {
165171
!self.is_ok()
166172
}
167-
168-
pub fn is_none(&self) -> bool {
169-
match self {
170-
Command::None => true,
171-
_ => false,
172-
}
173-
}
174173
}
175174

176175
#[test]
177176
fn errors_outside_command_are_fine() {
178177
let input =
179178
"haha\" unterminated quotes @bot modify labels: +bug. Terminating after the command";
180179
let mut input = Input::new(input, "bot");
181-
assert!(input.parse_command().is_ok());
180+
assert!(input.next().unwrap().is_ok());
182181
}
183182

184183
#[test]
185184
fn code_1() {
186185
let input = "`@bot modify labels: +bug.`";
187186
let mut input = Input::new(input, "bot");
188-
assert!(input.parse_command().is_none());
187+
assert!(input.next().is_none());
189188
}
190189

191190
#[test]
@@ -194,7 +193,7 @@ fn code_2() {
194193
@bot modify labels: +bug.
195194
```";
196195
let mut input = Input::new(input, "bot");
197-
assert!(input.parse_command().is_none());
196+
assert!(input.next().is_none());
198197
}
199198

200199
#[test]
@@ -203,7 +202,7 @@ fn edit_1() {
203202
let mut input_old = Input::new(input_old, "bot");
204203
let input_new = "Adding labels: @bot modify labels: +bug. some other text";
205204
let mut input_new = Input::new(input_new, "bot");
206-
assert_eq!(input_old.parse_command(), input_new.parse_command());
205+
assert_eq!(input_old.next(), input_new.next());
207206
}
208207

209208
#[test]
@@ -212,23 +211,22 @@ fn edit_2() {
212211
let mut input_old = Input::new(input_old, "bot");
213212
let input_new = "@bot modify labels: +bug.";
214213
let mut input_new = Input::new(input_new, "bot");
215-
assert_ne!(input_old.parse_command(), input_new.parse_command());
214+
assert_ne!(input_old.next(), input_new.next());
216215
}
217216

218217
#[test]
219218
fn move_input_along() {
220219
let input = "@bot modify labels: +bug. Afterwards, delete the world.";
221220
let mut input = Input::new(input, "bot");
222-
let parsed = input.parse_command();
223-
assert!(parsed.is_ok());
221+
assert!(input.next().unwrap().is_ok());
224222
assert_eq!(&input.all[input.parsed..], " Afterwards, delete the world.");
225223
}
226224

227225
#[test]
228226
fn move_input_along_1() {
229227
let input = "@bot modify labels\": +bug. Afterwards, delete the world.";
230228
let mut input = Input::new(input, "bot");
231-
assert!(input.parse_command().is_err());
229+
assert!(input.next().unwrap().is_err());
232230
// don't move input along if parsing the command fails
233-
assert_eq!(input.parsed, 0);
231+
assert_eq!(&input.all[..input.parsed], "@bot");
234232
}

src/handlers.rs

Lines changed: 69 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,16 @@ mod prioritize;
3535
mod relabel;
3636
mod rustc_commits;
3737

38-
// TODO: Return multiple handler errors ?
39-
pub async fn handle(ctx: &Context, event: &Event) -> Result<(), HandlerError> {
38+
pub async fn handle(ctx: &Context, event: &Event) -> Vec<HandlerError> {
4039
let config = config::get(&ctx.github, event.repo_name()).await;
40+
let mut errors = Vec::new();
4141

4242
if let (Ok(config), Event::Issue(event)) = (config.as_ref(), event) {
43-
handle_issue(ctx, event, config).await?;
43+
handle_issue(ctx, event, config, &mut errors).await;
4444
}
4545

4646
if let Some(body) = event.comment_body() {
47-
handle_command(ctx, event, &config, body).await?;
47+
handle_command(ctx, event, &config, body, &mut errors).await;
4848
}
4949

5050
if let Err(e) = notification::handle(ctx, event).await {
@@ -63,28 +63,34 @@ pub async fn handle(ctx: &Context, event: &Event) -> Result<(), HandlerError> {
6363
);
6464
}
6565

66-
Ok(())
66+
errors
6767
}
6868

6969
macro_rules! issue_handlers {
7070
($($name:ident,)*) => {
71-
async fn handle_issue(ctx: &Context, event: &IssuesEvent, config: &Arc<Config>) -> Result<(), HandlerError> {
71+
async fn handle_issue(
72+
ctx: &Context,
73+
event: &IssuesEvent,
74+
config: &Arc<Config>,
75+
errors: &mut Vec<HandlerError>,
76+
) {
7277
$(
73-
if let Some(input) = $name::parse_input(
74-
ctx, event, config.$name.as_ref(),
75-
).map_err(HandlerError::Message)? {
76-
if let Some(config) = &config.$name {
77-
$name::handle_input(ctx, config, event, input).await.map_err(HandlerError::Other)?;
78-
} else {
79-
return Err(HandlerError::Message(format!(
80-
"The feature `{}` is not enabled in this repository.\n\
81-
To enable it add its section in the `triagebot.toml` \
82-
in the root of the repository.",
83-
stringify!($name)
84-
)));
78+
match $name::parse_input(ctx, event, config.$name.as_ref()) {
79+
Err(err) => errors.push(HandlerError::Message(err)),
80+
Ok(Some(input)) => {
81+
if let Some(config) = &config.$name {
82+
$name::handle_input(ctx, config, event, input).await.unwrap_or_else(|err| errors.push(HandlerError::Other(err)));
83+
} else {
84+
errors.push(HandlerError::Message(format!(
85+
"The feature `{}` is not enabled in this repository.\n\
86+
To enable it add its section in the `triagebot.toml` \
87+
in the root of the repository.",
88+
stringify!($name)
89+
)));
90+
}
8591
}
92+
Ok(None) => {}
8693
})*
87-
Ok(())
8894
}
8995
}
9096
}
@@ -101,72 +107,73 @@ issue_handlers! {
101107

102108
macro_rules! command_handlers {
103109
($($name:ident: $enum:ident,)*) => {
104-
async fn handle_command(ctx: &Context, event: &Event, config: &Result<Arc<Config>, ConfigurationError>, body: &str) -> Result<(), HandlerError> {
110+
async fn handle_command(
111+
ctx: &Context,
112+
event: &Event,
113+
config: &Result<Arc<Config>, ConfigurationError>,
114+
body: &str,
115+
errors: &mut Vec<HandlerError>,
116+
) {
105117
if let Event::Issue(e) = event {
106118
if !matches!(e.action, IssuesAction::Opened | IssuesAction::Edited) {
107119
// no change in issue's body for these events, so skip
108120
log::debug!("skipping event, issue was {:?}", e.action);
109-
return Ok(());
121+
return;
110122
}
111123
}
112124

113-
// TODO: parse multiple commands and diff them
114-
let mut input = Input::new(&body, &ctx.username);
115-
let command = input.parse_command();
116-
117-
if let Some(previous) = event.comment_from() {
118-
let mut prev_input = Input::new(&previous, &ctx.username);
119-
let prev_command = prev_input.parse_command();
120-
if command == prev_command {
121-
log::info!("skipping unmodified command: {:?} -> {:?}", prev_command, command);
122-
return Ok(());
123-
} else {
124-
log::debug!("executing modified command: {:?} -> {:?}", prev_command, command);
125-
}
126-
}
125+
let input = Input::new(&body, &ctx.username);
126+
let commands = if let Some(previous) = event.comment_from() {
127+
let prev_commands = Input::new(&previous, &ctx.username).collect::<Vec<_>>();
128+
input.filter(|cmd| !prev_commands.contains(cmd)).collect::<Vec<_>>()
129+
} else {
130+
input.collect()
131+
};
127132

128-
if command == Command::None {
129-
return Ok(());
133+
if commands.is_empty() {
134+
return;
130135
}
131136

132137
let config = match config {
133138
Ok(config) => config,
134139
Err(e @ ConfigurationError::Missing) => {
135-
return Err(HandlerError::Message(e.to_string()));
140+
return errors.push(HandlerError::Message(e.to_string()));
136141
}
137142
Err(e @ ConfigurationError::Toml(_)) => {
138-
return Err(HandlerError::Message(e.to_string()));
143+
return errors.push(HandlerError::Message(e.to_string()));
139144
}
140145
Err(e @ ConfigurationError::Http(_)) => {
141-
return Err(HandlerError::Other(e.clone().into()));
146+
return errors.push(HandlerError::Other(e.clone().into()));
142147
}
143148
};
144149

145-
match command {
146-
$(
147-
Command::$enum(Ok(command)) => {
148-
if let Some(config) = &config.$name {
149-
$name::handle_command(ctx, config, event, command).await.map_err(HandlerError::Other)?;
150-
} else {
151-
return Err(HandlerError::Message(format!(
152-
"The feature `{}` is not enabled in this repository.\n\
153-
To enable it add its section in the `triagebot.toml` \
154-
in the root of the repository.",
155-
stringify!($name)
156-
)));
150+
for command in commands {
151+
match command {
152+
$(
153+
Command::$enum(Ok(command)) => {
154+
if let Some(config) = &config.$name {
155+
$name::handle_command(ctx, config, event, command)
156+
.await
157+
.unwrap_or_else(|err| errors.push(HandlerError::Other(err)));
158+
} else {
159+
errors.push(HandlerError::Message(format!(
160+
"The feature `{}` is not enabled in this repository.\n\
161+
To enable it add its section in the `triagebot.toml` \
162+
in the root of the repository.",
163+
stringify!($name)
164+
)));
165+
}
157166
}
167+
Command::$enum(Err(err)) => {
168+
errors.push(HandlerError::Message(format!(
169+
"Parsing {} command in [comment]({}) failed: {}",
170+
stringify!($name),
171+
event.html_url().expect("has html url"),
172+
err
173+
)));
174+
})*
158175
}
159-
Command::$enum(Err(err)) => {
160-
return Err(HandlerError::Message(format!(
161-
"Parsing {} command in [comment]({}) failed: {}",
162-
stringify!($name),
163-
event.html_url().expect("has html url"),
164-
err
165-
)));
166-
})*
167-
Command::None => unreachable!(),
168176
}
169-
Ok(())
170177
}
171178
}
172179
}

src/lib.rs

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -155,21 +155,34 @@ pub async fn webhook(
155155
return Ok(false);
156156
}
157157
};
158-
if let Err(err) = handlers::handle(&ctx, &event).await {
158+
let errors = handlers::handle(&ctx, &event).await;
159+
let mut other_error = false;
160+
let mut message = String::new();
161+
for err in errors {
159162
match err {
160-
HandlerError::Message(message) => {
161-
if let Some(issue) = event.issue() {
162-
let cmnt = ErrorComment::new(issue, message);
163-
cmnt.post(&ctx.github).await?;
163+
HandlerError::Message(msg) => {
164+
if !message.is_empty() {
165+
message.push_str("\n\n");
164166
}
167+
message.push_str(&msg);
165168
}
166169
HandlerError::Other(err) => {
167170
log::error!("handling event failed: {:?}", err);
168-
return Err(WebhookError(anyhow::anyhow!(
169-
"handling failed, error logged",
170-
)));
171+
other_error = true;
171172
}
172173
}
173174
}
174-
Ok(true)
175+
if !message.is_empty() {
176+
if let Some(issue) = event.issue() {
177+
let cmnt = ErrorComment::new(issue, message);
178+
cmnt.post(&ctx.github).await?;
179+
}
180+
}
181+
if other_error {
182+
Err(WebhookError(anyhow::anyhow!(
183+
"handling failed, error logged",
184+
)))
185+
} else {
186+
Ok(true)
187+
}
175188
}

0 commit comments

Comments
 (0)