assign.rs 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. //! Permit assignment of any user to issues, without requiring "write" access to the repository.
  2. //!
  3. //! We need to fake-assign ourselves and add a 'claimed by' section to the top-level comment.
  4. //!
  5. //! Such assigned issues should also be placed in a queue to ensure that the user remains
  6. //! active; the assigned user will be asked for a status report every 2 weeks (XXX: timing).
  7. //!
  8. //! If we're intending to ask for a status report but no comments from the assigned user have
  9. //! been given for the past 2 weeks, the bot will de-assign the user. They can once more claim
  10. //! the issue if necessary.
  11. //!
  12. //! Assign users with `@rustbot assign @gh-user` or `@rustbot claim` (self-claim).
  13. use crate::{
  14. config::AssignConfig,
  15. github::{self, Event, Selection},
  16. handlers::{Context, Handler},
  17. interactions::EditIssueBody,
  18. };
  19. use anyhow::Context as _;
  20. use futures::future::{BoxFuture, FutureExt};
  21. use parser::command::assign::AssignCommand;
  22. use parser::command::{Command, Input};
  23. pub(super) struct AssignmentHandler;
  24. #[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
  25. struct AssignData {
  26. user: Option<String>,
  27. }
  28. impl Handler for AssignmentHandler {
  29. type Input = AssignCommand;
  30. type Config = AssignConfig;
  31. fn parse_input(
  32. &self,
  33. ctx: &Context,
  34. event: &Event,
  35. _: Option<&AssignConfig>,
  36. ) -> Result<Option<Self::Input>, String> {
  37. let body = if let Some(b) = event.comment_body() {
  38. b
  39. } else {
  40. // not interested in other events
  41. return Ok(None);
  42. };
  43. if let Event::Issue(e) = event {
  44. if e.action != github::IssuesAction::Opened {
  45. log::debug!("skipping event, issue was {:?}", e.action);
  46. // skip events other than opening the issue to avoid retriggering commands in the
  47. // issue body
  48. return Ok(None);
  49. }
  50. }
  51. let mut input = Input::new(&body, &ctx.username);
  52. match input.parse_command() {
  53. Command::Assign(Ok(command)) => Ok(Some(command)),
  54. Command::Assign(Err(err)) => {
  55. return Err(format!(
  56. "Parsing assign command in [comment]({}) failed: {}",
  57. event.html_url().expect("has html url"),
  58. err
  59. ));
  60. }
  61. _ => Ok(None),
  62. }
  63. }
  64. fn handle_input<'a>(
  65. &self,
  66. ctx: &'a Context,
  67. _config: &'a AssignConfig,
  68. event: &'a Event,
  69. cmd: AssignCommand,
  70. ) -> BoxFuture<'a, anyhow::Result<()>> {
  71. handle_input(ctx, event, cmd).boxed()
  72. }
  73. }
  74. async fn handle_input(ctx: &Context, event: &Event, cmd: AssignCommand) -> anyhow::Result<()> {
  75. let is_team_member = if let Err(_) | Ok(false) = event.user().is_team_member(&ctx.github).await
  76. {
  77. false
  78. } else {
  79. true
  80. };
  81. let issue = event.issue().unwrap();
  82. if issue.is_pr() {
  83. let username = match &cmd {
  84. AssignCommand::Own => event.user().login.clone(),
  85. AssignCommand::User { username } => username.clone(),
  86. AssignCommand::Release => {
  87. log::trace!(
  88. "ignoring release on PR {:?}, must always have assignee",
  89. issue.global_id()
  90. );
  91. return Ok(());
  92. }
  93. };
  94. if let Err(err) = issue
  95. .set_assignee(&ctx.github, &username)
  96. .await
  97. {
  98. log::warn!(
  99. "failed to set assignee of PR {} to {}: {:?}",
  100. issue.global_id(),
  101. username,
  102. err
  103. );
  104. }
  105. return Ok(());
  106. }
  107. let e = EditIssueBody::new(&issue, "ASSIGN");
  108. let to_assign = match cmd {
  109. AssignCommand::Own => event.user().login.clone(),
  110. AssignCommand::User { username } => {
  111. if !is_team_member && username != event.user().login {
  112. anyhow::bail!("Only Rust team members can assign other users");
  113. }
  114. username.clone()
  115. }
  116. AssignCommand::Release => {
  117. if let Some(AssignData {
  118. user: Some(current),
  119. }) = e.current_data()
  120. {
  121. if current == event.user().login || is_team_member {
  122. issue
  123. .remove_assignees(&ctx.github, Selection::All)
  124. .await?;
  125. e.apply(&ctx.github, String::new(), AssignData { user: None })
  126. .await?;
  127. return Ok(());
  128. } else {
  129. anyhow::bail!("Cannot release another user's assignment");
  130. }
  131. } else {
  132. let current = &event.user().login;
  133. if issue.contain_assignee(current) {
  134. issue
  135. .remove_assignees(&ctx.github, Selection::One(&current))
  136. .await?;
  137. e.apply(&ctx.github, String::new(), AssignData { user: None })
  138. .await?;
  139. return Ok(());
  140. } else {
  141. anyhow::bail!("Cannot release unassigned issue");
  142. }
  143. };
  144. }
  145. };
  146. let data = AssignData {
  147. user: Some(to_assign.clone()),
  148. };
  149. e.apply(&ctx.github, String::new(), &data).await?;
  150. match issue
  151. .set_assignee(&ctx.github, &to_assign)
  152. .await
  153. {
  154. Ok(()) => return Ok(()), // we are done
  155. Err(github::AssignmentError::InvalidAssignee) => {
  156. issue
  157. .set_assignee(&ctx.github, &ctx.username)
  158. .await
  159. .context("self-assignment failed")?;
  160. let cmt_body = format!(
  161. "This issue has been assigned to @{} via [this comment]({}).",
  162. to_assign,
  163. event.html_url().unwrap()
  164. );
  165. e.apply(&ctx.github, cmt_body, &data).await?;
  166. }
  167. Err(e) => return Err(e.into()),
  168. }
  169. Ok(())
  170. }