Skip to content

Commit edcbb6c

Browse files
committed
Add mentions.
1 parent 8252d63 commit edcbb6c

File tree

7 files changed

+191
-1
lines changed

7 files changed

+191
-1
lines changed

Cargo.lock

Lines changed: 2 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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
2929
url = "2.1.0"
3030
once_cell = "1"
3131
chrono = { version = "0.4", features = ["serde"] }
32-
tokio-postgres = { version = "0.7.2", features = ["with-chrono-0_4"] }
32+
tokio-postgres = { version = "0.7.2", features = ["with-chrono-0_4", "with-serde_json-1"] }
3333
postgres-native-tls = "0.5.0"
3434
native-tls = "0.2"
3535
serde_path_to_error = "0.1.2"

src/config.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ pub(crate) struct Config {
3232
pub(crate) review_submitted: Option<ReviewSubmittedConfig>,
3333
pub(crate) shortcut: Option<ShortcutConfig>,
3434
pub(crate) note: Option<NoteConfig>,
35+
pub(crate) mentions: Option<MentionsConfig>,
3536
}
3637

3738
#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
@@ -84,6 +85,19 @@ pub(crate) struct NoteConfig {
8485
_empty: (),
8586
}
8687

88+
#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
89+
pub(crate) struct MentionsConfig {
90+
#[serde(flatten)]
91+
pub(crate) paths: HashMap<String, MentionsPathConfig>,
92+
}
93+
94+
#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
95+
pub(crate) struct MentionsPathConfig {
96+
pub(crate) message: Option<String>,
97+
#[serde(default)]
98+
pub(crate) reviewers: Vec<String>,
99+
}
100+
87101
#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
88102
#[serde(rename_all = "kebab-case")]
89103
pub(crate) struct RelabelConfig {
@@ -350,6 +364,7 @@ mod tests {
350364
notify_zulip: None,
351365
github_releases: None,
352366
review_submitted: None,
367+
mentions: None,
353368
}
354369
);
355370
}

src/db.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use std::sync::{Arc, Mutex};
55
use tokio::sync::{OwnedSemaphorePermit, Semaphore};
66
use tokio_postgres::Client as DbClient;
77

8+
pub mod issue_data;
89
pub mod notifications;
910
pub mod rustc_commits;
1011

@@ -206,4 +207,13 @@ CREATE TABLE rustc_commits (
206207
);
207208
",
208209
"ALTER TABLE rustc_commits ADD COLUMN pr INTEGER;",
210+
"
211+
CREATE TABLE issue_data (
212+
repo TEXT,
213+
issue_number INTEGER,
214+
key TEXT,
215+
data JSONB,
216+
PRIMARY KEY (repo, issue_number, key)
217+
);
218+
",
209219
];

src/db/issue_data.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//! The `issue_data` table provides a way to track extra metadata about an
2+
//! issue/PR.
3+
//!
4+
//! Each issue has a unique "key" where you can store data under. Typically
5+
//! that key should be the name of the handler. The data can be anything that
6+
//! can be serialized to JSON.
7+
8+
use crate::github::Issue;
9+
use anyhow::{Context, Result};
10+
use serde::{Deserialize, Serialize};
11+
use tokio_postgres::types::Json;
12+
use tokio_postgres::Client as DbClient;
13+
14+
pub async fn load<T: for<'a> Deserialize<'a>>(
15+
db: &DbClient,
16+
issue: &Issue,
17+
key: &str,
18+
) -> Result<Option<T>> {
19+
let repo = issue.repository().to_string();
20+
let data = db
21+
.query_opt(
22+
"SELECT data FROM issue_data WHERE \
23+
repo = $1 AND issue_number = $2 AND key = $3",
24+
&[&repo, &(issue.number as i32), &key],
25+
)
26+
.await
27+
.context("selecting issue data")?
28+
.map(|row| row.get::<usize, Json<T>>(0).0);
29+
Ok(data)
30+
}
31+
32+
pub async fn save<T: Serialize + std::fmt::Debug + Sync>(
33+
db: &DbClient,
34+
issue: &Issue,
35+
key: &str,
36+
data: &T,
37+
) -> Result<()> {
38+
let repo = issue.repository().to_string();
39+
db.execute(
40+
"INSERT INTO issue_data (repo, issue_number, key, data) VALUES ($1, $2, $3, $4) \
41+
ON CONFLICT (repo, issue_number, key) DO UPDATE SET data=EXCLUDED.data",
42+
&[&repo, &(issue.number as i32), &key, &Json(data)],
43+
)
44+
.await
45+
.context("inserting issue data")?;
46+
Ok(())
47+
}

src/handlers.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ mod close;
2929
mod github_releases;
3030
mod glacier;
3131
mod major_change;
32+
mod mentions;
3233
mod milestone_prs;
3334
mod nominate;
3435
mod note;
@@ -153,6 +154,7 @@ macro_rules! issue_handlers {
153154
issue_handlers! {
154155
autolabel,
155156
major_change,
157+
mentions,
156158
notify_zulip,
157159
}
158160

src/handlers/mentions.rs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
//! Purpose: When opening a PR, or pushing new changes, check for any paths
2+
//! that are in the `mentions` config, and add a comment that pings the listed
3+
//! interested people.
4+
5+
use crate::{
6+
config::{MentionsConfig, MentionsPathConfig},
7+
db::issue_data,
8+
github::{files_changed, IssuesAction, IssuesEvent},
9+
handlers::Context,
10+
};
11+
use anyhow::Context as _;
12+
use serde::{Deserialize, Serialize};
13+
use std::fmt::Write;
14+
use std::path::Path;
15+
use tracing as log;
16+
17+
const MENTIONS_KEY: &str = "mentions";
18+
19+
pub(super) struct MentionsInput {
20+
paths: Vec<String>,
21+
}
22+
23+
#[derive(Debug, Default, Deserialize, Serialize)]
24+
struct MentionState {
25+
paths: Vec<String>,
26+
}
27+
28+
pub(super) async fn parse_input(
29+
ctx: &Context,
30+
event: &IssuesEvent,
31+
config: Option<&MentionsConfig>,
32+
) -> Result<Option<MentionsInput>, String> {
33+
let config = match config {
34+
Some(config) => config,
35+
None => return Ok(None),
36+
};
37+
38+
if !matches!(
39+
event.action,
40+
IssuesAction::Opened | IssuesAction::Synchronize
41+
) {
42+
return Ok(None);
43+
}
44+
45+
if let Some(diff) = event
46+
.issue
47+
.diff(&ctx.github)
48+
.await
49+
.map_err(|e| {
50+
log::error!("failed to fetch diff: {:?}", e);
51+
})
52+
.unwrap_or_default()
53+
{
54+
let files = files_changed(&diff);
55+
let file_paths: Vec<_> = files.iter().map(|p| Path::new(p)).collect();
56+
let to_mention: Vec<_> = config
57+
.paths
58+
.iter()
59+
// Only mention matching paths.
60+
// Don't mention if the author is in the list.
61+
.filter(|(path, MentionsPathConfig { reviewers, .. })| {
62+
let path = Path::new(path);
63+
file_paths.iter().any(|p| p.starts_with(path))
64+
&& !reviewers.iter().any(|r| r == &event.issue.user.login)
65+
})
66+
.map(|(key, _mention)| key.to_string())
67+
.collect();
68+
if !to_mention.is_empty() {
69+
return Ok(Some(MentionsInput { paths: to_mention }));
70+
}
71+
}
72+
Ok(None)
73+
}
74+
75+
pub(super) async fn handle_input(
76+
ctx: &Context,
77+
config: &MentionsConfig,
78+
event: &IssuesEvent,
79+
input: MentionsInput,
80+
) -> anyhow::Result<()> {
81+
let client = ctx.db.get().await;
82+
let mut state: MentionState = issue_data::load(&client, &event.issue, MENTIONS_KEY)
83+
.await?
84+
.unwrap_or_default();
85+
// Build the message to post to the issue.
86+
let mut result = String::new();
87+
for to_mention in &input.paths {
88+
if state.paths.iter().any(|p| p == to_mention) {
89+
// Avoid duplicate mentions.
90+
continue;
91+
}
92+
let MentionsPathConfig { message, reviewers } = &config.paths[to_mention];
93+
if !result.is_empty() {
94+
result.push_str("\n\n");
95+
}
96+
match message {
97+
Some(m) => result.push_str(m),
98+
None => write!(result, "Some changes occurred in {to_mention}").unwrap(),
99+
}
100+
if !reviewers.is_empty() {
101+
write!(result, "\n\ncc {}", reviewers.join(",")).unwrap();
102+
}
103+
state.paths.push(to_mention.to_string());
104+
}
105+
if !result.is_empty() {
106+
event
107+
.issue
108+
.post_comment(&ctx.github, &result)
109+
.await
110+
.context("failed to post mentions comment")?;
111+
issue_data::save(&client, &event.issue, MENTIONS_KEY, &state).await?;
112+
}
113+
Ok(())
114+
}

0 commit comments

Comments
 (0)