github.rs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906
  1. use anyhow::Context;
  2. use chrono::{DateTime, FixedOffset, Utc};
  3. use futures::stream::{FuturesUnordered, StreamExt};
  4. use futures::{future::BoxFuture, FutureExt};
  5. use once_cell::sync::OnceCell;
  6. use reqwest::header::{AUTHORIZATION, USER_AGENT};
  7. use reqwest::{Client, Request, RequestBuilder, Response, StatusCode};
  8. use std::{
  9. fmt,
  10. time::{Duration, SystemTime},
  11. };
  12. #[derive(Debug, PartialEq, Eq, serde::Deserialize)]
  13. pub struct User {
  14. pub login: String,
  15. pub id: Option<i64>,
  16. }
  17. impl GithubClient {
  18. async fn _send_req(&self, req: RequestBuilder) -> Result<(Response, String), reqwest::Error> {
  19. const MAX_ATTEMPTS: usize = 2;
  20. log::debug!("_send_req with {:?}", req);
  21. let req = req.build()?;
  22. let req_dbg = format!("{:?}", req);
  23. let mut resp = self.client.execute(req.try_clone().unwrap()).await?;
  24. if let Some(sleep) = Self::needs_retry(&resp).await {
  25. drop(resp);
  26. resp = self.retry(req, sleep, MAX_ATTEMPTS).await?;
  27. }
  28. if let Err(e) = resp.error_for_status_ref() {
  29. return Err(e);
  30. }
  31. Ok((resp, req_dbg))
  32. }
  33. async fn needs_retry(resp: &Response) -> Option<Duration> {
  34. const REMAINING: &str = "X-RateLimit-Remaining";
  35. const RESET: &str = "X-RateLimit-Reset";
  36. if resp.status().is_success() {
  37. return None;
  38. }
  39. let headers = resp.headers();
  40. if !(headers.contains_key(REMAINING) && headers.contains_key(RESET)) {
  41. return None;
  42. }
  43. // Weird github api behavior. It asks us to retry but also has a remaining count above 1
  44. // Try again immediately and hope for the best...
  45. if headers[REMAINING] != "0" {
  46. return Some(Duration::from_secs(0));
  47. }
  48. let reset_time = headers[RESET].to_str().unwrap().parse::<u64>().unwrap();
  49. Some(Duration::from_secs(Self::calc_sleep(reset_time) + 10))
  50. }
  51. fn calc_sleep(reset_time: u64) -> u64 {
  52. let epoch_time = SystemTime::now()
  53. .duration_since(SystemTime::UNIX_EPOCH)
  54. .unwrap()
  55. .as_secs();
  56. reset_time.saturating_sub(epoch_time)
  57. }
  58. fn retry(
  59. &self,
  60. req: Request,
  61. sleep: Duration,
  62. remaining_attempts: usize,
  63. ) -> BoxFuture<Result<Response, reqwest::Error>> {
  64. #[derive(Debug, serde::Deserialize)]
  65. struct RateLimit {
  66. pub limit: u64,
  67. pub remaining: u64,
  68. pub reset: u64,
  69. }
  70. #[derive(Debug, serde::Deserialize)]
  71. struct RateLimitResponse {
  72. pub resources: Resources,
  73. }
  74. #[derive(Debug, serde::Deserialize)]
  75. struct Resources {
  76. pub core: RateLimit,
  77. pub search: RateLimit,
  78. pub graphql: RateLimit,
  79. pub source_import: RateLimit,
  80. }
  81. async move {
  82. tokio::time::delay_for(sleep).await;
  83. // check rate limit
  84. let rate_resp = self
  85. .client
  86. .execute(
  87. self.client
  88. .get("https://api.github.com/rate_limit")
  89. .configure(self)
  90. .build()
  91. .unwrap(),
  92. )
  93. .await?;
  94. let rate_limit_response = rate_resp.json::<RateLimitResponse>().await?;
  95. // Check url for search path because github has different rate limits for the search api
  96. let rate_limit = if req
  97. .url()
  98. .path_segments()
  99. .map(|mut segments| matches!(segments.next(), Some("search")))
  100. .unwrap_or(false)
  101. {
  102. rate_limit_response.resources.search
  103. } else {
  104. rate_limit_response.resources.core
  105. };
  106. // If we still don't have any more remaining attempts, try sleeping for the remaining
  107. // period of time
  108. if rate_limit.remaining == 0 {
  109. let sleep = Self::calc_sleep(rate_limit.reset);
  110. if sleep > 0 {
  111. tokio::time::delay_for(Duration::from_secs(sleep)).await;
  112. }
  113. }
  114. let resp = self.client.execute(req.try_clone().unwrap()).await?;
  115. if let Some(sleep) = Self::needs_retry(&resp).await {
  116. if remaining_attempts > 0 {
  117. drop(resp);
  118. return self.retry(req, sleep, remaining_attempts - 1).await;
  119. }
  120. }
  121. Ok(resp)
  122. }
  123. .boxed()
  124. }
  125. async fn send_req(&self, req: RequestBuilder) -> anyhow::Result<Vec<u8>> {
  126. let (mut resp, req_dbg) = self._send_req(req).await?;
  127. let mut body = Vec::new();
  128. while let Some(chunk) = resp.chunk().await.transpose() {
  129. let chunk = chunk
  130. .context("reading stream failed")
  131. .map_err(anyhow::Error::from)
  132. .context(req_dbg.clone())?;
  133. body.extend_from_slice(&chunk);
  134. }
  135. Ok(body)
  136. }
  137. pub async fn json<T>(&self, req: RequestBuilder) -> anyhow::Result<T>
  138. where
  139. T: serde::de::DeserializeOwned,
  140. {
  141. let (resp, req_dbg) = self._send_req(req).await?;
  142. Ok(resp.json().await.context(req_dbg)?)
  143. }
  144. }
  145. impl User {
  146. pub async fn current(client: &GithubClient) -> anyhow::Result<Self> {
  147. client.json(client.get("https://api.github.com/user")).await
  148. }
  149. pub async fn is_team_member<'a>(&'a self, client: &'a GithubClient) -> anyhow::Result<bool> {
  150. let permission = crate::team_data::teams(client).await?;
  151. let map = permission.teams;
  152. let is_triager = map
  153. .get("wg-triage")
  154. .map_or(false, |w| w.members.iter().any(|g| g.github == self.login));
  155. let is_pri_member = map
  156. .get("wg-prioritization")
  157. .map_or(false, |w| w.members.iter().any(|g| g.github == self.login));
  158. Ok(
  159. map["all"].members.iter().any(|g| g.github == self.login)
  160. || is_triager
  161. || is_pri_member,
  162. )
  163. }
  164. // Returns the ID of the given user, if the user is in the `all` team.
  165. pub async fn get_id<'a>(&'a self, client: &'a GithubClient) -> anyhow::Result<Option<usize>> {
  166. let permission = crate::team_data::teams(client).await?;
  167. let map = permission.teams;
  168. Ok(map["all"]
  169. .members
  170. .iter()
  171. .find(|g| g.github == self.login)
  172. .map(|u| u.github_id))
  173. }
  174. }
  175. pub async fn get_team(
  176. client: &GithubClient,
  177. team: &str,
  178. ) -> anyhow::Result<Option<rust_team_data::v1::Team>> {
  179. let permission = crate::team_data::teams(client).await?;
  180. let mut map = permission.teams;
  181. Ok(map.swap_remove(team))
  182. }
  183. #[derive(PartialEq, Eq, Debug, Clone, serde::Deserialize)]
  184. pub struct Label {
  185. pub name: String,
  186. }
  187. impl Label {
  188. async fn exists<'a>(&'a self, repo_api_prefix: &'a str, client: &'a GithubClient) -> bool {
  189. #[allow(clippy::redundant_pattern_matching)]
  190. let url = format!("{}/labels/{}", repo_api_prefix, self.name);
  191. match client.send_req(client.get(&url)).await {
  192. Ok(_) => true,
  193. // XXX: Error handling if the request failed for reasons beyond 'label didn't exist'
  194. Err(_) => false,
  195. }
  196. }
  197. }
  198. #[derive(Debug, serde::Deserialize)]
  199. pub struct PullRequestDetails {
  200. // none for now
  201. }
  202. #[derive(Debug, serde::Deserialize)]
  203. pub struct Issue {
  204. pub number: u64,
  205. pub body: String,
  206. created_at: chrono::DateTime<Utc>,
  207. pub title: String,
  208. pub html_url: String,
  209. pub user: User,
  210. labels: Vec<Label>,
  211. assignees: Vec<User>,
  212. pull_request: Option<PullRequestDetails>,
  213. // API URL
  214. comments_url: String,
  215. #[serde(skip)]
  216. repository: OnceCell<IssueRepository>,
  217. }
  218. #[derive(Debug, serde::Deserialize)]
  219. pub struct Comment {
  220. #[serde(deserialize_with = "opt_string")]
  221. pub body: String,
  222. pub html_url: String,
  223. pub user: User,
  224. #[serde(alias = "submitted_at")] // for pull request reviews
  225. pub updated_at: chrono::DateTime<Utc>,
  226. }
  227. fn opt_string<'de, D>(deserializer: D) -> Result<String, D::Error>
  228. where
  229. D: serde::de::Deserializer<'de>,
  230. {
  231. use serde::de::Deserialize;
  232. match <Option<String>>::deserialize(deserializer) {
  233. Ok(v) => Ok(v.unwrap_or_default()),
  234. Err(e) => Err(e),
  235. }
  236. }
  237. #[derive(Debug)]
  238. pub enum AssignmentError {
  239. InvalidAssignee,
  240. Http(reqwest::Error),
  241. }
  242. #[derive(Debug)]
  243. pub enum Selection<'a, T> {
  244. All,
  245. One(&'a T),
  246. }
  247. impl fmt::Display for AssignmentError {
  248. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  249. match self {
  250. AssignmentError::InvalidAssignee => write!(f, "invalid assignee"),
  251. AssignmentError::Http(e) => write!(f, "cannot assign: {}", e),
  252. }
  253. }
  254. }
  255. impl std::error::Error for AssignmentError {}
  256. #[derive(Debug)]
  257. pub struct IssueRepository {
  258. pub organization: String,
  259. pub repository: String,
  260. }
  261. impl fmt::Display for IssueRepository {
  262. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  263. write!(f, "{}/{}", self.organization, self.repository)
  264. }
  265. }
  266. impl IssueRepository {
  267. fn url(&self) -> String {
  268. format!(
  269. "https://api.github.com/repos/{}/{}",
  270. self.organization, self.repository
  271. )
  272. }
  273. }
  274. impl Issue {
  275. pub fn zulip_topic_reference(&self) -> String {
  276. let repo = self.repository();
  277. if repo.organization == "rust-lang" {
  278. if repo.repository == "rust" {
  279. format!("#{}", self.number)
  280. } else {
  281. format!("{}#{}", repo.repository, self.number)
  282. }
  283. } else {
  284. format!("{}/{}#{}", repo.organization, repo.repository, self.number)
  285. }
  286. }
  287. pub fn repository(&self) -> &IssueRepository {
  288. self.repository.get_or_init(|| {
  289. // https://api.github.com/repos/rust-lang/rust/issues/69257/comments
  290. log::trace!("get repository for {}", self.comments_url);
  291. let url = url::Url::parse(&self.comments_url).unwrap();
  292. let mut segments = url.path_segments().unwrap();
  293. let _comments = segments.next_back().unwrap();
  294. let _number = segments.next_back().unwrap();
  295. let _issues_or_prs = segments.next_back().unwrap();
  296. let repository = segments.next_back().unwrap();
  297. let organization = segments.next_back().unwrap();
  298. IssueRepository {
  299. organization: organization.into(),
  300. repository: repository.into(),
  301. }
  302. })
  303. }
  304. pub fn global_id(&self) -> String {
  305. format!("{}#{}", self.repository(), self.number)
  306. }
  307. pub fn is_pr(&self) -> bool {
  308. self.pull_request.is_some()
  309. }
  310. pub async fn get_comment(&self, client: &GithubClient, id: usize) -> anyhow::Result<Comment> {
  311. let comment_url = format!("{}/issues/comments/{}", self.repository().url(), id);
  312. let comment = client.json(client.get(&comment_url)).await?;
  313. Ok(comment)
  314. }
  315. pub async fn edit_body(&self, client: &GithubClient, body: &str) -> anyhow::Result<()> {
  316. let edit_url = format!("{}/issues/{}", self.repository().url(), self.number);
  317. #[derive(serde::Serialize)]
  318. struct ChangedIssue<'a> {
  319. body: &'a str,
  320. }
  321. client
  322. ._send_req(client.patch(&edit_url).json(&ChangedIssue { body }))
  323. .await
  324. .context("failed to edit issue body")?;
  325. Ok(())
  326. }
  327. pub async fn edit_comment(
  328. &self,
  329. client: &GithubClient,
  330. id: usize,
  331. new_body: &str,
  332. ) -> anyhow::Result<()> {
  333. let comment_url = format!("{}/issues/comments/{}", self.repository().url(), id);
  334. #[derive(serde::Serialize)]
  335. struct NewComment<'a> {
  336. body: &'a str,
  337. }
  338. client
  339. ._send_req(
  340. client
  341. .patch(&comment_url)
  342. .json(&NewComment { body: new_body }),
  343. )
  344. .await
  345. .context("failed to edit comment")?;
  346. Ok(())
  347. }
  348. pub async fn post_comment(&self, client: &GithubClient, body: &str) -> anyhow::Result<()> {
  349. #[derive(serde::Serialize)]
  350. struct PostComment<'a> {
  351. body: &'a str,
  352. }
  353. client
  354. ._send_req(client.post(&self.comments_url).json(&PostComment { body }))
  355. .await
  356. .context("failed to post comment")?;
  357. Ok(())
  358. }
  359. pub async fn set_labels(
  360. &self,
  361. client: &GithubClient,
  362. labels: Vec<Label>,
  363. ) -> anyhow::Result<()> {
  364. log::info!("set_labels {} to {:?}", self.global_id(), labels);
  365. // PUT /repos/:owner/:repo/issues/:number/labels
  366. // repo_url = https://api.github.com/repos/Codertocat/Hello-World
  367. let url = format!(
  368. "{repo_url}/issues/{number}/labels",
  369. repo_url = self.repository().url(),
  370. number = self.number
  371. );
  372. let mut stream = labels
  373. .into_iter()
  374. .map(|label| async { (label.exists(&self.repository().url(), &client).await, label) })
  375. .collect::<FuturesUnordered<_>>();
  376. let mut labels = Vec::new();
  377. while let Some((true, label)) = stream.next().await {
  378. labels.push(label);
  379. }
  380. #[derive(serde::Serialize)]
  381. struct LabelsReq {
  382. labels: Vec<String>,
  383. }
  384. client
  385. ._send_req(client.put(&url).json(&LabelsReq {
  386. labels: labels.iter().map(|l| l.name.clone()).collect(),
  387. }))
  388. .await
  389. .context("failed to set labels")?;
  390. Ok(())
  391. }
  392. pub fn labels(&self) -> &[Label] {
  393. &self.labels
  394. }
  395. pub fn contain_assignee(&self, user: &User) -> bool {
  396. self.assignees.contains(user)
  397. }
  398. pub async fn remove_assignees(
  399. &self,
  400. client: &GithubClient,
  401. selection: Selection<'_, User>,
  402. ) -> Result<(), AssignmentError> {
  403. log::info!("remove {:?} assignees for {}", selection, self.global_id());
  404. let url = format!(
  405. "{repo_url}/issues/{number}/assignees",
  406. repo_url = self.repository().url(),
  407. number = self.number
  408. );
  409. let assignees = match selection {
  410. Selection::All => self
  411. .assignees
  412. .iter()
  413. .map(|u| u.login.as_str())
  414. .collect::<Vec<_>>(),
  415. Selection::One(user) => vec![user.login.as_str()],
  416. };
  417. #[derive(serde::Serialize)]
  418. struct AssigneeReq<'a> {
  419. assignees: &'a [&'a str],
  420. }
  421. client
  422. ._send_req(client.delete(&url).json(&AssigneeReq {
  423. assignees: &assignees[..],
  424. }))
  425. .await
  426. .map_err(AssignmentError::Http)?;
  427. Ok(())
  428. }
  429. pub async fn set_assignee(
  430. &self,
  431. client: &GithubClient,
  432. user: &str,
  433. ) -> Result<(), AssignmentError> {
  434. log::info!("set_assignee for {} to {}", self.global_id(), user);
  435. let url = format!(
  436. "{repo_url}/issues/{number}/assignees",
  437. repo_url = self.repository().url(),
  438. number = self.number
  439. );
  440. let check_url = format!(
  441. "{repo_url}/assignees/{name}",
  442. repo_url = self.repository().url(),
  443. name = user,
  444. );
  445. match client._send_req(client.get(&check_url)).await {
  446. Ok((resp, _)) => {
  447. if resp.status() == reqwest::StatusCode::NO_CONTENT {
  448. // all okay
  449. log::debug!("set_assignee: assignee is valid");
  450. } else {
  451. log::error!(
  452. "unknown status for assignee check, assuming all okay: {:?}",
  453. resp
  454. );
  455. }
  456. }
  457. Err(e) => {
  458. if e.status() == Some(reqwest::StatusCode::NOT_FOUND) {
  459. log::debug!("set_assignee: assignee is invalid, returning");
  460. return Err(AssignmentError::InvalidAssignee);
  461. }
  462. log::debug!("set_assignee: get {} failed, {:?}", check_url, e);
  463. return Err(AssignmentError::Http(e));
  464. }
  465. }
  466. self.remove_assignees(client, Selection::All).await?;
  467. #[derive(serde::Serialize)]
  468. struct AssigneeReq<'a> {
  469. assignees: &'a [&'a str],
  470. }
  471. client
  472. ._send_req(client.post(&url).json(&AssigneeReq { assignees: &[user] }))
  473. .await
  474. .map_err(AssignmentError::Http)?;
  475. Ok(())
  476. }
  477. }
  478. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  479. #[serde(rename_all = "lowercase")]
  480. pub enum PullRequestReviewAction {
  481. Submitted,
  482. Edited,
  483. Dismissed,
  484. }
  485. #[derive(Debug, serde::Deserialize)]
  486. pub struct PullRequestReviewEvent {
  487. pub action: PullRequestReviewAction,
  488. pub pull_request: Issue,
  489. pub review: Comment,
  490. pub repository: Repository,
  491. }
  492. #[derive(Debug, serde::Deserialize)]
  493. pub struct PullRequestReviewComment {
  494. pub action: IssueCommentAction,
  495. #[serde(rename = "pull_request")]
  496. pub issue: Issue,
  497. pub comment: Comment,
  498. pub repository: Repository,
  499. }
  500. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  501. #[serde(rename_all = "lowercase")]
  502. pub enum IssueCommentAction {
  503. Created,
  504. Edited,
  505. Deleted,
  506. }
  507. #[derive(Debug, serde::Deserialize)]
  508. pub struct IssueCommentEvent {
  509. pub action: IssueCommentAction,
  510. pub issue: Issue,
  511. pub comment: Comment,
  512. pub repository: Repository,
  513. }
  514. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  515. #[serde(rename_all = "lowercase")]
  516. pub enum IssuesAction {
  517. Opened,
  518. Edited,
  519. Deleted,
  520. Transferred,
  521. Pinned,
  522. Unpinned,
  523. Closed,
  524. Reopened,
  525. Assigned,
  526. Unassigned,
  527. Labeled,
  528. Unlabeled,
  529. Locked,
  530. Unlocked,
  531. Milestoned,
  532. Demilestoned,
  533. ReviewRequested,
  534. ReviewRequestRemoved,
  535. ReadyForReview,
  536. Synchronize,
  537. }
  538. #[derive(Debug, serde::Deserialize)]
  539. pub struct IssuesEvent {
  540. pub action: IssuesAction,
  541. #[serde(alias = "pull_request")]
  542. pub issue: Issue,
  543. pub repository: Repository,
  544. /// Some if action is IssuesAction::Labeled, for example
  545. pub label: Option<Label>,
  546. }
  547. #[derive(Debug, serde::Deserialize)]
  548. pub struct IssueSearchResult {
  549. pub total_count: usize,
  550. pub incomplete_results: bool,
  551. pub items: Vec<Issue>,
  552. }
  553. #[derive(Debug, serde::Deserialize)]
  554. pub struct Repository {
  555. pub full_name: String,
  556. }
  557. impl Repository {
  558. fn base_url(&self) -> &str {
  559. "https://api.github.com"
  560. }
  561. pub async fn get_issues(
  562. &self,
  563. client: &GithubClient,
  564. filters: &Vec<(&str, &str)>,
  565. include_labels: &Vec<&str>,
  566. exclude_labels: &Vec<&str>,
  567. ) -> anyhow::Result<Vec<Issue>> {
  568. let use_issues = exclude_labels.is_empty() || filters.iter().any(|&(key, _)| key == "no");
  569. // negating filters can only be handled by the search api
  570. let url = if use_issues {
  571. self.build_issues_url(filters, include_labels)
  572. } else {
  573. self.build_search_issues_url(filters, include_labels, exclude_labels)
  574. };
  575. let result = client.get(&url);
  576. if use_issues {
  577. client
  578. .json(result)
  579. .await
  580. .with_context(|| format!("failed to list issues from {}", url))
  581. } else {
  582. let result = client
  583. .json::<IssueSearchResult>(result)
  584. .await
  585. .with_context(|| format!("failed to list issues from {}", url))?;
  586. Ok(result.items)
  587. }
  588. }
  589. fn build_issues_url(&self, filters: &Vec<(&str, &str)>, include_labels: &Vec<&str>) -> String {
  590. let filters = filters
  591. .iter()
  592. .map(|(key, val)| format!("{}={}", key, val))
  593. .chain(std::iter::once(format!(
  594. "labels={}",
  595. include_labels.join(",")
  596. )))
  597. // if no state is defined, assume `state=all` so we don't fall back to the
  598. // `state=open` default of github.
  599. .chain(
  600. if filters.iter().any(|&(key, _)| key == "state") {
  601. None
  602. } else {
  603. Some("state=all".to_owned())
  604. }
  605. .into_iter(),
  606. )
  607. .chain(std::iter::once("filter=all".to_owned()))
  608. .collect::<Vec<_>>()
  609. .join("&");
  610. format!(
  611. "{}/repos/{}/issues?{}",
  612. self.base_url(),
  613. self.full_name,
  614. filters
  615. )
  616. }
  617. fn build_search_issues_url(
  618. &self,
  619. filters: &Vec<(&str, &str)>,
  620. include_labels: &Vec<&str>,
  621. exclude_labels: &Vec<&str>,
  622. ) -> String {
  623. let filters = filters
  624. .iter()
  625. .map(|(key, val)| format!("{}:{}", key, val))
  626. .chain(
  627. include_labels
  628. .iter()
  629. .map(|label| format!("label:{}", label)),
  630. )
  631. .chain(
  632. exclude_labels
  633. .iter()
  634. .map(|label| format!("-label:{}", label)),
  635. )
  636. .chain(std::iter::once(format!("repo:{}", self.full_name)))
  637. .collect::<Vec<_>>()
  638. .join("+");
  639. format!("{}/search/issues?q={}", self.base_url(), filters)
  640. }
  641. }
  642. #[derive(Debug)]
  643. pub enum Event {
  644. IssueComment(IssueCommentEvent),
  645. Issue(IssuesEvent),
  646. }
  647. impl Event {
  648. pub fn repo_name(&self) -> &str {
  649. match self {
  650. Event::IssueComment(event) => &event.repository.full_name,
  651. Event::Issue(event) => &event.repository.full_name,
  652. }
  653. }
  654. pub fn issue(&self) -> Option<&Issue> {
  655. match self {
  656. Event::IssueComment(event) => Some(&event.issue),
  657. Event::Issue(event) => Some(&event.issue),
  658. }
  659. }
  660. /// This will both extract from IssueComment events but also Issue events
  661. pub fn comment_body(&self) -> Option<&str> {
  662. match self {
  663. Event::Issue(e) => Some(&e.issue.body),
  664. Event::IssueComment(e) => Some(&e.comment.body),
  665. }
  666. }
  667. pub fn html_url(&self) -> Option<&str> {
  668. match self {
  669. Event::Issue(e) => Some(&e.issue.html_url),
  670. Event::IssueComment(e) => Some(&e.comment.html_url),
  671. }
  672. }
  673. pub fn user(&self) -> &User {
  674. match self {
  675. Event::Issue(e) => &e.issue.user,
  676. Event::IssueComment(e) => &e.comment.user,
  677. }
  678. }
  679. pub fn time(&self) -> chrono::DateTime<FixedOffset> {
  680. match self {
  681. Event::Issue(e) => e.issue.created_at.into(),
  682. Event::IssueComment(e) => e.comment.updated_at.into(),
  683. }
  684. }
  685. }
  686. trait RequestSend: Sized {
  687. fn configure(self, g: &GithubClient) -> Self;
  688. }
  689. impl RequestSend for RequestBuilder {
  690. fn configure(self, g: &GithubClient) -> RequestBuilder {
  691. self.header(USER_AGENT, "rust-lang-triagebot")
  692. .header(AUTHORIZATION, format!("token {}", g.token))
  693. }
  694. }
  695. #[derive(Clone)]
  696. pub struct GithubClient {
  697. token: String,
  698. client: Client,
  699. }
  700. impl GithubClient {
  701. pub fn new(client: Client, token: String) -> Self {
  702. GithubClient { client, token }
  703. }
  704. pub fn raw(&self) -> &Client {
  705. &self.client
  706. }
  707. pub async fn raw_file(
  708. &self,
  709. repo: &str,
  710. branch: &str,
  711. path: &str,
  712. ) -> anyhow::Result<Option<Vec<u8>>> {
  713. let url = format!(
  714. "https://raw.githubusercontent.com/{}/{}/{}",
  715. repo, branch, path
  716. );
  717. let req = self.get(&url);
  718. let req_dbg = format!("{:?}", req);
  719. let req = req
  720. .build()
  721. .with_context(|| format!("failed to build request {:?}", req_dbg))?;
  722. let mut resp = self.client.execute(req).await.context(req_dbg.clone())?;
  723. let status = resp.status();
  724. match status {
  725. StatusCode::OK => {
  726. let mut buf = Vec::with_capacity(resp.content_length().unwrap_or(4) as usize);
  727. while let Some(chunk) = resp.chunk().await.transpose() {
  728. let chunk = chunk
  729. .context("reading stream failed")
  730. .map_err(anyhow::Error::from)
  731. .context(req_dbg.clone())?;
  732. buf.extend_from_slice(&chunk);
  733. }
  734. Ok(Some(buf))
  735. }
  736. StatusCode::NOT_FOUND => Ok(None),
  737. status => anyhow::bail!("failed to GET {}: {}", url, status),
  738. }
  739. }
  740. fn get(&self, url: &str) -> RequestBuilder {
  741. log::trace!("get {:?}", url);
  742. self.client.get(url).configure(self)
  743. }
  744. fn patch(&self, url: &str) -> RequestBuilder {
  745. log::trace!("patch {:?}", url);
  746. self.client.patch(url).configure(self)
  747. }
  748. fn delete(&self, url: &str) -> RequestBuilder {
  749. log::trace!("delete {:?}", url);
  750. self.client.delete(url).configure(self)
  751. }
  752. fn post(&self, url: &str) -> RequestBuilder {
  753. log::trace!("post {:?}", url);
  754. self.client.post(url).configure(self)
  755. }
  756. fn put(&self, url: &str) -> RequestBuilder {
  757. log::trace!("put {:?}", url);
  758. self.client.put(url).configure(self)
  759. }
  760. pub async fn rust_commit(&self, sha: &str) -> Option<GithubCommit> {
  761. let req = self.get(&format!(
  762. "https://api.github.com/repos/rust-lang/rust/commits/{}",
  763. sha
  764. ));
  765. match self.json(req).await {
  766. Ok(r) => Some(r),
  767. Err(e) => {
  768. log::error!("Failed to query commit {:?}: {:?}", sha, e);
  769. None
  770. }
  771. }
  772. }
  773. /// This does not retrieve all of them, only the last several.
  774. pub async fn bors_commits(&self) -> Vec<GithubCommit> {
  775. let req = self.get("https://api.github.com/repos/rust-lang/rust/commits?author=bors");
  776. match self.json(req).await {
  777. Ok(r) => r,
  778. Err(e) => {
  779. log::error!("Failed to query commit list: {:?}", e);
  780. Vec::new()
  781. }
  782. }
  783. }
  784. }
  785. #[derive(Debug, serde::Deserialize)]
  786. pub struct GithubCommit {
  787. pub sha: String,
  788. pub commit: GitCommit,
  789. pub parents: Vec<Parent>,
  790. }
  791. #[derive(Debug, serde::Deserialize)]
  792. pub struct GitCommit {
  793. pub author: GitUser,
  794. }
  795. #[derive(Debug, serde::Deserialize)]
  796. pub struct GitUser {
  797. pub date: DateTime<FixedOffset>,
  798. }
  799. #[derive(Debug, serde::Deserialize)]
  800. pub struct Parent {
  801. pub sha: String,
  802. }