autolabel.rs 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. use crate::{
  2. config::AutolabelConfig,
  3. github::{files_changed, IssuesAction, IssuesEvent, Label},
  4. handlers::Context,
  5. };
  6. use anyhow::Context as _;
  7. use tracing as log;
  8. pub(super) struct AutolabelInput {
  9. add: Vec<Label>,
  10. remove: Vec<Label>,
  11. }
  12. pub(super) async fn parse_input(
  13. ctx: &Context,
  14. event: &IssuesEvent,
  15. config: Option<&AutolabelConfig>,
  16. ) -> Result<Option<AutolabelInput>, String> {
  17. let config = match config {
  18. Some(config) => config,
  19. None => return Ok(None),
  20. };
  21. // On opening a new PR or sync'ing the branch, look at the diff and try to
  22. // add any appropriate labels.
  23. //
  24. // FIXME: This will re-apply labels after a push that the user had tried to
  25. // remove. Not much can be done about that currently; the before/after on
  26. // synchronize may be straddling a rebase, which will break diff generation.
  27. if event.action == IssuesAction::Opened || event.action == IssuesAction::Synchronize {
  28. let diff = event
  29. .issue
  30. .diff(&ctx.github)
  31. .await
  32. .map_err(|e| {
  33. log::error!("failed to fetch diff: {:?}", e);
  34. })
  35. .unwrap_or_default();
  36. let files = diff.as_deref().map(files_changed);
  37. let mut autolabels = Vec::new();
  38. 'outer: for (label, cfg) in config.labels.iter() {
  39. let exclude_patterns: Vec<glob::Pattern> = cfg
  40. .exclude_labels
  41. .iter()
  42. .filter_map(|label| match glob::Pattern::new(label) {
  43. Ok(exclude_glob) => Some(exclude_glob),
  44. Err(error) => {
  45. log::error!("Invalid glob pattern: {}", error);
  46. None
  47. }
  48. })
  49. .collect();
  50. for label in event.issue.labels() {
  51. for pat in &exclude_patterns {
  52. if pat.matches(&label.name) {
  53. // If we hit an excluded label, ignore this autolabel and check the next
  54. continue 'outer;
  55. }
  56. }
  57. }
  58. if let Some(files) = &files {
  59. if cfg
  60. .trigger_files
  61. .iter()
  62. .any(|f| files.iter().any(|diff_file| diff_file.starts_with(f)))
  63. {
  64. autolabels.push(Label {
  65. name: label.to_owned(),
  66. });
  67. }
  68. if cfg.new_pr && event.action == IssuesAction::Opened {
  69. autolabels.push(Label {
  70. name: label.to_owned(),
  71. });
  72. }
  73. }
  74. if event.issue.pull_request.is_none()
  75. && cfg.new_issue
  76. && event.action == IssuesAction::Opened
  77. {
  78. autolabels.push(Label {
  79. name: label.to_owned(),
  80. });
  81. }
  82. }
  83. if !autolabels.is_empty() {
  84. return Ok(Some(AutolabelInput {
  85. add: autolabels,
  86. remove: vec![],
  87. }));
  88. }
  89. }
  90. if event.action == IssuesAction::Labeled {
  91. let mut autolabels = Vec::new();
  92. let applied_label = &event.label.as_ref().expect("label").name;
  93. 'outer: for (label, config) in config.get_by_trigger(applied_label) {
  94. let exclude_patterns: Vec<glob::Pattern> = config
  95. .exclude_labels
  96. .iter()
  97. .filter_map(|label| match glob::Pattern::new(label) {
  98. Ok(exclude_glob) => Some(exclude_glob),
  99. Err(error) => {
  100. log::error!("Invalid glob pattern: {}", error);
  101. None
  102. }
  103. })
  104. .collect();
  105. for label in event.issue.labels() {
  106. for pat in &exclude_patterns {
  107. if pat.matches(&label.name) {
  108. // If we hit an excluded label, ignore this autolabel and check the next
  109. continue 'outer;
  110. }
  111. }
  112. }
  113. // If we reach here, no excluded labels were found, so we should apply the autolabel.
  114. autolabels.push(Label {
  115. name: label.to_owned(),
  116. });
  117. }
  118. if !autolabels.is_empty() {
  119. return Ok(Some(AutolabelInput {
  120. add: autolabels,
  121. remove: vec![],
  122. }));
  123. }
  124. }
  125. Ok(None)
  126. }
  127. pub(super) async fn handle_input(
  128. ctx: &Context,
  129. _config: &AutolabelConfig,
  130. event: &IssuesEvent,
  131. input: AutolabelInput,
  132. ) -> anyhow::Result<()> {
  133. match event.issue.add_labels(&ctx.github, input.add).await {
  134. Ok(()) => {}
  135. Err(e) => {
  136. use crate::github::UnknownLabels;
  137. if let Some(err @ UnknownLabels { .. }) = e.downcast_ref() {
  138. event
  139. .issue
  140. .post_comment(&ctx.github, &err.to_string())
  141. .await
  142. .context("failed to post missing label comment")?;
  143. return Ok(());
  144. }
  145. return Err(e);
  146. }
  147. }
  148. for label in input.remove {
  149. event
  150. .issue
  151. .remove_label(&ctx.github, &label.name)
  152. .await
  153. .with_context(|| {
  154. format!(
  155. "failed to remove {:?} from {:?}",
  156. label,
  157. event.issue.global_id()
  158. )
  159. })?;
  160. }
  161. Ok(())
  162. }