Skip to content

Commit 7cc9937

Browse files
committed
make the ffcp-auto-close/postpone behavior configurable. fixes #201
1 parent 0f5ed05 commit 7cc9937

File tree

3 files changed

+223
-73
lines changed

3 files changed

+223
-73
lines changed

src/github/nag.rs

+23-21
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use domain::rfcbot::{FcpConcern, FcpProposal, FcpReviewRequest, FeedbackRequest,
1414
use domain::schema::*;
1515
use error::*;
1616
use github::models::CommentFromJson;
17-
use teams::TEAMS;
17+
use teams::SETUP;
1818
use super::GH;
1919

2020
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
@@ -464,24 +464,31 @@ fn evaluate_nags() -> DashResult<()> {
464464
Ok(())
465465
}
466466

467-
fn execute_ffcp_actions(issue: &Issue, disposition: FcpDisposition) {
468-
if !is_rfc_repo(&issue) { return; }
467+
fn can_ffcp_close(issue: &Issue) -> bool {
468+
SETUP.ffcp_auto_close(&issue.repository)
469+
}
470+
471+
fn can_ffcp_postpone(issue: &Issue) -> bool {
472+
SETUP.ffcp_auto_postpone(&issue.repository)
473+
}
469474

475+
fn execute_ffcp_actions(issue: &Issue, disposition: FcpDisposition) {
470476
match disposition {
471477
FcpDisposition::Merge => {
472478
// TODO: This one will require a lot of work to
473479
// auto-merge RFCs and create the tracking issue.
474480
},
475-
FcpDisposition::Close => {
481+
FcpDisposition::Close if can_ffcp_close(issue) => {
476482
let _ = issue.add_label(Label::Closed);
477483
issue.remove_label(Label::DispositionClose);
478484
issue.close();
479485
},
480-
FcpDisposition::Postpone => {
486+
FcpDisposition::Postpone if can_ffcp_postpone(issue) => {
481487
let _ = issue.add_label(Label::Postponed);
482488
issue.remove_label(Label::DispositionPostpone);
483489
issue.close();
484490
},
491+
_ => {},
485492
}
486493
}
487494

@@ -566,9 +573,9 @@ fn subteam_members(issue: &Issue) -> DashResult<Vec<GitHubUser>> {
566573

567574
// retrieve all of the teams tagged on this issue
568575
// cannot WAIT for by-ref/by-val inference
569-
let member_logins = TEAMS.iter()
576+
let member_logins = SETUP.teams()
570577
.filter(|&(ref label, _)| issue.labels.contains(&label.0))
571-
.flat_map(|(_, team)| &team.member_logins)
578+
.flat_map(|(_, team)| team.member_logins())
572579
.collect::<BTreeSet<_>>()
573580
.into_iter() // diesel won't work with btreeset, and dedup has weird lifetime errors
574581
.collect::<Vec<_>>();
@@ -1028,10 +1035,6 @@ enum CommentType<'a> {
10281035
},
10291036
}
10301037

1031-
fn is_rfc_repo(issue: &Issue) -> bool {
1032-
issue.repository == "rust-lang/rfcs"
1033-
}
1034-
10351038
impl<'a> RfcBotComment<'a> {
10361039
fn new(issue: &'a Issue, comment_type: CommentType<'a>) -> RfcBotComment<'a> {
10371040

@@ -1146,16 +1149,15 @@ impl<'a> RfcBotComment<'a> {
11461149
Self::add_comment_url(issue, &mut msg, status_comment_id);
11471150
msg.push_str("), is now **complete**.");
11481151

1149-
if is_rfc_repo(issue) {
1150-
match disposition {
1151-
FcpDisposition::Merge => {}
1152-
FcpDisposition::Close => {
1153-
msg.push_str("\n\nBy the power vested in me by Rust, I hereby close this RFC.");
1154-
},
1155-
FcpDisposition::Postpone => {
1156-
msg.push_str("\n\nBy the power vested in me by Rust, I hereby postpone this RFC.");
1157-
},
1158-
}
1152+
match disposition {
1153+
FcpDisposition::Merge => {}
1154+
FcpDisposition::Close if can_ffcp_close(issue) => {
1155+
msg.push_str("\n\nBy the power vested in me by Rust, I hereby close this RFC.");
1156+
},
1157+
FcpDisposition::Postpone if can_ffcp_postpone(issue) => {
1158+
msg.push_str("\n\nBy the power vested in me by Rust, I hereby postpone this RFC.");
1159+
},
1160+
_ => {},
11591161
}
11601162

11611163
if !added_label {

src/main.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ fn main() {
7272
let _ = DB_POOL.get().expect("Unable to test connection pool.");
7373

7474
// we want to panic if we're unable to find any of the usernames
75-
let parsed_teams = teams::TEAMS.keys().collect::<Vec<_>>();
75+
let parsed_teams = teams::SETUP.team_labels().collect::<Vec<_>>();
7676
info!("parsed teams: {:?}", parsed_teams);
7777

7878
// FIXME(anp) need to handle panics in both the listeners and crash the server

src/teams.rs

+199-51
Original file line numberDiff line numberDiff line change
@@ -4,84 +4,232 @@ use std::collections::BTreeMap;
44

55
use diesel::prelude::*;
66
use toml;
7+
use serde::de;
78

89
use super::DB_POOL;
910
use domain::github::GitHubUser;
1011
use error::*;
1112

13+
//==============================================================================
14+
// Public API
15+
//==============================================================================
16+
1217
lazy_static! {
13-
pub static ref TEAMS: Teams = {
14-
let teams_file =
15-
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/teams.toml"));
16-
let teams_from_file: TeamsFromFile =
17-
toml::from_str(teams_file).expect("couldn't parse teams");
18-
19-
let mut teams = BTreeMap::new();
20-
21-
for (label, team_from_file) in teams_from_file {
22-
let label = TeamLabel(label);
23-
let team = team_from_file.validate()
24-
.expect("unable to verify team member from database.
25-
if you're running this for tests, make sure you've pulled github users from prod");
26-
teams.insert(label, team);
27-
}
18+
pub static ref SETUP: RfcbotConfig = read_rfcbot_cfg_validated();
19+
}
20+
21+
#[derive(Debug, Deserialize)]
22+
pub struct RfcbotConfig {
23+
fcp_behaviors: BTreeMap<String, FcpBehavior>,
24+
teams: BTreeMap<TeamLabel, Team>,
25+
}
26+
27+
impl RfcbotConfig {
28+
/// Retrive an iterator over all the team labels.
29+
pub fn team_labels(&self) -> impl Iterator<Item = &TeamLabel> {
30+
self.teams.keys()
31+
}
32+
33+
/// Retrive an iterator over all the (team label, team) pairs.
34+
pub fn teams(&self) -> impl Iterator<Item = (&TeamLabel, &Team)> {
35+
self.teams.iter()
36+
}
37+
38+
/// Are we allowed to auto-close issues after F-FCP in this repo?
39+
pub fn ffcp_auto_close(&self, repo: &str) -> bool {
40+
self.fcp_behaviors.get(repo).map(|fcp| fcp.close).unwrap_or_default()
41+
}
2842

29-
teams
30-
};
43+
/// Are we allowed to auto-postpone issues after F-FCP in this repo?
44+
pub fn ffcp_auto_postpone(&self, repo: &str) -> bool {
45+
self.fcp_behaviors.get(repo).map(|fcp| fcp.postpone).unwrap_or_default()
46+
}
47+
}
48+
49+
#[derive(Debug, Deserialize)]
50+
pub struct FcpBehavior {
51+
#[serde(default)]
52+
pub close: bool,
53+
#[serde(default)]
54+
pub postpone: bool,
55+
}
56+
57+
#[derive(Debug, Deserialize)]
58+
pub struct Team {
59+
// FIXME: The two following first fields don't seem to be used anywhere...
60+
// If this is intended, document why.
61+
name: String,
62+
ping: String,
63+
members: Vec<String>,
64+
}
65+
66+
impl Team {
67+
pub fn member_logins(&self) -> impl Iterator<Item = &str> {
68+
self.members.iter().map(|s| s.as_str())
69+
}
3170
}
3271

3372
#[derive(Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
3473
pub struct TeamLabel(pub String);
3574

36-
type TeamsFromFile = BTreeMap<String, TeamFromFile>;
37-
pub type Teams = BTreeMap<TeamLabel, Team>;
75+
impl<'de> de::Deserialize<'de> for TeamLabel {
76+
fn deserialize<D: de::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
77+
String::deserialize(de).map(TeamLabel)
78+
}
3879

39-
#[derive(Debug, Deserialize)]
40-
struct TeamFromFile {
41-
name: String,
42-
ping: String,
43-
members: Vec<String>,
80+
fn deserialize_in_place<D: de::Deserializer<'de>>(de: D, place: &mut Self)
81+
-> Result<(), D::Error>
82+
{
83+
String::deserialize_in_place(de, &mut place.0)
84+
}
85+
}
86+
87+
//==============================================================================
88+
// Implementation details
89+
//==============================================================================
90+
91+
/// Read the validated `rfcbot.toml` configuration file.
92+
fn read_rfcbot_cfg_validated() -> RfcbotConfig {
93+
let cfg = read_rfcbot_cfg();
94+
95+
cfg.teams.values().for_each(|team|
96+
team.validate()
97+
.expect("unable to verify team member from database.
98+
if you're running this for tests, make sure you've pulled github users from prod")
99+
);
100+
101+
cfg
102+
}
103+
104+
/// Read the unprocessed `rfcbot.toml` configuration file.
105+
fn read_rfcbot_cfg() -> RfcbotConfig {
106+
read_rfcbot_cfg_from(
107+
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/rfcbot.toml")))
108+
}
109+
110+
fn read_rfcbot_cfg_from(input: &str) -> RfcbotConfig {
111+
toml::from_str(input).expect("couldn't parse rfcbot.toml!")
44112
}
45113

46-
impl TeamFromFile {
47-
pub fn validate(self) -> DashResult<Team> {
48-
use domain::schema::githubuser::dsl::*;
49-
let conn = &*(DB_POOL.get()?);
50-
51-
// bail if they don't exist, but we don't want to actually keep the id in ram
52-
for member_login in &self.members {
53-
match githubuser
54-
.filter(login.eq(member_login))
55-
.first::<GitHubUser>(conn) {
56-
Ok(_) => (),
57-
Err(why) => {
58-
error!("unable to find {} in database: {:?}", member_login, why);
59-
return Err(why.into());
114+
impl Team {
115+
fn validate(&self) -> DashResult<()> {
116+
use domain::schema::githubuser::dsl::*;
117+
let conn = &*(DB_POOL.get()?);
118+
119+
// bail if they don't exist, but we don't want to actually keep the id in ram
120+
for member_login in self.member_logins() {
121+
match githubuser
122+
.filter(login.eq(member_login))
123+
.first::<GitHubUser>(conn)
124+
{
125+
Ok(_) => (),
126+
Err(why) => {
127+
error!("unable to find {} in database: {:?}", member_login, why);
128+
return Err(why.into());
129+
}
60130
}
61131
}
62-
}
63132

64-
Ok(Team {
65-
name: self.name,
66-
ping: self.ping,
67-
member_logins: self.members,
68-
})
69-
}
133+
Ok(())
134+
}
70135
}
71136

72-
pub struct Team {
73-
pub name: String,
74-
pub ping: String,
75-
pub member_logins: Vec<String>,
76-
}
137+
//==============================================================================
138+
// Tests
139+
//==============================================================================
77140

78141
#[cfg(test)]
79142
mod test {
80143
use super::*;
81144

145+
#[test]
146+
fn setup_parser_correct() {
147+
let test = r#"
148+
[fcp_behaviors]
149+
150+
[fcp_behaviors."rust-lang/alpha"]
151+
close = true
152+
postpone = true
153+
154+
[fcp_behaviors."foobar/beta"]
155+
close = false
156+
157+
[fcp_behaviors."bazquux/gamma"]
158+
postpone = false
159+
160+
[fcp_behaviors."wibble/epsilon"]
161+
162+
[teams]
163+
164+
[teams.avengers]
165+
name = "The Avengers"
166+
ping = "marvel/avengers"
167+
members = [
168+
"hulk",
169+
"thor",
170+
"thevision",
171+
"blackwidow",
172+
"spiderman",
173+
"captainamerica",
174+
]
175+
176+
[teams.justice-league]
177+
name = "Justice League of America"
178+
ping = "dc-comics/justice-league"
179+
members = [
180+
"superman",
181+
"wonderwoman",
182+
"aquaman",
183+
"batman",
184+
"theflash"
185+
]
186+
"#;
187+
let cfg = read_rfcbot_cfg_from(test);
188+
189+
// Labels are correct:
190+
assert_eq!(cfg.team_labels().map(|tl| tl.0.clone()).collect::<Vec<_>>(),
191+
vec!["avengers", "justice-league"]);
192+
193+
// Teams are correct:
194+
let map: BTreeMap<_, _> =
195+
cfg.teams().map(|(k, v)| (k.0.clone(), v.clone())).collect();
196+
197+
let avengers = map.get("avengers").unwrap();
198+
assert_eq!(avengers.name, "The Avengers");
199+
assert_eq!(avengers.ping, "marvel/avengers");
200+
assert_eq!(avengers.member_logins().collect::<Vec<_>>(),
201+
vec!["hulk", "thor", "thevision", "blackwidow",
202+
"spiderman", "captainamerica"]);
203+
204+
let jsa = map.get("justice-league").unwrap();
205+
assert_eq!(jsa.name, "Justice League of America");
206+
assert_eq!(jsa.ping, "dc-comics/justice-league");
207+
assert_eq!(jsa.member_logins().collect::<Vec<_>>(),
208+
vec!["superman", "wonderwoman", "aquaman", "batman", "theflash"]);
209+
210+
// FFCP behavior correct:
211+
assert!(cfg.ffcp_auto_close("rust-lang/alpha"));
212+
assert!(cfg.ffcp_auto_postpone("rust-lang/alpha"));
213+
assert!(!cfg.ffcp_auto_close("foobar/beta"));
214+
assert!(!cfg.ffcp_auto_postpone("foobar/beta"));
215+
assert!(!cfg.ffcp_auto_close("bazquux/gamma"));
216+
assert!(!cfg.ffcp_auto_postpone("bazquux/gamma"));
217+
assert!(!cfg.ffcp_auto_close("wibble/epsilon"));
218+
assert!(!cfg.ffcp_auto_postpone("wibble/epsilon"));
219+
assert!(!cfg.ffcp_auto_close("random"));
220+
assert!(!cfg.ffcp_auto_postpone("random"));
221+
}
222+
223+
#[test]
224+
fn cfg_file_wellformed() {
225+
// Just parse it and ensure that we get no panics for now!
226+
// This is a crap test; but, better than nothing.
227+
let _ = read_rfcbot_cfg();
228+
}
229+
82230
#[test]
83231
fn team_members_exist() {
84-
for (label, team) in TEAMS.iter() {
232+
for (label, _) in SETUP.teams.iter() {
85233
println!("found team {:?}", label);
86234
}
87235
}

0 commit comments

Comments
 (0)