Skip to content

Commit 1e570d9

Browse files
Centrilanp
authored andcommitted
Accept multiple @rfcbot invocations per comment (#211)
closes #168
1 parent fd4e44f commit 1e570d9

File tree

7 files changed

+143
-35
lines changed

7 files changed

+143
-35
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@ rfcbot behaves so that the people who develop rfcbot or interact with it can
88
understand the bot better.
99

1010
## Changes
11+
12+
+ rfcbot now accepts multiple invocations / commands per comment you post.

Cargo.lock

+7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ serde_json = "1.0"
2424
toml = "0.4"
2525
url = "1.4"
2626
urlencoded = "0.5"
27+
maplit = "1.0.1"
2728

2829
[dependencies.chrono]
2930
features = ["serde"]

README.md

+43
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,49 @@ Only the first of these commands will be registered:
4040

4141
Examples are in each section.
4242

43+
### Command grammar
44+
45+
rfcbot accepts roughly the following grammar:
46+
47+
```ebnf
48+
merge ::= "merge" | "merged" | "merging" | "merges" ;
49+
close ::= "close" | "closed" | "closing" | "closes" ;
50+
postpone ::= "postpone" | "postponed" | "postponing" | "postpones" ;
51+
cancel ::= "cancel | "canceled" | "canceling" | "cancels" ;
52+
review ::= "reviewed" | "review" | "reviewing" | "reviews" ;
53+
concern ::= "concern" | "concerned" | "concerning" | "concerns" ;
54+
resolve ::= "resolve" | "resolved" | "resolving" | "resolves" ;
55+
56+
line_remainder ::= .+$ ;
57+
ws_separated ::= ... ;
58+
59+
subcommand ::= merge | close | postpone | cancel | review
60+
| concern line_remainder
61+
| resolve line_remainder
62+
;
63+
64+
invocation ::= "fcp" subcommand
65+
| "pr" subcommand
66+
| "f?" ws_separated
67+
| subcommand
68+
;
69+
70+
grammar ::= "@rfcbot" ":"? invocation ;
71+
```
72+
73+
Multiple occurrences of `grammar` are allowed in each comment you make on GitHub.
74+
This means that the following is OK:
75+
76+
```
77+
@rfcbot merge
78+
79+
Some stuff you want to say...
80+
81+
@rfcbot concern foobar
82+
83+
Explain the concern...
84+
```
85+
4386
### Final Comment Period
4487

4588
Before proposing a final comment period on an issue/PR/RFC, please double check to make sure that the correct team label(s) has been applied to the issue. As of 9/17/16, rfcbot recognizes these labels:

src/github/client.rs

+5-15
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,6 @@ pub const DELAY: u64 = 300;
2626

2727
type ParameterMap = BTreeMap<&'static str, String>;
2828

29-
macro_rules! params {
30-
($($key: expr => $val: expr),*) => {{
31-
let mut map = BTreeMap::<_, _>::new();
32-
$(map.insert($key, $val);)*
33-
map
34-
}};
35-
}
36-
3729
header! { (TZ, "Time-Zone") => [String] }
3830
header! { (Accept, "Accept") => [String] }
3931
header! { (RateLimitRemaining, "X-RateLimit-Remaining") => [u32] }
@@ -94,7 +86,7 @@ impl Client {
9486

9587
pub fn issues_since(&self, repo: &str, start: DateTime<Utc>) -> DashResult<Vec<IssueFromJson>> {
9688
self.get_models(&format!("{}/repos/{}/issues", BASE_URL, repo),
97-
Some(&params! {
89+
Some(&btreemap! {
9890
"state" => "all".to_string(),
9991
"since" => format!("{:?}", start),
10092
"per_page" => format!("{}", PER_PAGE),
@@ -107,7 +99,7 @@ impl Client {
10799
start: DateTime<Utc>)
108100
-> DashResult<Vec<CommentFromJson>> {
109101
self.get_models(&format!("{}/repos/{}/issues/comments", BASE_URL, repo),
110-
Some(&params! {
102+
Some(&btreemap! {
111103
"sort" => "created".to_string(),
112104
"direction" => "asc".to_string(),
113105
"since" => format!("{:?}", start),
@@ -164,7 +156,7 @@ impl Client {
164156

165157
pub fn close_issue(&self, repo: &str, issue_num: i32) -> DashResult<()> {
166158
let url = format!("{}/repos/{}/issues/{}", BASE_URL, repo, issue_num);
167-
let payload = serde_json::to_string(&params!("state" => "closed"))?;
159+
let payload = serde_json::to_string(&btreemap!("state" => "closed"))?;
168160
let mut res = self.patch(&url, &payload)?;
169161

170162
if StatusCode::Ok != res.status {
@@ -208,9 +200,7 @@ impl Client {
208200
text: &str)
209201
-> DashResult<CommentFromJson> {
210202
let url = format!("{}/repos/{}/issues/{}/comments", BASE_URL, repo, issue_num);
211-
212-
let payload = serde_json::to_string(&params!("body" => text))?;
213-
203+
let payload = serde_json::to_string(&btreemap!("body" => text))?;
214204
// FIXME propagate an error if it's a 404 or other error
215205
self.deserialize(&mut self.post(&url, &payload)?)
216206
}
@@ -225,7 +215,7 @@ impl Client {
225215
repo,
226216
comment_num);
227217

228-
let payload = serde_json::to_string(&params!("body" => text))?;
218+
let payload = serde_json::to_string(&btreemap!("body" => text))?;
229219

230220
// FIXME propagate an error if it's a 404 or other error
231221
self.deserialize(&mut self.patch(&url, &payload)?)

src/github/nag.rs

+83-20
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,13 @@ pub fn update_nags(comment: &IssueComment) -> DashResult<()> {
8585

8686
let subteam_members = subteam_members(&issue)?;
8787

88-
// attempt to parse a command out of the comment
89-
if let Ok(command) = RfcBotCommand::from_str(&comment.body) {
88+
// Attempt to parse all commands out of the comment
89+
let mut any = false;
90+
for command in RfcBotCommand::from_str_all(&comment.body) {
91+
any = true;
9092

91-
// don't accept bot commands from non-subteam members
93+
// Don't accept bot commands from non-subteam members.
94+
// Early return because we'll just get here again...
9295
if subteam_members.iter().find(|&u| u == &author).is_none() {
9396
info!("command author ({}) doesn't appear in any relevant subteams",
9497
author.login);
@@ -104,8 +107,9 @@ pub fn update_nags(comment: &IssueComment) -> DashResult<()> {
104107
});
105108

106109
debug!("rfcbot command is processed");
110+
}
107111

108-
} else {
112+
if !any {
109113
ok_or!(resolve_applicable_feedback_requests(&author, &issue, comment),
110114
why => error!("Unable to resolve feedback requests for comment id {}: {:?}",
111115
comment.id, why));
@@ -578,6 +582,32 @@ fn parse_command_text<'a>(command: &'a str, subcommand: &'a str) -> &'a str {
578582
/// Parses all subcommands under the fcp command.
579583
/// If `fcp_context` is set to false, `@rfcbot <subcommand>`
580584
/// was passed and not `@rfcbot fcp <subcommand>`.
585+
///
586+
/// @rfcbot accepts roughly the following grammar:
587+
///
588+
/// merge ::= "merge" | "merged" | "merging" | "merges" ;
589+
/// close ::= "close" | "closed" | "closing" | "closes" ;
590+
/// postpone ::= "postpone" | "postponed" | "postponing" | "postpones" ;
591+
/// cancel ::= "cancel | "canceled" | "canceling" | "cancels" ;
592+
/// review ::= "reviewed" | "review" | "reviewing" | "reviews" ;
593+
/// concern ::= "concern" | "concerned" | "concerning" | "concerns" ;
594+
/// resolve ::= "resolve" | "resolved" | "resolving" | "resolves" ;
595+
///
596+
/// line_remainder ::= .+$ ;
597+
/// ws_separated ::= ... ;
598+
///
599+
/// subcommand ::= merge | close | postpone | cancel | review
600+
/// | concern line_remainder
601+
/// | resolve line_remainder
602+
/// ;
603+
///
604+
/// invocation ::= "fcp" subcommand
605+
/// | "pr" subcommand
606+
/// | "f?" ws_separated
607+
/// | subcommand
608+
/// ;
609+
///
610+
/// grammar ::= "@rfcbot" ":"? invocation ;
581611
fn parse_fcp_subcommand<'a>(
582612
command: &'a str,
583613
subcommand: &'a str,
@@ -876,20 +906,20 @@ impl<'a> RfcBotCommand<'a> {
876906
Ok(())
877907
}
878908

879-
pub fn from_str(command: &'a str) -> DashResult<RfcBotCommand<'a>> {
880-
// get the tokens for the command line (starts with a bot mention)
881-
let command = command
882-
.lines()
883-
.find(|&l| l.starts_with(RFC_BOT_MENTION))
884-
.ok_or(DashError::Misc(None))?
885-
.trim_left_matches(RFC_BOT_MENTION)
886-
.trim_left_matches(':')
887-
.trim();
888-
889-
let mut tokens = command.split_whitespace();
909+
pub fn from_str_all(command: &'a str) -> impl Iterator<Item = RfcBotCommand<'a>> {
910+
// Get the tokens for each command line (starts with a bot mention)
911+
command.lines()
912+
.filter(|&l| l.starts_with(RFC_BOT_MENTION))
913+
.map(Self::from_invocation_line)
914+
.filter_map(Result::ok)
915+
}
890916

917+
fn from_invocation_line(command: &'a str) -> DashResult<RfcBotCommand<'a>> {
918+
let mut tokens = command.trim_left_matches(RFC_BOT_MENTION)
919+
.trim_left_matches(':')
920+
.trim()
921+
.split_whitespace();
891922
let invocation = tokens.next().ok_or(DashError::Misc(None))?;
892-
893923
match invocation {
894924
"fcp" | "pr" => {
895925
let subcommand = tokens.next().ok_or(DashError::Misc(None))?;
@@ -913,6 +943,7 @@ impl<'a> RfcBotCommand<'a> {
913943
_ => parse_fcp_subcommand(command, invocation, false),
914944
}
915945
}
946+
916947
}
917948

918949
struct RfcBotComment<'a> {
@@ -1120,6 +1151,34 @@ impl<'a> RfcBotComment<'a> {
11201151
mod test {
11211152
use super::*;
11221153

1154+
#[test]
1155+
fn multiple_commands() {
1156+
let text = r#"
1157+
someothertext
1158+
@rfcbot: resolved CONCERN_NAME
1159+
somemoretext
1160+
1161+
somemoretext
1162+
1163+
@rfcbot: fcp cancel
1164+
foobar
1165+
@rfcbot concern foobar
1166+
"#;
1167+
1168+
let cmd = RfcBotCommand::from_str_all(text).collect::<Vec<_>>();
1169+
assert_eq!(cmd, vec![
1170+
RfcBotCommand::ResolveConcern("CONCERN_NAME"),
1171+
RfcBotCommand::FcpCancel,
1172+
RfcBotCommand::NewConcern("foobar"),
1173+
]);
1174+
}
1175+
1176+
fn ensure_take_singleton<I: Iterator>(mut iter: I) -> I::Item {
1177+
let singleton = iter.next().unwrap();
1178+
assert!(iter.next().is_none());
1179+
singleton
1180+
}
1181+
11231182
macro_rules! justification {
11241183
() => { "\n\nSome justification here." };
11251184
}
@@ -1148,8 +1207,11 @@ somemoretext")
11481207
let body = concat!("@rfcbot: ", $cmd);
11491208
let body_no_colon = concat!("@rfcbot ", $cmd);
11501209

1151-
let with_colon = RfcBotCommand::from_str(body).unwrap();
1152-
let without_colon = RfcBotCommand::from_str(body_no_colon).unwrap();
1210+
let with_colon =
1211+
ensure_take_singleton(RfcBotCommand::from_str_all(body));
1212+
1213+
let without_colon =
1214+
ensure_take_singleton(RfcBotCommand::from_str_all(body_no_colon));
11531215

11541216
assert_eq!(with_colon, without_colon);
11551217
assert_eq!(with_colon, expected);
@@ -1220,8 +1282,9 @@ somemoretext
12201282
12211283
somemoretext";
12221284

1223-
let with_colon = RfcBotCommand::from_str(body).unwrap();
1224-
let without_colon = RfcBotCommand::from_str(body_no_colon).unwrap();
1285+
let with_colon = ensure_take_singleton(RfcBotCommand::from_str_all(body));
1286+
let without_colon =
1287+
ensure_take_singleton(RfcBotCommand::from_str_all(body_no_colon));
12251288

12261289
assert_eq!(with_colon, without_colon);
12271290
assert_eq!(with_colon, RfcBotCommand::ResolveConcern("CONCERN_NAME"));

src/main.rs

+2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ extern crate serde_json;
3030
extern crate toml;
3131
extern crate url;
3232
extern crate urlencoded;
33+
#[macro_use]
34+
extern crate maplit;
3335

3436
#[macro_use]
3537
mod macros;

0 commit comments

Comments
 (0)