assign.rs 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  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. // Don't re-assign if aleady assigned, e.g. on comment edit
  95. if issue.contain_assignee(&username) {
  96. log::trace!(
  97. "ignoring assign PR {} to {}, already assigned",
  98. issue.global_id(),
  99. username,
  100. );
  101. return Ok(());
  102. }
  103. if let Err(err) = issue
  104. .set_assignee(&ctx.github, &username)
  105. .await
  106. {
  107. log::warn!(
  108. "failed to set assignee of PR {} to {}: {:?}",
  109. issue.global_id(),
  110. username,
  111. err
  112. );
  113. }
  114. return Ok(());
  115. }
  116. let e = EditIssueBody::new(&issue, "ASSIGN");
  117. let to_assign = match cmd {
  118. AssignCommand::Own => event.user().login.clone(),
  119. AssignCommand::User { username } => {
  120. if !is_team_member && username != event.user().login {
  121. anyhow::bail!("Only Rust team members can assign other users");
  122. }
  123. username.clone()
  124. }
  125. AssignCommand::Release => {
  126. if let Some(AssignData {
  127. user: Some(current),
  128. }) = e.current_data()
  129. {
  130. if current == event.user().login || is_team_member {
  131. issue
  132. .remove_assignees(&ctx.github, Selection::All)
  133. .await?;
  134. e.apply(&ctx.github, String::new(), AssignData { user: None })
  135. .await?;
  136. return Ok(());
  137. } else {
  138. anyhow::bail!("Cannot release another user's assignment");
  139. }
  140. } else {
  141. let current = &event.user().login;
  142. if issue.contain_assignee(current) {
  143. issue
  144. .remove_assignees(&ctx.github, Selection::One(&current))
  145. .await?;
  146. e.apply(&ctx.github, String::new(), AssignData { user: None })
  147. .await?;
  148. return Ok(());
  149. } else {
  150. anyhow::bail!("Cannot release unassigned issue");
  151. }
  152. };
  153. }
  154. };
  155. // Don't re-assign if aleady assigned, e.g. on comment edit
  156. if issue.contain_assignee(&to_assign) {
  157. log::trace!(
  158. "ignoring assign issue {} to {}, already assigned",
  159. issue.global_id(),
  160. to_assign,
  161. );
  162. return Ok(());
  163. }
  164. let data = AssignData {
  165. user: Some(to_assign.clone()),
  166. };
  167. e.apply(&ctx.github, String::new(), &data).await?;
  168. match issue
  169. .set_assignee(&ctx.github, &to_assign)
  170. .await
  171. {
  172. Ok(()) => return Ok(()), // we are done
  173. Err(github::AssignmentError::InvalidAssignee) => {
  174. issue
  175. .set_assignee(&ctx.github, &ctx.username)
  176. .await
  177. .context("self-assignment failed")?;
  178. let cmt_body = format!(
  179. "This issue has been assigned to @{} via [this comment]({}).",
  180. to_assign,
  181. event.html_url().unwrap()
  182. );
  183. e.apply(&ctx.github, cmt_body, &data).await?;
  184. }
  185. Err(e) => return Err(e.into()),
  186. }
  187. Ok(())
  188. }