123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245 |
- //! Purpose: Allow any user to modify issue labels on GitHub via comments.
- //!
- //! Labels are checked against the labels in the project; the bot does not support creating new
- //! labels.
- //!
- //! Parsing is done in the `parser::command::relabel` module.
- //!
- //! If the command was successful, there will be no feedback beyond the label change to reduce
- //! notification noise.
- use crate::{
- config::RelabelConfig,
- github::{self, Event, GithubClient},
- handlers::Context,
- interactions::ErrorComment,
- };
- use parser::command::relabel::{LabelDelta, RelabelCommand};
- pub(super) async fn handle_command(
- ctx: &Context,
- config: &RelabelConfig,
- event: &Event,
- input: RelabelCommand,
- ) -> anyhow::Result<()> {
- let mut results = vec![];
- let mut to_add = vec![];
- for delta in &input.0 {
- let name = delta.label().as_str();
- let err = match check_filter(name, config, is_member(&event.user(), &ctx.github).await) {
- Ok(CheckFilterResult::Allow) => None,
- Ok(CheckFilterResult::Deny) => Some(format!(
- "Label {} can only be set by Rust team members",
- name
- )),
- Ok(CheckFilterResult::DenyUnknown) => Some(format!(
- "Label {} can only be set by Rust team members;\
- we were unable to check if you are a team member.",
- name
- )),
- Err(err) => Some(err),
- };
- if let Some(msg) = err {
- let cmnt = ErrorComment::new(&event.issue().unwrap(), msg);
- cmnt.post(&ctx.github).await?;
- return Ok(());
- }
- match delta {
- LabelDelta::Add(label) => {
- to_add.push(github::Label {
- name: label.to_string(),
- });
- }
- LabelDelta::Remove(label) => {
- results.push((
- label,
- event.issue().unwrap().remove_label(&ctx.github, &label),
- ));
- }
- }
- }
- if let Err(e) = event
- .issue()
- .unwrap()
- .add_labels(&ctx.github, to_add.clone())
- .await
- {
- tracing::error!(
- "failed to add {:?} from issue {}: {:?}",
- to_add,
- event.issue().unwrap().global_id(),
- e
- );
- return Err(e);
- }
- for (label, res) in results {
- if let Err(e) = res.await {
- tracing::error!(
- "failed to remove {:?} from issue {}: {:?}",
- label,
- event.issue().unwrap().global_id(),
- e
- );
- return Err(e);
- }
- }
- Ok(())
- }
- #[derive(Debug, PartialEq, Eq)]
- enum TeamMembership {
- Member,
- Outsider,
- Unknown,
- }
- async fn is_member(user: &github::User, client: &GithubClient) -> TeamMembership {
- match user.is_team_member(client).await {
- Ok(true) => TeamMembership::Member,
- Ok(false) => TeamMembership::Outsider,
- Err(err) => {
- eprintln!("failed to check team membership: {:?}", err);
- TeamMembership::Unknown
- }
- }
- }
- #[cfg_attr(test, derive(Debug, PartialEq, Eq))]
- enum CheckFilterResult {
- Allow,
- Deny,
- DenyUnknown,
- }
- fn check_filter(
- label: &str,
- config: &RelabelConfig,
- is_member: TeamMembership,
- ) -> Result<CheckFilterResult, String> {
- if is_member == TeamMembership::Member {
- return Ok(CheckFilterResult::Allow);
- }
- let mut matched = false;
- for pattern in &config.allow_unauthenticated {
- match match_pattern(pattern, label) {
- Ok(MatchPatternResult::Allow) => matched = true,
- Ok(MatchPatternResult::Deny) => {
- // An explicit deny overrides any allowed pattern
- matched = false;
- break;
- }
- Ok(MatchPatternResult::NoMatch) => {}
- Err(err) => {
- eprintln!("failed to match pattern {}: {}", pattern, err);
- return Err(format!("failed to match pattern {}", pattern));
- }
- }
- }
- if matched {
- return Ok(CheckFilterResult::Allow);
- } else if is_member == TeamMembership::Outsider {
- return Ok(CheckFilterResult::Deny);
- } else {
- return Ok(CheckFilterResult::DenyUnknown);
- }
- }
- #[cfg_attr(test, derive(Debug, PartialEq, Eq))]
- enum MatchPatternResult {
- Allow,
- Deny,
- NoMatch,
- }
- fn match_pattern(pattern: &str, label: &str) -> anyhow::Result<MatchPatternResult> {
- let (pattern, inverse) = if pattern.starts_with('!') {
- (&pattern[1..], true)
- } else {
- (pattern, false)
- };
- let glob = glob::Pattern::new(pattern)?;
- let mut matchopts = glob::MatchOptions::default();
- matchopts.case_sensitive = false;
- Ok(match (glob.matches_with(label, matchopts), inverse) {
- (true, false) => MatchPatternResult::Allow,
- (true, true) => MatchPatternResult::Deny,
- (false, _) => MatchPatternResult::NoMatch,
- })
- }
- #[cfg(test)]
- mod tests {
- use super::{
- check_filter, match_pattern, CheckFilterResult, MatchPatternResult, TeamMembership,
- };
- use crate::config::RelabelConfig;
- #[test]
- fn test_match_pattern() -> anyhow::Result<()> {
- assert_eq!(
- match_pattern("I-*", "I-nominated")?,
- MatchPatternResult::Allow
- );
- assert_eq!(
- match_pattern("i-*", "I-nominated")?,
- MatchPatternResult::Allow
- );
- assert_eq!(
- match_pattern("!I-no*", "I-nominated")?,
- MatchPatternResult::Deny
- );
- assert_eq!(
- match_pattern("I-*", "T-infra")?,
- MatchPatternResult::NoMatch
- );
- assert_eq!(
- match_pattern("!I-no*", "T-infra")?,
- MatchPatternResult::NoMatch
- );
- Ok(())
- }
- #[test]
- fn test_check_filter() -> anyhow::Result<()> {
- macro_rules! t {
- ($($member:ident { $($label:expr => $res:ident,)* })*) => {
- let config = RelabelConfig {
- allow_unauthenticated: vec!["T-*".into(), "I-*".into(), "!I-*nominated".into()],
- };
- $($(assert_eq!(
- check_filter($label, &config, TeamMembership::$member),
- Ok(CheckFilterResult::$res)
- );)*)*
- }
- }
- t! {
- Member {
- "T-release" => Allow,
- "I-slow" => Allow,
- "I-lang-nominated" => Allow,
- "I-nominated" => Allow,
- "A-spurious" => Allow,
- }
- Outsider {
- "T-release" => Allow,
- "I-slow" => Allow,
- "I-lang-nominated" => Deny,
- "I-nominated" => Deny,
- "A-spurious" => Deny,
- }
- Unknown {
- "T-release" => Allow,
- "I-slow" => Allow,
- "I-lang-nominated" => DenyUnknown,
- "I-nominated" => DenyUnknown,
- "A-spurious" => DenyUnknown,
- }
- }
- Ok(())
- }
- }
|