github.rs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837
  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. eprintln!(
  26. "Need to retry request {}. Sleeping for {}",
  27. req_dbg,
  28. sleep.as_secs()
  29. );
  30. resp = self.retry(req, sleep, MAX_ATTEMPTS).await?;
  31. }
  32. if let Err(e) = resp.error_for_status_ref() {
  33. return Err(e);
  34. }
  35. Ok((resp, req_dbg))
  36. }
  37. async fn needs_retry(resp: &Response) -> Option<Duration> {
  38. const REMAINING: &str = "X-RateLimit-Remaining";
  39. const RESET: &str = "X-RateLimit-Reset";
  40. if resp.status().is_success() {
  41. return None;
  42. }
  43. let headers = resp.headers();
  44. if !(headers.contains_key(REMAINING) && headers.contains_key(RESET)) {
  45. return None;
  46. }
  47. // Weird github api behavior. It asks us to retry but also has a remaining count above 1
  48. // Try again immediately and hope for the best...
  49. if headers[REMAINING] != "0" {
  50. return Some(Duration::from_secs(0));
  51. }
  52. let reset_time = headers[RESET].to_str().unwrap().parse::<u64>().unwrap();
  53. Some(Duration::from_secs(Self::calc_sleep(reset_time) + 10))
  54. }
  55. fn calc_sleep(reset_time: u64) -> u64 {
  56. let epoch_time = SystemTime::now()
  57. .duration_since(SystemTime::UNIX_EPOCH)
  58. .unwrap()
  59. .as_secs();
  60. reset_time.saturating_sub(epoch_time)
  61. }
  62. fn retry(
  63. &self,
  64. req: Request,
  65. sleep: Duration,
  66. remaining_attempts: usize,
  67. ) -> BoxFuture<Result<Response, reqwest::Error>> {
  68. #[derive(Debug, serde::Deserialize)]
  69. struct RateLimit {
  70. pub limit: u64,
  71. pub remaining: u64,
  72. pub reset: u64,
  73. }
  74. #[derive(Debug, serde::Deserialize)]
  75. struct RateLimitResponse {
  76. pub resources: Resources,
  77. }
  78. #[derive(Debug, serde::Deserialize)]
  79. struct Resources {
  80. pub core: RateLimit,
  81. pub search: RateLimit,
  82. pub graphql: RateLimit,
  83. pub source_import: RateLimit,
  84. }
  85. async move {
  86. eprintln!("Sleeping for {}", sleep.as_secs());
  87. tokio::time::delay_for(sleep).await;
  88. eprintln!("Done sleeping");
  89. // check rate limit
  90. let rate_resp = self
  91. .client
  92. .execute(
  93. self.client
  94. .get("https://api.github.com/rate_limit")
  95. .configure(self)
  96. .build()
  97. .unwrap(),
  98. )
  99. .await?;
  100. let search_rate_limit = rate_resp
  101. .json::<RateLimitResponse>()
  102. .await?
  103. .resources
  104. .search;
  105. eprintln!("search rate limit info: {:?}", search_rate_limit);
  106. // If we still don't have any more remaining attempts, try sleeping for the remaining
  107. // period of time
  108. if search_rate_limit.remaining == 0 {
  109. let sleep = Self::calc_sleep(search_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>,
  565. ) -> anyhow::Result<IssueSearchResult> {
  566. let filters = filters.join("+");
  567. let url = format!("{}/search/issues?q={}", self.base_url(), filters);
  568. let result = client.get(&url);
  569. client
  570. .json(result)
  571. .await
  572. .with_context(|| format!("failed to list issues from {}", url))
  573. }
  574. }
  575. #[derive(Debug)]
  576. pub enum Event {
  577. IssueComment(IssueCommentEvent),
  578. Issue(IssuesEvent),
  579. }
  580. impl Event {
  581. pub fn repo_name(&self) -> &str {
  582. match self {
  583. Event::IssueComment(event) => &event.repository.full_name,
  584. Event::Issue(event) => &event.repository.full_name,
  585. }
  586. }
  587. pub fn issue(&self) -> Option<&Issue> {
  588. match self {
  589. Event::IssueComment(event) => Some(&event.issue),
  590. Event::Issue(event) => Some(&event.issue),
  591. }
  592. }
  593. /// This will both extract from IssueComment events but also Issue events
  594. pub fn comment_body(&self) -> Option<&str> {
  595. match self {
  596. Event::Issue(e) => Some(&e.issue.body),
  597. Event::IssueComment(e) => Some(&e.comment.body),
  598. }
  599. }
  600. pub fn html_url(&self) -> Option<&str> {
  601. match self {
  602. Event::Issue(e) => Some(&e.issue.html_url),
  603. Event::IssueComment(e) => Some(&e.comment.html_url),
  604. }
  605. }
  606. pub fn user(&self) -> &User {
  607. match self {
  608. Event::Issue(e) => &e.issue.user,
  609. Event::IssueComment(e) => &e.comment.user,
  610. }
  611. }
  612. pub fn time(&self) -> chrono::DateTime<FixedOffset> {
  613. match self {
  614. Event::Issue(e) => e.issue.created_at.into(),
  615. Event::IssueComment(e) => e.comment.updated_at.into(),
  616. }
  617. }
  618. }
  619. trait RequestSend: Sized {
  620. fn configure(self, g: &GithubClient) -> Self;
  621. }
  622. impl RequestSend for RequestBuilder {
  623. fn configure(self, g: &GithubClient) -> RequestBuilder {
  624. self.header(USER_AGENT, "rust-lang-triagebot")
  625. .header(AUTHORIZATION, format!("token {}", g.token))
  626. }
  627. }
  628. #[derive(Clone)]
  629. pub struct GithubClient {
  630. token: String,
  631. client: Client,
  632. }
  633. impl GithubClient {
  634. pub fn new(client: Client, token: String) -> Self {
  635. GithubClient { client, token }
  636. }
  637. pub fn raw(&self) -> &Client {
  638. &self.client
  639. }
  640. pub async fn raw_file(
  641. &self,
  642. repo: &str,
  643. branch: &str,
  644. path: &str,
  645. ) -> anyhow::Result<Option<Vec<u8>>> {
  646. let url = format!(
  647. "https://raw.githubusercontent.com/{}/{}/{}",
  648. repo, branch, path
  649. );
  650. let req = self.get(&url);
  651. let req_dbg = format!("{:?}", req);
  652. let req = req
  653. .build()
  654. .with_context(|| format!("failed to build request {:?}", req_dbg))?;
  655. let mut resp = self.client.execute(req).await.context(req_dbg.clone())?;
  656. let status = resp.status();
  657. match status {
  658. StatusCode::OK => {
  659. let mut buf = Vec::with_capacity(resp.content_length().unwrap_or(4) as usize);
  660. while let Some(chunk) = resp.chunk().await.transpose() {
  661. let chunk = chunk
  662. .context("reading stream failed")
  663. .map_err(anyhow::Error::from)
  664. .context(req_dbg.clone())?;
  665. buf.extend_from_slice(&chunk);
  666. }
  667. Ok(Some(buf))
  668. }
  669. StatusCode::NOT_FOUND => Ok(None),
  670. status => anyhow::bail!("failed to GET {}: {}", url, status),
  671. }
  672. }
  673. fn get(&self, url: &str) -> RequestBuilder {
  674. log::trace!("get {:?}", url);
  675. self.client.get(url).configure(self)
  676. }
  677. fn patch(&self, url: &str) -> RequestBuilder {
  678. log::trace!("patch {:?}", url);
  679. self.client.patch(url).configure(self)
  680. }
  681. fn delete(&self, url: &str) -> RequestBuilder {
  682. log::trace!("delete {:?}", url);
  683. self.client.delete(url).configure(self)
  684. }
  685. fn post(&self, url: &str) -> RequestBuilder {
  686. log::trace!("post {:?}", url);
  687. self.client.post(url).configure(self)
  688. }
  689. fn put(&self, url: &str) -> RequestBuilder {
  690. log::trace!("put {:?}", url);
  691. self.client.put(url).configure(self)
  692. }
  693. pub async fn rust_commit(&self, sha: &str) -> Option<GithubCommit> {
  694. let req = self.get(&format!(
  695. "https://api.github.com/repos/rust-lang/rust/commits/{}",
  696. sha
  697. ));
  698. match self.json(req).await {
  699. Ok(r) => Some(r),
  700. Err(e) => {
  701. log::error!("Failed to query commit {:?}: {:?}", sha, e);
  702. None
  703. }
  704. }
  705. }
  706. /// This does not retrieve all of them, only the last several.
  707. pub async fn bors_commits(&self) -> Vec<GithubCommit> {
  708. let req = self.get("https://api.github.com/repos/rust-lang/rust/commits?author=bors");
  709. match self.json(req).await {
  710. Ok(r) => r,
  711. Err(e) => {
  712. log::error!("Failed to query commit list: {:?}", e);
  713. Vec::new()
  714. }
  715. }
  716. }
  717. }
  718. #[derive(Debug, serde::Deserialize)]
  719. pub struct GithubCommit {
  720. pub sha: String,
  721. pub commit: GitCommit,
  722. pub parents: Vec<Parent>,
  723. }
  724. #[derive(Debug, serde::Deserialize)]
  725. pub struct GitCommit {
  726. pub author: GitUser,
  727. }
  728. #[derive(Debug, serde::Deserialize)]
  729. pub struct GitUser {
  730. pub date: DateTime<FixedOffset>,
  731. }
  732. #[derive(Debug, serde::Deserialize)]
  733. pub struct Parent {
  734. pub sha: String,
  735. }