Skip to content

Commit 252acb2

Browse files
meysam81ehuss
authored andcommitted
feat: valdiate proposed change in triagetoml for PRs ✨
fixes rust-lang/rust#106104
1 parent 522cfb1 commit 252acb2

File tree

4 files changed

+134
-2
lines changed

4 files changed

+134
-2
lines changed

src/config.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::sync::{Arc, RwLock};
66
use std::time::{Duration, Instant};
77
use tracing as log;
88

9-
static CONFIG_FILE_NAME: &str = "triagebot.toml";
9+
pub(crate) static CONFIG_FILE_NAME: &str = "triagebot.toml";
1010
const REFRESH_EVERY: Duration = Duration::from_secs(2 * 60); // Every two minutes
1111

1212
lazy_static::lazy_static! {
@@ -36,6 +36,9 @@ pub(crate) struct Config {
3636
pub(crate) note: Option<NoteConfig>,
3737
pub(crate) mentions: Option<MentionsConfig>,
3838
pub(crate) no_merges: Option<NoMergesConfig>,
39+
// We want this validation to run even without the entry in the config file
40+
#[serde(default = "ValidateConfig::default")]
41+
pub(crate) validate_config: Option<ValidateConfig>,
3942
}
4043

4144
#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
@@ -167,6 +170,15 @@ pub(crate) struct PrioritizeConfig {
167170
pub(crate) label: String,
168171
}
169172

173+
#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
174+
pub(crate) struct ValidateConfig {}
175+
176+
impl ValidateConfig {
177+
fn default() -> Option<Self> {
178+
Some(ValidateConfig {})
179+
}
180+
}
181+
170182
#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
171183
pub(crate) struct AutolabelConfig {
172184
#[serde(flatten)]
@@ -450,6 +462,7 @@ mod tests {
450462
review_requested: None,
451463
mentions: None,
452464
no_merges: None,
465+
validate_config: Some(ValidateConfig {}),
453466
}
454467
);
455468
}

src/github.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -999,7 +999,7 @@ struct PullRequestEventFields {}
999999

10001000
#[derive(Clone, Debug, serde::Deserialize)]
10011001
pub struct CommitBase {
1002-
sha: String,
1002+
pub sha: String,
10031003
#[serde(rename = "ref")]
10041004
pub git_ref: String,
10051005
pub repo: Repository,

src/handlers.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ mod rfc_helper;
4646
pub mod rustc_commits;
4747
mod shortcut;
4848
pub mod types_planning_updates;
49+
mod validate_config;
4950

5051
pub async fn handle(ctx: &Context, event: &Event) -> Vec<HandlerError> {
5152
let config = config::get(&ctx.github, event.repo()).await;
@@ -166,6 +167,7 @@ issue_handlers! {
166167
no_merges,
167168
notify_zulip,
168169
review_requested,
170+
validate_config,
169171
}
170172

171173
macro_rules! command_handlers {

src/handlers/validate_config.rs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
//! For pull requests that have changed the triagebot.toml, validate that the
2+
//! changes are a valid configuration file.
3+
//! It won't validate anything unless the PR is open and has changed.
4+
5+
use crate::{
6+
config::{ValidateConfig, CONFIG_FILE_NAME},
7+
handlers::{Context, IssuesEvent},
8+
};
9+
use tracing as log;
10+
11+
pub(super) async fn parse_input(
12+
ctx: &Context,
13+
event: &IssuesEvent,
14+
_config: Option<&ValidateConfig>,
15+
) -> Result<Option<()>, String> {
16+
// All processing needs to be done in parse_input (instead of
17+
// handle_input) because we want this to *always* run. handle_input
18+
// requires the config to exist in triagebot.toml, but we want this to run
19+
// even if it isn't configured. As a consequence, error handling needs to
20+
// be a little more cautious here, since we don't want to relay
21+
// un-actionable errors to the user.
22+
let diff = match event.issue.diff(&ctx.github).await {
23+
Ok(Some(diff)) => diff,
24+
Ok(None) => return Ok(None),
25+
Err(e) => {
26+
log::error!("failed to get diff {e}");
27+
return Ok(None);
28+
}
29+
};
30+
if !diff.iter().any(|diff| diff.path == CONFIG_FILE_NAME) {
31+
return Ok(None);
32+
}
33+
34+
let Some(pr_source) = &event.issue.head else {
35+
log::error!("expected head commit in {event:?}");
36+
return Ok(None);
37+
};
38+
let triagebot_content = match ctx
39+
.github
40+
.raw_file(&pr_source.repo.full_name, &pr_source.sha, CONFIG_FILE_NAME)
41+
.await
42+
{
43+
Ok(Some(c)) => c,
44+
Ok(None) => {
45+
log::error!("{CONFIG_FILE_NAME} modified, but failed to get content");
46+
return Ok(None);
47+
}
48+
Err(e) => {
49+
log::error!("failed to get {CONFIG_FILE_NAME}: {e}");
50+
return Ok(None);
51+
}
52+
};
53+
54+
let triagebot_content = String::from_utf8_lossy(&*triagebot_content);
55+
if let Err(e) = toml::from_str::<crate::handlers::Config>(&triagebot_content) {
56+
let position = match e.span() {
57+
// toml sometimes gives bad spans, see https://github.com/toml-rs/toml/issues/589
58+
Some(span) if span != (0..0) => {
59+
let (line, col) = translate_position(&triagebot_content, span.start);
60+
let url = format!(
61+
"https://github.com/{}/blob/{}/{CONFIG_FILE_NAME}#L{line}",
62+
pr_source.repo.full_name, pr_source.sha
63+
);
64+
format!(" at position [{line}:{col}]({url})",)
65+
}
66+
Some(_) | None => String::new(),
67+
};
68+
69+
return Err(format!(
70+
"Invalid `triagebot.toml`{position}:\n\
71+
`````\n\
72+
{e}\n\
73+
`````",
74+
));
75+
}
76+
Ok(None)
77+
}
78+
79+
pub(super) async fn handle_input(
80+
_ctx: &Context,
81+
_config: &ValidateConfig,
82+
_event: &IssuesEvent,
83+
_input: (),
84+
) -> anyhow::Result<()> {
85+
Ok(())
86+
}
87+
88+
/// Helper to translate a toml span to a `(line_no, col_no)` (1-based).
89+
fn translate_position(input: &str, index: usize) -> (usize, usize) {
90+
if input.is_empty() {
91+
return (0, index);
92+
}
93+
94+
let safe_index = index.min(input.len() - 1);
95+
let column_offset = index - safe_index;
96+
97+
let nl = input[0..safe_index]
98+
.as_bytes()
99+
.iter()
100+
.rev()
101+
.enumerate()
102+
.find(|(_, b)| **b == b'\n')
103+
.map(|(nl, _)| safe_index - nl - 1);
104+
let line_start = match nl {
105+
Some(nl) => nl + 1,
106+
None => 0,
107+
};
108+
let line = input[0..line_start]
109+
.as_bytes()
110+
.iter()
111+
.filter(|c| **c == b'\n')
112+
.count();
113+
let column = input[line_start..=safe_index].chars().count() - 1;
114+
let column = column + column_offset;
115+
116+
(line + 1, column + 1)
117+
}

0 commit comments

Comments
 (0)