Skip to content

Commit 0f09c95

Browse files
authored
Merge pull request #9 from rust-lang/orgwide
Add per-repo config and prepare for org-wide webhook
2 parents c6dcf9f + 5496e8f commit 0f09c95

File tree

10 files changed

+274
-166
lines changed

10 files changed

+274
-166
lines changed

Cargo.lock

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ hex = "0.3.2"
2121
env_logger = "0.6"
2222
parser = { path = "parser" }
2323
rust_team_data = { git = "https://github.com/rust-lang/team" }
24+
glob = "0.3.0"
25+
toml = "0.5.0"
2426

2527
[dependencies.serde]
2628
version = "1"

parser/src/command.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ use crate::code_block::ColorCodeBlocks;
22
use crate::error::Error;
33
use crate::token::{Token, Tokenizer};
44

5-
pub mod label;
5+
pub mod relabel;
66

77
pub fn find_commmand_start(input: &str, bot: &str) -> Option<usize> {
88
input.find(&format!("@{}", bot))
99
}
1010

1111
#[derive(Debug)]
1212
pub enum Command<'a> {
13-
Label(Result<label::LabelCommand, Error<'a>>),
13+
Relabel(Result<relabel::RelabelCommand, Error<'a>>),
1414
None,
1515
}
1616

@@ -50,14 +50,14 @@ impl<'a> Input<'a> {
5050

5151
{
5252
let mut tok = original_tokenizer.clone();
53-
let res = label::LabelCommand::parse(&mut tok);
53+
let res = relabel::RelabelCommand::parse(&mut tok);
5454
match res {
5555
Ok(None) => {}
5656
Ok(Some(cmd)) => {
57-
success.push((tok, Command::Label(Ok(cmd))));
57+
success.push((tok, Command::Relabel(Ok(cmd))));
5858
}
5959
Err(err) => {
60-
success.push((tok, Command::Label(Err(err))));
60+
success.push((tok, Command::Relabel(Err(err))));
6161
}
6262
}
6363
}
@@ -94,7 +94,7 @@ impl<'a> Input<'a> {
9494
impl<'a> Command<'a> {
9595
pub fn is_ok(&self) -> bool {
9696
match self {
97-
Command::Label(r) => r.is_ok(),
97+
Command::Relabel(r) => r.is_ok(),
9898
Command::None => true,
9999
}
100100
}

parser/src/command/label.rs renamed to parser/src/command/relabel.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ use std::error::Error as _;
3030
use std::fmt;
3131

3232
#[derive(Debug)]
33-
pub struct LabelCommand(pub Vec<LabelDelta>);
33+
pub struct RelabelCommand(pub Vec<LabelDelta>);
3434

3535
#[derive(Debug, PartialEq, Eq)]
3636
pub enum LabelDelta {
@@ -124,7 +124,7 @@ fn delta_empty() {
124124
assert_eq!(err.position(), 1);
125125
}
126126

127-
impl LabelCommand {
127+
impl RelabelCommand {
128128
pub fn parse<'a>(input: &mut Tokenizer<'a>) -> Result<Option<Self>, Error<'a>> {
129129
let mut toks = input.clone();
130130
if let Some(Token::Word("modify")) = toks.next_token()? {
@@ -163,7 +163,7 @@ impl LabelCommand {
163163
if let Some(Token::Dot) | Some(Token::EndOfLine) = toks.peek_token()? {
164164
toks.next_token()?;
165165
*input = toks;
166-
return Ok(Some(LabelCommand(deltas)));
166+
return Ok(Some(RelabelCommand(deltas)));
167167
}
168168
}
169169
}
@@ -172,7 +172,7 @@ impl LabelCommand {
172172
#[cfg(test)]
173173
fn parse<'a>(input: &'a str) -> Result<Option<Vec<LabelDelta>>, Error<'a>> {
174174
let mut toks = Tokenizer::new(input);
175-
Ok(LabelCommand::parse(&mut toks)?.map(|c| c.0))
175+
Ok(RelabelCommand::parse(&mut toks)?.map(|c| c.0))
176176
}
177177

178178
#[test]

src/config.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
use crate::github::GithubClient;
2+
use failure::Error;
3+
use std::collections::HashMap;
4+
use std::sync::{Arc, RwLock};
5+
use std::time::{Duration, Instant};
6+
7+
static CONFIG_FILE_NAME: &str = "triagebot.toml";
8+
const REFRESH_EVERY: Duration = Duration::from_secs(2 * 60); // Every two minutes
9+
10+
lazy_static::lazy_static! {
11+
static ref CONFIG_CACHE: RwLock<HashMap<String, (Arc<Config>, Instant)>> =
12+
RwLock::new(HashMap::new());
13+
}
14+
15+
#[derive(serde::Deserialize)]
16+
pub(crate) struct Config {
17+
pub(crate) relabel: Option<RelabelConfig>,
18+
}
19+
20+
#[derive(serde::Deserialize)]
21+
#[serde(rename_all = "kebab-case")]
22+
pub(crate) struct RelabelConfig {
23+
#[serde(default)]
24+
pub(crate) allow_unauthenticated: Vec<String>,
25+
}
26+
27+
pub(crate) fn get(gh: &GithubClient, repo: &str) -> Result<Arc<Config>, Error> {
28+
if let Some(config) = get_cached_config(repo) {
29+
Ok(config)
30+
} else {
31+
get_fresh_config(gh, repo)
32+
}
33+
}
34+
35+
fn get_cached_config(repo: &str) -> Option<Arc<Config>> {
36+
let cache = CONFIG_CACHE.read().unwrap();
37+
cache.get(repo).and_then(|(config, fetch_time)| {
38+
if fetch_time.elapsed() < REFRESH_EVERY {
39+
Some(config.clone())
40+
} else {
41+
None
42+
}
43+
})
44+
}
45+
46+
fn get_fresh_config(gh: &GithubClient, repo: &str) -> Result<Arc<Config>, Error> {
47+
let contents = gh
48+
.raw_file(repo, "master", CONFIG_FILE_NAME)?
49+
.ok_or_else(|| {
50+
failure::err_msg(
51+
"This repository is not enabled to use triagebot.\n\
52+
Add a `triagebot.toml` in the root of the master branch to enable it.",
53+
)
54+
})?;
55+
let config = Arc::new(toml::from_slice::<Config>(&contents)?);
56+
CONFIG_CACHE
57+
.write()
58+
.unwrap()
59+
.insert(repo.to_string(), (config.clone(), Instant::now()));
60+
Ok(config)
61+
}

src/github.rs

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use failure::{Error, ResultExt};
22
use reqwest::header::{AUTHORIZATION, USER_AGENT};
3-
use reqwest::{Client, Error as HttpError, RequestBuilder, Response};
3+
use reqwest::{Client, Error as HttpError, RequestBuilder, Response, StatusCode};
4+
use std::io::Read;
45

56
#[derive(Debug, serde::Deserialize)]
67
pub struct User {
@@ -149,6 +150,46 @@ impl Issue {
149150
}
150151
}
151152

153+
#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
154+
#[serde(rename_all = "lowercase")]
155+
pub enum IssueCommentAction {
156+
Created,
157+
Edited,
158+
Deleted,
159+
}
160+
161+
#[derive(Debug, serde::Deserialize)]
162+
pub struct IssueCommentEvent {
163+
pub action: IssueCommentAction,
164+
pub issue: Issue,
165+
pub comment: Comment,
166+
pub repository: Repository,
167+
}
168+
169+
#[derive(Debug, serde::Deserialize)]
170+
pub struct Repository {
171+
pub full_name: String,
172+
}
173+
174+
#[derive(Debug)]
175+
pub enum Event {
176+
IssueComment(IssueCommentEvent),
177+
}
178+
179+
impl Event {
180+
pub fn repo_name(&self) -> &str {
181+
match self {
182+
Event::IssueComment(event) => &event.repository.full_name,
183+
}
184+
}
185+
186+
pub fn issue(&self) -> Option<&Issue> {
187+
match self {
188+
Event::IssueComment(event) => Some(&event.issue),
189+
}
190+
}
191+
}
192+
152193
trait RequestSend: Sized {
153194
fn configure(self, g: &GithubClient) -> Self;
154195
fn send_req(self) -> Result<Response, HttpError>;
@@ -183,6 +224,23 @@ impl GithubClient {
183224
&self.client
184225
}
185226

227+
pub fn raw_file(&self, repo: &str, branch: &str, path: &str) -> Result<Option<Vec<u8>>, Error> {
228+
let url = format!(
229+
"https://raw.githubusercontent.com/{}/{}/{}",
230+
repo, branch, path
231+
);
232+
let mut resp = self.get(&url).send()?;
233+
match resp.status() {
234+
StatusCode::OK => {
235+
let mut buf = Vec::with_capacity(resp.content_length().unwrap_or(4) as usize);
236+
resp.read_to_end(&mut buf)?;
237+
Ok(Some(buf))
238+
}
239+
StatusCode::NOT_FOUND => Ok(None),
240+
status => failure::bail!("failed to GET {}: {}", url, status),
241+
}
242+
}
243+
186244
fn get(&self, url: &str) -> RequestBuilder {
187245
log::trace!("get {:?}", url);
188246
self.client.get(url).configure(self)

src/handlers.rs

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,51 @@
1-
use crate::github::GithubClient;
2-
use crate::registry::HandleRegistry;
3-
use std::sync::Arc;
1+
use crate::github::{Event, GithubClient};
2+
use failure::Error;
43

5-
//mod assign;
6-
mod label;
7-
//mod tracking_issue;
4+
macro_rules! handlers {
5+
($($name:ident = $handler:expr,)*) => {
6+
$(mod $name;)*
87

9-
pub fn register_all(registry: &mut HandleRegistry, client: GithubClient, username: Arc<String>) {
10-
registry.register(label::LabelHandler {
11-
client: client.clone(),
12-
username: username.clone(),
13-
});
14-
//registry.register(assign::AssignmentHandler {
15-
// client: client.clone(),
16-
//});
17-
//registry.register(tracking_issue::TrackingIssueHandler {
18-
// client: client.clone(),
19-
//});
8+
pub fn handle(ctx: &Context, event: &Event) -> Result<(), Error> {
9+
$(if let Some(input) = Handler::parse_input(&$handler, ctx, event)? {
10+
let config = crate::config::get(&ctx.github, event.repo_name())?;
11+
if let Some(config) = &config.$name {
12+
Handler::handle_input(&$handler, ctx, config, event, input)?;
13+
} else {
14+
failure::bail!(
15+
"The feature `{}` is not enabled in this repository.\n\
16+
To enable it add its section in the `triagebot.toml` \
17+
in the root of the repository.",
18+
stringify!($name)
19+
);
20+
}
21+
})*
22+
Ok(())
23+
}
24+
}
25+
}
26+
27+
handlers! {
28+
//assign = assign::AssignmentHandler,
29+
relabel = relabel::RelabelHandler,
30+
//tracking_issue = tracking_issue::TrackingIssueHandler,
31+
}
32+
33+
pub struct Context {
34+
pub github: GithubClient,
35+
pub username: String,
36+
}
37+
38+
pub trait Handler: Sync + Send {
39+
type Input;
40+
type Config;
41+
42+
fn parse_input(&self, ctx: &Context, event: &Event) -> Result<Option<Self::Input>, Error>;
43+
44+
fn handle_input(
45+
&self,
46+
ctx: &Context,
47+
config: &Self::Config,
48+
event: &Event,
49+
input: Self::Input,
50+
) -> Result<(), Error>;
2051
}

0 commit comments

Comments
 (0)