Skip to content

Commit a8013bb

Browse files
authored
Merge pull request #2084 from Urgau/move-validate_config-to-check_commits
Move `triagebot.toml` validation handler to check commits super-handler
2 parents 6460f78 + adf7cdb commit a8013bb

File tree

5 files changed

+158
-151
lines changed

5 files changed

+158
-151
lines changed

src/config.rs

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,6 @@ pub(crate) struct Config {
3838
pub(crate) concern: Option<ConcernConfig>,
3939
pub(crate) mentions: Option<MentionsConfig>,
4040
pub(crate) no_merges: Option<NoMergesConfig>,
41-
// We want this validation to run even without the entry in the config file
42-
#[serde(default = "ValidateConfig::default")]
43-
pub(crate) validate_config: Option<ValidateConfig>,
4441
pub(crate) pr_tracking: Option<ReviewPrefsConfig>,
4542
pub(crate) transfer: Option<TransferConfig>,
4643
pub(crate) merge_conflicts: Option<MergeConflictConfig>,
@@ -254,15 +251,6 @@ pub(crate) struct PrioritizeConfig {
254251
pub(crate) label: String,
255252
}
256253

257-
#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
258-
pub(crate) struct ValidateConfig {}
259-
260-
impl ValidateConfig {
261-
fn default() -> Option<Self> {
262-
Some(ValidateConfig {})
263-
}
264-
}
265-
266254
#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
267255
pub(crate) struct AutolabelConfig {
268256
#[serde(flatten)]
@@ -722,7 +710,6 @@ mod tests {
722710
review_requested: None,
723711
mentions: None,
724712
no_merges: None,
725-
validate_config: Some(ValidateConfig {}),
726713
pr_tracking: None,
727714
transfer: None,
728715
merge_conflicts: None,
@@ -813,7 +800,6 @@ mod tests {
813800
review_requested: None,
814801
mentions: None,
815802
no_merges: None,
816-
validate_config: Some(ValidateConfig {}),
817803
pr_tracking: None,
818804
transfer: None,
819805
merge_conflicts: None,

src/handlers.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ pub mod rustc_commits;
5757
mod shortcut;
5858
mod transfer;
5959
pub mod types_planning_updates;
60-
mod validate_config;
6160

6261
pub async fn handle(ctx: &Context, event: &Event) -> Vec<HandlerError> {
6362
let config = config::get(&ctx.github, event.repo()).await;
@@ -232,7 +231,6 @@ issue_handlers! {
232231
notify_zulip,
233232
review_requested,
234233
pr_tracking,
235-
validate_config,
236234
}
237235

238236
macro_rules! command_handlers {

src/handlers/check_commits.rs

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,17 @@ mod modified_submodule;
1919
mod no_mentions;
2020
mod no_merges;
2121
mod non_default_branch;
22+
mod validate_config;
2223

2324
/// Key for the state in the database
24-
const CHECK_COMMITS_WARNINGS_KEY: &str = "check-commits-warnings";
25+
const CHECK_COMMITS_KEY: &str = "check-commits-warnings";
2526

2627
/// State stored in the database
2728
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, Clone, PartialEq)]
28-
struct CheckCommitsWarningsState {
29+
struct CheckCommitsState {
30+
/// List of the last errors (comment body, comment node-id).
31+
#[serde(default)]
32+
last_errors: Vec<(String, String)>,
2933
/// List of the last warnings in the most recent comment.
3034
last_warnings: Vec<String>,
3135
/// ID of the most recent warning comment.
@@ -41,7 +45,10 @@ pub(super) async fn handle(ctx: &Context, event: &Event, config: &Config) -> any
4145

4246
if !matches!(
4347
event.action,
44-
IssuesAction::Opened | IssuesAction::Synchronize | IssuesAction::ReadyForReview
48+
IssuesAction::Opened
49+
| IssuesAction::Reopened
50+
| IssuesAction::Synchronize
51+
| IssuesAction::ReadyForReview
4552
) || !event.issue.is_pr()
4653
{
4754
return Ok(());
@@ -61,6 +68,7 @@ pub(super) async fn handle(ctx: &Context, event: &Event, config: &Config) -> any
6168
let commits = event.issue.commits(&ctx.github).await?;
6269
let diff = &compare.files;
6370

71+
let mut errors = Vec::new();
6472
let mut warnings = Vec::new();
6573
let mut labels = Vec::new();
6674

@@ -108,20 +116,51 @@ pub(super) async fn handle(ctx: &Context, event: &Event, config: &Config) -> any
108116
}
109117
}
110118

111-
handle_warnings_and_labels(ctx, event, warnings, labels).await
119+
// Check if the `triagebot.toml` config is valid
120+
errors.extend(
121+
validate_config::validate_config(ctx, &event, diff)
122+
.await
123+
.context("validating the the triagebot config")?,
124+
);
125+
126+
handle_new_state(ctx, event, errors, warnings, labels).await
112127
}
113128

114129
// Add, hide or hide&add a comment with the warnings.
115-
async fn handle_warnings_and_labels(
130+
async fn handle_new_state(
116131
ctx: &Context,
117132
event: &IssuesEvent,
133+
errors: Vec<String>,
118134
warnings: Vec<String>,
119135
labels: Vec<String>,
120136
) -> anyhow::Result<()> {
121137
// Get the state of the warnings for this PR in the database.
122138
let mut db = ctx.db.get().await;
123-
let mut state: IssueData<'_, CheckCommitsWarningsState> =
124-
IssueData::load(&mut db, &event.issue, CHECK_COMMITS_WARNINGS_KEY).await?;
139+
let mut state: IssueData<'_, CheckCommitsState> =
140+
IssueData::load(&mut db, &event.issue, CHECK_COMMITS_KEY).await?;
141+
142+
// Handles the errors, post the new ones, hide resolved ones and don't touch the one still active
143+
if !state.data.last_errors.is_empty() || !errors.is_empty() {
144+
let (errors_to_remove, errors_to_add) =
145+
calculate_error_changes(&state.data.last_errors, &errors);
146+
147+
for error_to_remove in errors_to_remove {
148+
event
149+
.issue
150+
.hide_comment(
151+
&ctx.github,
152+
&error_to_remove.1,
153+
ReportedContentClassifiers::Resolved,
154+
)
155+
.await?;
156+
state.data.last_errors.retain(|e| e != &error_to_remove);
157+
}
158+
159+
for error_to_add in errors_to_add {
160+
let comment = event.issue.post_comment(&ctx.github, &error_to_add).await?;
161+
state.data.last_errors.push((error_to_add, comment.node_id));
162+
}
163+
}
125164

126165
// We only post a new comment when we haven't posted one with the same warnings before.
127166
if !warnings.is_empty() && state.data.last_warnings != warnings {
@@ -225,6 +264,28 @@ fn calculate_label_changes(
225264
(removals, additions)
226265
}
227266

267+
// Calculate the error changes
268+
fn calculate_error_changes(
269+
previous: &Vec<(String, String)>,
270+
current: &Vec<String>,
271+
) -> (Vec<(String, String)>, Vec<String>) {
272+
let previous_set: HashSet<(String, String)> = previous.into_iter().cloned().collect();
273+
let current_set: HashSet<String> = current.into_iter().cloned().collect();
274+
275+
let removals = previous_set
276+
.iter()
277+
.filter(|(e, _)| !current_set.contains(e))
278+
.cloned()
279+
.collect();
280+
let additions = current_set
281+
.iter()
282+
.filter(|e| !previous_set.iter().any(|(e2, _)| e == &e2))
283+
.cloned()
284+
.collect();
285+
286+
(removals, additions)
287+
}
288+
228289
#[cfg(test)]
229290
fn dummy_commit_from_body(sha: &str, body: &str) -> GithubCommit {
230291
use chrono::{DateTime, FixedOffset};
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
//! For pull requests that have changed the triagebot.toml, validate that the
2+
//! changes are a valid configuration file.
3+
4+
use crate::{
5+
config::CONFIG_FILE_NAME,
6+
github::FileDiff,
7+
handlers::{Context, IssuesEvent},
8+
};
9+
use anyhow::{Context as _, bail};
10+
11+
pub(super) async fn validate_config(
12+
ctx: &Context,
13+
event: &IssuesEvent,
14+
diff: &[FileDiff],
15+
) -> anyhow::Result<Option<String>> {
16+
if !diff.iter().any(|diff| diff.filename == CONFIG_FILE_NAME) {
17+
return Ok(None);
18+
}
19+
20+
let Some(pr_source) = &event.issue.head else {
21+
bail!("expected head commit");
22+
};
23+
let Some(repo) = &pr_source.repo else {
24+
bail!("repo is not available");
25+
};
26+
27+
let triagebot_content = ctx
28+
.github
29+
.raw_file(&repo.full_name, &pr_source.sha, CONFIG_FILE_NAME)
30+
.await
31+
.context("{CONFIG_FILE_NAME} modified, but failed to get content")?;
32+
33+
let triagebot_content = triagebot_content.unwrap_or_default();
34+
let triagebot_content = String::from_utf8_lossy(&*triagebot_content);
35+
36+
let Err(e) = toml::from_str::<crate::handlers::Config>(&triagebot_content) else {
37+
return Ok(None);
38+
};
39+
40+
let position = match e.span() {
41+
// toml sometimes gives bad spans, see https://github.com/toml-rs/toml/issues/589
42+
Some(span) if span != (0..0) => {
43+
let (line, col) = translate_position(&triagebot_content, span.start);
44+
let url = format!(
45+
"https://github.com/{}/blob/{}/{CONFIG_FILE_NAME}#L{line}",
46+
repo.full_name, pr_source.sha
47+
);
48+
format!(" at position [{line}:{col}]({url})",)
49+
}
50+
Some(_) | None => String::new(),
51+
};
52+
53+
Ok(Some(format!(
54+
"Invalid `triagebot.toml`{position}:\n\
55+
`````\n\
56+
{e}\n\
57+
`````",
58+
)))
59+
}
60+
61+
/// Helper to translate a toml span to a `(line_no, col_no)` (1-based).
62+
fn translate_position(input: &str, index: usize) -> (usize, usize) {
63+
if input.is_empty() {
64+
return (0, index);
65+
}
66+
67+
let safe_index = index.min(input.len() - 1);
68+
let column_offset = index - safe_index;
69+
70+
let nl = input[0..safe_index]
71+
.as_bytes()
72+
.iter()
73+
.rev()
74+
.enumerate()
75+
.find(|(_, b)| **b == b'\n')
76+
.map(|(nl, _)| safe_index - nl - 1);
77+
let line_start = match nl {
78+
Some(nl) => nl + 1,
79+
None => 0,
80+
};
81+
let line = input[0..line_start]
82+
.as_bytes()
83+
.iter()
84+
.filter(|c| **c == b'\n')
85+
.count();
86+
let column = input[line_start..=safe_index].chars().count() - 1;
87+
let column = column + column_offset;
88+
89+
(line + 1, column + 1)
90+
}

0 commit comments

Comments
 (0)