Skip to content

Commit 7e3f5d3

Browse files
committed
Add handler for concern/resolve commands
1 parent 783d2bf commit 7e3f5d3

File tree

3 files changed

+241
-0
lines changed

3 files changed

+241
-0
lines changed

src/config.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ pub(crate) struct Config {
3535
pub(crate) review_requested: Option<ReviewRequestedConfig>,
3636
pub(crate) shortcut: Option<ShortcutConfig>,
3737
pub(crate) note: Option<NoteConfig>,
38+
pub(crate) concern: Option<ConcernConfig>,
3839
pub(crate) mentions: Option<MentionsConfig>,
3940
pub(crate) no_merges: Option<NoMergesConfig>,
4041
// We want this validation to run even without the entry in the config file
@@ -191,6 +192,14 @@ pub(crate) struct NoteConfig {
191192
_empty: (),
192193
}
193194

195+
#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
196+
#[serde(deny_unknown_fields)]
197+
pub(crate) struct ConcernConfig {
198+
/// Set the labels on the PR when concerns are active.
199+
#[serde(default)]
200+
pub(crate) labels: Vec<String>,
201+
}
202+
194203
#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
195204
pub(crate) struct MentionsConfig {
196205
#[serde(flatten)]
@@ -594,6 +603,9 @@ mod tests {
594603
595604
[note]
596605
606+
[concern]
607+
labels = ["has-concerns"]
608+
597609
[ping.compiler]
598610
message = """\
599611
So many people!\
@@ -691,6 +703,9 @@ mod tests {
691703
behind_upstream: Some(BehindUpstreamConfig {
692704
days_threshold: Some(14),
693705
}),
706+
concern: Some(ConcernConfig {
707+
labels: vec!["has-concerns".to_string()],
708+
}),
694709
}
695710
);
696711
}
@@ -742,6 +757,7 @@ mod tests {
742757
}),
743758
note: None,
744759
ping: None,
760+
concern: None,
745761
nominate: None,
746762
shortcut: None,
747763
prioritize: None,

src/handlers.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ mod autolabel;
2929
mod bot_pull_requests;
3030
mod check_commits;
3131
mod close;
32+
mod concern;
3233
pub mod docs_update;
3334
mod github_releases;
3435
mod issue_links;
@@ -365,6 +366,7 @@ command_handlers! {
365366
shortcut: Shortcut,
366367
close: Close,
367368
note: Note,
369+
concern: Concern,
368370
transfer: Transfer,
369371
}
370372

src/handlers/concern.rs

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
use std::fmt::Write;
2+
3+
use anyhow::{bail, Context as _};
4+
use octocrab::models::AuthorAssociation;
5+
6+
use crate::{
7+
config::ConcernConfig,
8+
github::{Event, Label},
9+
handlers::Context,
10+
interactions::EditIssueBody,
11+
};
12+
use parser::command::concern::ConcernCommand;
13+
14+
const CONCERN_ISSUE_KEY: &str = "CONCERN-ISSUE";
15+
16+
#[derive(Debug, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
17+
struct ConcernData {
18+
concerns: Vec<Concern>,
19+
}
20+
21+
#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
22+
struct Concern {
23+
title: String,
24+
author: String,
25+
comment_url: String,
26+
status: ConcernStatus,
27+
}
28+
29+
#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
30+
enum ConcernStatus {
31+
Active,
32+
Resolved { comment_url: String },
33+
}
34+
35+
pub(super) async fn handle_command(
36+
ctx: &Context,
37+
config: &ConcernConfig,
38+
event: &Event,
39+
cmd: ConcernCommand,
40+
) -> anyhow::Result<()> {
41+
let Event::IssueComment(issue_comment) = event else {
42+
bail!("concern issued on something other than a issue")
43+
};
44+
let Some(comment_url) = event.html_url() else {
45+
bail!("unable to retrieve the comment url")
46+
};
47+
let author = event.user().login.to_owned();
48+
let issue = &issue_comment.issue;
49+
50+
// Verify that this issue isn't a rfcbot FCP, skip if it is
51+
match crate::rfcbot::get_all_fcps().await {
52+
Ok(fcps) => {
53+
if fcps.iter().any(|(_, fcp)| {
54+
fcp.issue.number as u64 == issue.number
55+
&& fcp.issue.repository == issue_comment.repository.full_name
56+
}) {
57+
tracing::info!(
58+
"{}#{} tried to register a concern, blocked by our rfcbot FCP check",
59+
issue_comment.repository.full_name,
60+
issue.number,
61+
);
62+
return Ok(());
63+
}
64+
}
65+
Err(err) => {
66+
tracing::warn!(
67+
"unable to fetch rfcbot active FCPs: {:?}, skipping check",
68+
err
69+
);
70+
}
71+
}
72+
73+
// Verify that the comment author is at least a member of the org, error if it's not
74+
if !matches!(
75+
issue_comment.comment.author_association,
76+
AuthorAssociation::Member | AuthorAssociation::Owner
77+
) {
78+
issue
79+
.post_comment(
80+
&ctx.github,
81+
"Only organization members can add or resolve concerns.",
82+
)
83+
.await?;
84+
return Ok(());
85+
}
86+
87+
let edit = EditIssueBody::new(&issue, CONCERN_ISSUE_KEY);
88+
let mut concern_data: ConcernData = edit.current_data().unwrap_or_default();
89+
90+
// Process the command by either adding a new comment or "deactivating" the old one
91+
match cmd {
92+
ConcernCommand::Concern { title } => concern_data.concerns.push(Concern {
93+
title,
94+
author,
95+
status: ConcernStatus::Active,
96+
comment_url: comment_url.to_string(),
97+
}),
98+
ConcernCommand::Resolve { title } => concern_data
99+
.concerns
100+
.iter_mut()
101+
.filter(|c| c.title == title)
102+
.for_each(|c| {
103+
c.status = ConcernStatus::Resolved {
104+
comment_url: comment_url.to_string(),
105+
}
106+
}),
107+
}
108+
109+
// Create the new markdown content listing all the concerns
110+
let new_content = markdown_content(&concern_data.concerns);
111+
112+
// Add or remove the labels
113+
if concern_data
114+
.concerns
115+
.iter()
116+
.any(|c| matches!(c.status, ConcernStatus::Active))
117+
{
118+
if let Err(err) = issue
119+
.add_labels(
120+
&ctx.github,
121+
config
122+
.labels
123+
.iter()
124+
.map(|l| Label {
125+
name: l.to_string(),
126+
})
127+
.collect(),
128+
)
129+
.await
130+
{
131+
tracing::error!("unable to add concern labels: {:?}", err);
132+
let labels = config.labels.join(", ");
133+
issue.post_comment(
134+
&ctx.github,
135+
&format!("*Psst, I was unable to add the labels ({labels}), could someone do it for me.*"),
136+
).await?;
137+
}
138+
} else {
139+
for l in &config.labels {
140+
issue.remove_label(&ctx.github, &l).await?;
141+
}
142+
}
143+
144+
// Apply the new markdown concerns list to the issue
145+
edit.apply(&ctx.github, new_content, concern_data)
146+
.await
147+
.context("failed to apply the new concerns section markdown")?;
148+
149+
Ok(())
150+
}
151+
152+
fn markdown_content(concerns: &[Concern]) -> String {
153+
if concerns.is_empty() {
154+
return "".to_string();
155+
}
156+
157+
let mut md = String::new();
158+
159+
let _ = writeln!(md, "\n# Concerns");
160+
let _ = writeln!(md, "");
161+
162+
for &Concern {
163+
ref title,
164+
ref author,
165+
ref status,
166+
ref comment_url,
167+
} in concerns
168+
{
169+
let _ = match status {
170+
ConcernStatus::Active => {
171+
writeln!(
172+
md,
173+
" - [{title}]({comment_url}) by [{author}](https://github.com/{author})"
174+
)
175+
}
176+
ConcernStatus::Resolved {
177+
comment_url: resolved_comment_url,
178+
} => {
179+
writeln!(
180+
md,
181+
" - ~~[{title}]({comment_url}) by [{author}](https://github.com/{author})~~ resolved [in this comment]({resolved_comment_url})"
182+
)
183+
}
184+
};
185+
}
186+
187+
let _ = writeln!(md, "");
188+
let _ = writeln!(md, "Generated by triagebot, see [help](https://forge.rust-lang.org/triagebot/concern.html) for how to use them.");
189+
190+
md
191+
}
192+
193+
#[test]
194+
fn simple_markdown_content() {
195+
let concerns = &[
196+
Concern {
197+
title: "This is my concern about concern".to_string(),
198+
author: "Urgau".to_string(),
199+
status: ConcernStatus::Active,
200+
comment_url: "https://github.com/fake-comment-1234".to_string(),
201+
},
202+
Concern {
203+
title: "This is a resolved concern".to_string(),
204+
author: "Kobzol".to_string(),
205+
status: ConcernStatus::Resolved {
206+
comment_url: "https:://github.com/fake-comment-8888".to_string(),
207+
},
208+
comment_url: "https://github.com/fake-comment-4561".to_string(),
209+
},
210+
];
211+
212+
assert_eq!(
213+
markdown_content(concerns),
214+
r#"
215+
# Concerns
216+
217+
- [This is my concern about concern](https://github.com/fake-comment-1234) by [Urgau](https://github.com/Urgau)
218+
- ~~[This is a resolved concern](https://github.com/fake-comment-4561) by [Kobzol](https://github.com/Kobzol)~~ resolved [in this comment](https:://github.com/fake-comment-8888)
219+
220+
Generated by triagebot, see [help](https://forge.rust-lang.org/triagebot/concern.html) for how to use them.
221+
"#
222+
);
223+
}

0 commit comments

Comments
 (0)