|
| 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