github.rs 29 KB

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