relabel.rs 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. //! Purpose: Allow any user to modify issue labels on GitHub via comments.
  2. //!
  3. //! Labels are checked against the labels in the project; the bot does not support creating new
  4. //! labels.
  5. //!
  6. //! Parsing is done in the `parser::command::relabel` module.
  7. //!
  8. //! If the command was successful, there will be no feedback beyond the label change to reduce
  9. //! notification noise.
  10. use crate::{
  11. config::RelabelConfig,
  12. github::{self, Event, GithubClient},
  13. handlers::Context,
  14. interactions::ErrorComment,
  15. };
  16. use parser::command::relabel::{LabelDelta, RelabelCommand};
  17. pub(super) async fn handle_command(
  18. ctx: &Context,
  19. config: &RelabelConfig,
  20. event: &Event,
  21. input: RelabelCommand,
  22. ) -> anyhow::Result<()> {
  23. let mut issue_labels = event.issue().unwrap().labels().to_owned();
  24. let mut changed = false;
  25. for delta in &input.0 {
  26. let name = delta.label().as_str();
  27. let err = match check_filter(name, config, is_member(&event.user(), &ctx.github).await) {
  28. Ok(CheckFilterResult::Allow) => None,
  29. Ok(CheckFilterResult::Deny) => Some(format!(
  30. "Label {} can only be set by Rust team members",
  31. name
  32. )),
  33. Ok(CheckFilterResult::DenyUnknown) => Some(format!(
  34. "Label {} can only be set by Rust team members;\
  35. we were unable to check if you are a team member.",
  36. name
  37. )),
  38. Err(err) => Some(err),
  39. };
  40. if let Some(msg) = err {
  41. let cmnt = ErrorComment::new(&event.issue().unwrap(), msg);
  42. cmnt.post(&ctx.github).await?;
  43. return Ok(());
  44. }
  45. match delta {
  46. LabelDelta::Add(label) => {
  47. if !issue_labels.iter().any(|l| l.name == label.as_str()) {
  48. changed = true;
  49. issue_labels.push(github::Label {
  50. name: label.to_string(),
  51. });
  52. }
  53. }
  54. LabelDelta::Remove(label) => {
  55. if let Some(pos) = issue_labels.iter().position(|l| l.name == label.as_str()) {
  56. changed = true;
  57. issue_labels.remove(pos);
  58. }
  59. }
  60. }
  61. }
  62. if changed {
  63. event
  64. .issue()
  65. .unwrap()
  66. .set_labels(&ctx.github, issue_labels)
  67. .await?;
  68. }
  69. Ok(())
  70. }
  71. #[derive(Debug, PartialEq, Eq)]
  72. enum TeamMembership {
  73. Member,
  74. Outsider,
  75. Unknown,
  76. }
  77. async fn is_member(user: &github::User, client: &GithubClient) -> TeamMembership {
  78. match user.is_team_member(client).await {
  79. Ok(true) => TeamMembership::Member,
  80. Ok(false) => TeamMembership::Outsider,
  81. Err(err) => {
  82. eprintln!("failed to check team membership: {:?}", err);
  83. TeamMembership::Unknown
  84. }
  85. }
  86. }
  87. #[cfg_attr(test, derive(Debug, PartialEq, Eq))]
  88. enum CheckFilterResult {
  89. Allow,
  90. Deny,
  91. DenyUnknown,
  92. }
  93. fn check_filter(
  94. label: &str,
  95. config: &RelabelConfig,
  96. is_member: TeamMembership,
  97. ) -> Result<CheckFilterResult, String> {
  98. if is_member == TeamMembership::Member {
  99. return Ok(CheckFilterResult::Allow);
  100. }
  101. let mut matched = false;
  102. for pattern in &config.allow_unauthenticated {
  103. match match_pattern(pattern, label) {
  104. Ok(MatchPatternResult::Allow) => matched = true,
  105. Ok(MatchPatternResult::Deny) => {
  106. // An explicit deny overrides any allowed pattern
  107. matched = false;
  108. break;
  109. }
  110. Ok(MatchPatternResult::NoMatch) => {}
  111. Err(err) => {
  112. eprintln!("failed to match pattern {}: {}", pattern, err);
  113. return Err(format!("failed to match pattern {}", pattern));
  114. }
  115. }
  116. }
  117. if matched {
  118. return Ok(CheckFilterResult::Allow);
  119. } else if is_member == TeamMembership::Outsider {
  120. return Ok(CheckFilterResult::Deny);
  121. } else {
  122. return Ok(CheckFilterResult::DenyUnknown);
  123. }
  124. }
  125. #[cfg_attr(test, derive(Debug, PartialEq, Eq))]
  126. enum MatchPatternResult {
  127. Allow,
  128. Deny,
  129. NoMatch,
  130. }
  131. fn match_pattern(pattern: &str, label: &str) -> anyhow::Result<MatchPatternResult> {
  132. let (pattern, inverse) = if pattern.starts_with('!') {
  133. (&pattern[1..], true)
  134. } else {
  135. (pattern, false)
  136. };
  137. let glob = glob::Pattern::new(pattern)?;
  138. Ok(match (glob.matches(label), inverse) {
  139. (true, false) => MatchPatternResult::Allow,
  140. (true, true) => MatchPatternResult::Deny,
  141. (false, _) => MatchPatternResult::NoMatch,
  142. })
  143. }
  144. #[cfg(test)]
  145. mod tests {
  146. use super::{
  147. check_filter, match_pattern, CheckFilterResult, MatchPatternResult, TeamMembership,
  148. };
  149. use crate::config::RelabelConfig;
  150. #[test]
  151. fn test_match_pattern() -> anyhow::Result<()> {
  152. assert_eq!(
  153. match_pattern("I-*", "I-nominated")?,
  154. MatchPatternResult::Allow
  155. );
  156. assert_eq!(
  157. match_pattern("!I-no*", "I-nominated")?,
  158. MatchPatternResult::Deny
  159. );
  160. assert_eq!(
  161. match_pattern("I-*", "T-infra")?,
  162. MatchPatternResult::NoMatch
  163. );
  164. assert_eq!(
  165. match_pattern("!I-no*", "T-infra")?,
  166. MatchPatternResult::NoMatch
  167. );
  168. Ok(())
  169. }
  170. #[test]
  171. fn test_check_filter() -> anyhow::Result<()> {
  172. macro_rules! t {
  173. ($($member:ident { $($label:expr => $res:ident,)* })*) => {
  174. let config = RelabelConfig {
  175. allow_unauthenticated: vec!["T-*".into(), "I-*".into(), "!I-nominated".into()],
  176. };
  177. $($(assert_eq!(
  178. check_filter($label, &config, TeamMembership::$member),
  179. Ok(CheckFilterResult::$res)
  180. );)*)*
  181. }
  182. }
  183. t! {
  184. Member {
  185. "T-release" => Allow,
  186. "I-slow" => Allow,
  187. "I-nominated" => Allow,
  188. "A-spurious" => Allow,
  189. }
  190. Outsider {
  191. "T-release" => Allow,
  192. "I-slow" => Allow,
  193. "I-nominated" => Deny,
  194. "A-spurious" => Deny,
  195. }
  196. Unknown {
  197. "T-release" => Allow,
  198. "I-slow" => Allow,
  199. "I-nominated" => DenyUnknown,
  200. "A-spurious" => DenyUnknown,
  201. }
  202. }
  203. Ok(())
  204. }
  205. }