github.rs 46 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567
  1. use anyhow::Context;
  2. use async_trait::async_trait;
  3. use chrono::{DateTime, FixedOffset, Utc};
  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::collections::HashMap;
  10. use std::convert::TryInto;
  11. use std::{
  12. fmt,
  13. time::{Duration, SystemTime},
  14. };
  15. use tracing as log;
  16. pub mod graphql;
  17. #[derive(Debug, PartialEq, Eq, serde::Deserialize)]
  18. pub struct User {
  19. pub login: String,
  20. pub id: Option<i64>,
  21. }
  22. impl GithubClient {
  23. async fn _send_req(&self, req: RequestBuilder) -> anyhow::Result<(Response, String)> {
  24. const MAX_ATTEMPTS: usize = 2;
  25. log::debug!("_send_req with {:?}", req);
  26. let req_dbg = format!("{:?}", req);
  27. let req = req
  28. .build()
  29. .with_context(|| format!("building reqwest {}", req_dbg))?;
  30. let mut resp = self.client.execute(req.try_clone().unwrap()).await?;
  31. if let Some(sleep) = Self::needs_retry(&resp).await {
  32. resp = self.retry(req, sleep, MAX_ATTEMPTS).await?;
  33. }
  34. resp.error_for_status_ref()?;
  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::UNIX_EPOCH.elapsed().unwrap().as_secs();
  57. reset_time.saturating_sub(epoch_time)
  58. }
  59. fn retry(
  60. &self,
  61. req: Request,
  62. sleep: Duration,
  63. remaining_attempts: usize,
  64. ) -> BoxFuture<Result<Response, reqwest::Error>> {
  65. #[derive(Debug, serde::Deserialize)]
  66. struct RateLimit {
  67. #[allow(unused)]
  68. pub limit: u64,
  69. pub remaining: u64,
  70. pub reset: u64,
  71. }
  72. #[derive(Debug, serde::Deserialize)]
  73. struct RateLimitResponse {
  74. pub resources: Resources,
  75. }
  76. #[derive(Debug, serde::Deserialize)]
  77. struct Resources {
  78. pub core: RateLimit,
  79. pub search: RateLimit,
  80. #[allow(unused)]
  81. pub graphql: RateLimit,
  82. #[allow(unused)]
  83. pub source_import: RateLimit,
  84. }
  85. log::warn!(
  86. "Retrying after {} seconds, remaining attepts {}",
  87. sleep.as_secs(),
  88. remaining_attempts,
  89. );
  90. async move {
  91. tokio::time::sleep(sleep).await;
  92. // check rate limit
  93. let rate_resp = self
  94. .client
  95. .execute(
  96. self.client
  97. .get("https://api.github.com/rate_limit")
  98. .configure(self)
  99. .build()
  100. .unwrap(),
  101. )
  102. .await?;
  103. let rate_limit_response = rate_resp.json::<RateLimitResponse>().await?;
  104. // Check url for search path because github has different rate limits for the search api
  105. let rate_limit = if req
  106. .url()
  107. .path_segments()
  108. .map(|mut segments| matches!(segments.next(), Some("search")))
  109. .unwrap_or(false)
  110. {
  111. rate_limit_response.resources.search
  112. } else {
  113. rate_limit_response.resources.core
  114. };
  115. // If we still don't have any more remaining attempts, try sleeping for the remaining
  116. // period of time
  117. if rate_limit.remaining == 0 {
  118. let sleep = Self::calc_sleep(rate_limit.reset);
  119. if sleep > 0 {
  120. tokio::time::sleep(Duration::from_secs(sleep)).await;
  121. }
  122. }
  123. let resp = self.client.execute(req.try_clone().unwrap()).await?;
  124. if let Some(sleep) = Self::needs_retry(&resp).await {
  125. if remaining_attempts > 0 {
  126. return self.retry(req, sleep, remaining_attempts - 1).await;
  127. }
  128. }
  129. Ok(resp)
  130. }
  131. .boxed()
  132. }
  133. async fn send_req(&self, req: RequestBuilder) -> anyhow::Result<Vec<u8>> {
  134. let (mut resp, req_dbg) = self._send_req(req).await?;
  135. let mut body = Vec::new();
  136. while let Some(chunk) = resp.chunk().await.transpose() {
  137. let chunk = chunk
  138. .context("reading stream failed")
  139. .map_err(anyhow::Error::from)
  140. .context(req_dbg.clone())?;
  141. body.extend_from_slice(&chunk);
  142. }
  143. Ok(body)
  144. }
  145. pub async fn json<T>(&self, req: RequestBuilder) -> anyhow::Result<T>
  146. where
  147. T: serde::de::DeserializeOwned,
  148. {
  149. let (resp, req_dbg) = self._send_req(req).await?;
  150. Ok(resp.json().await.context(req_dbg)?)
  151. }
  152. }
  153. impl User {
  154. pub async fn current(client: &GithubClient) -> anyhow::Result<Self> {
  155. client.json(client.get("https://api.github.com/user")).await
  156. }
  157. pub async fn is_team_member<'a>(&'a self, client: &'a GithubClient) -> anyhow::Result<bool> {
  158. log::trace!("Getting team membership for {:?}", self.login);
  159. let permission = crate::team_data::teams(client).await?;
  160. let map = permission.teams;
  161. let is_triager = map
  162. .get("wg-triage")
  163. .map_or(false, |w| w.members.iter().any(|g| g.github == self.login));
  164. let is_pri_member = map
  165. .get("wg-prioritization")
  166. .map_or(false, |w| w.members.iter().any(|g| g.github == self.login));
  167. let is_async_member = map
  168. .get("wg-async")
  169. .map_or(false, |w| w.members.iter().any(|g| g.github == self.login));
  170. let in_all = map["all"].members.iter().any(|g| g.github == self.login);
  171. log::trace!(
  172. "{:?} is all?={:?}, triager?={:?}, prioritizer?={:?}, async?={:?}",
  173. self.login,
  174. in_all,
  175. is_triager,
  176. is_pri_member,
  177. is_async_member,
  178. );
  179. Ok(in_all || is_triager || is_pri_member || is_async_member)
  180. }
  181. // Returns the ID of the given user, if the user is in the `all` team.
  182. pub async fn get_id<'a>(&'a self, client: &'a GithubClient) -> anyhow::Result<Option<usize>> {
  183. let permission = crate::team_data::teams(client).await?;
  184. let map = permission.teams;
  185. Ok(map["all"]
  186. .members
  187. .iter()
  188. .find(|g| g.github == self.login)
  189. .map(|u| u.github_id))
  190. }
  191. }
  192. pub async fn get_team(
  193. client: &GithubClient,
  194. team: &str,
  195. ) -> anyhow::Result<Option<rust_team_data::v1::Team>> {
  196. let permission = crate::team_data::teams(client).await?;
  197. let mut map = permission.teams;
  198. Ok(map.swap_remove(team))
  199. }
  200. #[derive(PartialEq, Eq, Debug, Clone, serde::Deserialize)]
  201. pub struct Label {
  202. pub name: String,
  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. #[serde(deserialize_with = "opt_string")]
  212. pub body: String,
  213. created_at: chrono::DateTime<Utc>,
  214. pub updated_at: chrono::DateTime<Utc>,
  215. #[serde(default)]
  216. pub merge_commit_sha: Option<String>,
  217. pub title: String,
  218. pub html_url: String,
  219. pub user: User,
  220. pub labels: Vec<Label>,
  221. pub assignees: Vec<User>,
  222. pub pull_request: Option<PullRequestDetails>,
  223. #[serde(default)]
  224. pub merged: bool,
  225. #[serde(default)]
  226. pub draft: bool,
  227. // API URL
  228. comments_url: String,
  229. #[serde(skip)]
  230. repository: OnceCell<IssueRepository>,
  231. #[serde(default)]
  232. base: Option<CommitBase>,
  233. #[serde(default)]
  234. head: Option<CommitBase>,
  235. }
  236. /// Contains only the parts of `Issue` that are needed for turning the issue title into a Zulip
  237. /// topic.
  238. #[derive(Clone, Debug, PartialEq, Eq)]
  239. pub struct ZulipGitHubReference {
  240. pub number: u64,
  241. pub title: String,
  242. pub repository: IssueRepository,
  243. }
  244. impl ZulipGitHubReference {
  245. pub fn zulip_topic_reference(&self) -> String {
  246. let repo = &self.repository;
  247. if repo.organization == "rust-lang" {
  248. if repo.repository == "rust" {
  249. format!("#{}", self.number)
  250. } else {
  251. format!("{}#{}", repo.repository, self.number)
  252. }
  253. } else {
  254. format!("{}/{}#{}", repo.organization, repo.repository, self.number)
  255. }
  256. }
  257. }
  258. #[derive(Debug, serde::Deserialize)]
  259. pub struct Comment {
  260. #[serde(deserialize_with = "opt_string")]
  261. pub body: String,
  262. pub html_url: String,
  263. pub user: User,
  264. #[serde(alias = "submitted_at")] // for pull request reviews
  265. pub updated_at: chrono::DateTime<Utc>,
  266. #[serde(default, rename = "state")]
  267. pub pr_review_state: Option<PullRequestReviewState>,
  268. }
  269. #[derive(Debug, serde::Deserialize, Eq, PartialEq)]
  270. #[serde(rename_all = "snake_case")]
  271. pub enum PullRequestReviewState {
  272. Approved,
  273. ChangesRequested,
  274. Commented,
  275. Dismissed,
  276. Pending,
  277. }
  278. fn opt_string<'de, D>(deserializer: D) -> Result<String, D::Error>
  279. where
  280. D: serde::de::Deserializer<'de>,
  281. {
  282. use serde::de::Deserialize;
  283. match <Option<String>>::deserialize(deserializer) {
  284. Ok(v) => Ok(v.unwrap_or_default()),
  285. Err(e) => Err(e),
  286. }
  287. }
  288. #[derive(Debug)]
  289. pub enum AssignmentError {
  290. InvalidAssignee,
  291. Http(anyhow::Error),
  292. }
  293. #[derive(Debug)]
  294. pub enum Selection<'a, T: ?Sized> {
  295. All,
  296. One(&'a T),
  297. Except(&'a T),
  298. }
  299. impl fmt::Display for AssignmentError {
  300. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  301. match self {
  302. AssignmentError::InvalidAssignee => write!(f, "invalid assignee"),
  303. AssignmentError::Http(e) => write!(f, "cannot assign: {}", e),
  304. }
  305. }
  306. }
  307. impl std::error::Error for AssignmentError {}
  308. #[derive(Debug, Clone, PartialEq, Eq)]
  309. pub struct IssueRepository {
  310. pub organization: String,
  311. pub repository: String,
  312. }
  313. impl fmt::Display for IssueRepository {
  314. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  315. write!(f, "{}/{}", self.organization, self.repository)
  316. }
  317. }
  318. impl IssueRepository {
  319. fn url(&self) -> String {
  320. format!(
  321. "https://api.github.com/repos/{}/{}",
  322. self.organization, self.repository
  323. )
  324. }
  325. async fn has_label(&self, client: &GithubClient, label: &str) -> anyhow::Result<bool> {
  326. #[allow(clippy::redundant_pattern_matching)]
  327. let url = format!("{}/labels/{}", self.url(), label);
  328. match client._send_req(client.get(&url)).await {
  329. Ok((_, _)) => Ok(true),
  330. Err(e) => {
  331. if e.downcast_ref::<reqwest::Error>()
  332. .map_or(false, |e| e.status() == Some(StatusCode::NOT_FOUND))
  333. {
  334. Ok(false)
  335. } else {
  336. Err(e)
  337. }
  338. }
  339. }
  340. }
  341. }
  342. #[derive(Debug)]
  343. pub(crate) struct UnknownLabels {
  344. labels: Vec<String>,
  345. }
  346. // NOTE: This is used to post the Github comment; make sure it's valid markdown.
  347. impl fmt::Display for UnknownLabels {
  348. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  349. write!(f, "Unknown labels: {}", &self.labels.join(", "))
  350. }
  351. }
  352. impl std::error::Error for UnknownLabels {}
  353. impl Issue {
  354. pub fn to_zulip_github_reference(&self) -> ZulipGitHubReference {
  355. ZulipGitHubReference {
  356. number: self.number,
  357. title: self.title.clone(),
  358. repository: self.repository().clone(),
  359. }
  360. }
  361. pub fn repository(&self) -> &IssueRepository {
  362. self.repository.get_or_init(|| {
  363. // https://api.github.com/repos/rust-lang/rust/issues/69257/comments
  364. log::trace!("get repository for {}", self.comments_url);
  365. let url = url::Url::parse(&self.comments_url).unwrap();
  366. let mut segments = url.path_segments().unwrap();
  367. let _comments = segments.next_back().unwrap();
  368. let _number = segments.next_back().unwrap();
  369. let _issues_or_prs = segments.next_back().unwrap();
  370. let repository = segments.next_back().unwrap();
  371. let organization = segments.next_back().unwrap();
  372. IssueRepository {
  373. organization: organization.into(),
  374. repository: repository.into(),
  375. }
  376. })
  377. }
  378. pub fn global_id(&self) -> String {
  379. format!("{}#{}", self.repository(), self.number)
  380. }
  381. pub fn is_pr(&self) -> bool {
  382. self.pull_request.is_some()
  383. }
  384. pub async fn get_comment(&self, client: &GithubClient, id: usize) -> anyhow::Result<Comment> {
  385. let comment_url = format!("{}/issues/comments/{}", self.repository().url(), id);
  386. let comment = client.json(client.get(&comment_url)).await?;
  387. Ok(comment)
  388. }
  389. pub async fn edit_body(&self, client: &GithubClient, body: &str) -> anyhow::Result<()> {
  390. let edit_url = format!("{}/issues/{}", self.repository().url(), self.number);
  391. #[derive(serde::Serialize)]
  392. struct ChangedIssue<'a> {
  393. body: &'a str,
  394. }
  395. client
  396. ._send_req(client.patch(&edit_url).json(&ChangedIssue { body }))
  397. .await
  398. .context("failed to edit issue body")?;
  399. Ok(())
  400. }
  401. pub async fn edit_comment(
  402. &self,
  403. client: &GithubClient,
  404. id: usize,
  405. new_body: &str,
  406. ) -> anyhow::Result<()> {
  407. let comment_url = format!("{}/issues/comments/{}", self.repository().url(), id);
  408. #[derive(serde::Serialize)]
  409. struct NewComment<'a> {
  410. body: &'a str,
  411. }
  412. client
  413. ._send_req(
  414. client
  415. .patch(&comment_url)
  416. .json(&NewComment { body: new_body }),
  417. )
  418. .await
  419. .context("failed to edit comment")?;
  420. Ok(())
  421. }
  422. pub async fn post_comment(&self, client: &GithubClient, body: &str) -> anyhow::Result<()> {
  423. #[derive(serde::Serialize)]
  424. struct PostComment<'a> {
  425. body: &'a str,
  426. }
  427. client
  428. ._send_req(client.post(&self.comments_url).json(&PostComment { body }))
  429. .await
  430. .context("failed to post comment")?;
  431. Ok(())
  432. }
  433. pub async fn remove_label(&self, client: &GithubClient, label: &str) -> anyhow::Result<()> {
  434. log::info!("remove_label from {}: {:?}", self.global_id(), label);
  435. // DELETE /repos/:owner/:repo/issues/:number/labels/{name}
  436. let url = format!(
  437. "{repo_url}/issues/{number}/labels/{name}",
  438. repo_url = self.repository().url(),
  439. number = self.number,
  440. name = label,
  441. );
  442. if !self.labels().iter().any(|l| l.name == label) {
  443. log::info!(
  444. "remove_label from {}: {:?} already not present, skipping",
  445. self.global_id(),
  446. label
  447. );
  448. return Ok(());
  449. }
  450. client
  451. ._send_req(client.delete(&url))
  452. .await
  453. .context("failed to delete label")?;
  454. Ok(())
  455. }
  456. pub async fn add_labels(
  457. &self,
  458. client: &GithubClient,
  459. labels: Vec<Label>,
  460. ) -> anyhow::Result<()> {
  461. log::info!("add_labels: {} +{:?}", self.global_id(), labels);
  462. // POST /repos/:owner/:repo/issues/:number/labels
  463. // repo_url = https://api.github.com/repos/Codertocat/Hello-World
  464. let url = format!(
  465. "{repo_url}/issues/{number}/labels",
  466. repo_url = self.repository().url(),
  467. number = self.number
  468. );
  469. // Don't try to add labels already present on this issue.
  470. let labels = labels
  471. .into_iter()
  472. .filter(|l| !self.labels().contains(&l))
  473. .map(|l| l.name)
  474. .collect::<Vec<_>>();
  475. log::info!("add_labels: {} filtered to {:?}", self.global_id(), labels);
  476. if labels.is_empty() {
  477. return Ok(());
  478. }
  479. let mut unknown_labels = vec![];
  480. let mut known_labels = vec![];
  481. for label in labels {
  482. if !self.repository().has_label(client, &label).await? {
  483. unknown_labels.push(label);
  484. } else {
  485. known_labels.push(label);
  486. }
  487. }
  488. if !unknown_labels.is_empty() {
  489. return Err(UnknownLabels {
  490. labels: unknown_labels,
  491. }
  492. .into());
  493. }
  494. #[derive(serde::Serialize)]
  495. struct LabelsReq {
  496. labels: Vec<String>,
  497. }
  498. client
  499. ._send_req(client.post(&url).json(&LabelsReq {
  500. labels: known_labels,
  501. }))
  502. .await
  503. .context("failed to add labels")?;
  504. Ok(())
  505. }
  506. pub fn labels(&self) -> &[Label] {
  507. &self.labels
  508. }
  509. pub fn contain_assignee(&self, user: &str) -> bool {
  510. self.assignees.iter().any(|a| a.login == user)
  511. }
  512. pub async fn remove_assignees(
  513. &self,
  514. client: &GithubClient,
  515. selection: Selection<'_, str>,
  516. ) -> Result<(), AssignmentError> {
  517. log::info!("remove {:?} assignees for {}", selection, self.global_id());
  518. let url = format!(
  519. "{repo_url}/issues/{number}/assignees",
  520. repo_url = self.repository().url(),
  521. number = self.number
  522. );
  523. let assignees = match selection {
  524. Selection::All => self
  525. .assignees
  526. .iter()
  527. .map(|u| u.login.as_str())
  528. .collect::<Vec<_>>(),
  529. Selection::One(user) => vec![user],
  530. Selection::Except(user) => self
  531. .assignees
  532. .iter()
  533. .map(|u| u.login.as_str())
  534. .filter(|&u| u != user)
  535. .collect::<Vec<_>>(),
  536. };
  537. #[derive(serde::Serialize)]
  538. struct AssigneeReq<'a> {
  539. assignees: &'a [&'a str],
  540. }
  541. client
  542. ._send_req(client.delete(&url).json(&AssigneeReq {
  543. assignees: &assignees[..],
  544. }))
  545. .await
  546. .map_err(AssignmentError::Http)?;
  547. Ok(())
  548. }
  549. pub async fn add_assignee(
  550. &self,
  551. client: &GithubClient,
  552. user: &str,
  553. ) -> Result<(), AssignmentError> {
  554. log::info!("add_assignee {} for {}", user, self.global_id());
  555. let url = format!(
  556. "{repo_url}/issues/{number}/assignees",
  557. repo_url = self.repository().url(),
  558. number = self.number
  559. );
  560. #[derive(serde::Serialize)]
  561. struct AssigneeReq<'a> {
  562. assignees: &'a [&'a str],
  563. }
  564. let result: Issue = client
  565. .json(client.post(&url).json(&AssigneeReq { assignees: &[user] }))
  566. .await
  567. .map_err(AssignmentError::Http)?;
  568. // Invalid assignees are silently ignored. We can just check if the user is now
  569. // contained in the assignees list.
  570. let success = result.assignees.iter().any(|u| u.login.as_str() == user);
  571. if success {
  572. Ok(())
  573. } else {
  574. Err(AssignmentError::InvalidAssignee)
  575. }
  576. }
  577. pub async fn set_assignee(
  578. &self,
  579. client: &GithubClient,
  580. user: &str,
  581. ) -> Result<(), AssignmentError> {
  582. log::info!("set_assignee for {} to {}", self.global_id(), user);
  583. self.add_assignee(client, user).await?;
  584. self.remove_assignees(client, Selection::Except(user))
  585. .await?;
  586. Ok(())
  587. }
  588. pub async fn set_milestone(&self, client: &GithubClient, title: &str) -> anyhow::Result<()> {
  589. log::trace!(
  590. "Setting milestone for rust-lang/rust#{} to {}",
  591. self.number,
  592. title
  593. );
  594. let create_url = format!("{}/milestones", self.repository().url());
  595. let resp = client
  596. .send_req(
  597. client
  598. .post(&create_url)
  599. .body(serde_json::to_vec(&MilestoneCreateBody { title }).unwrap()),
  600. )
  601. .await;
  602. // Explicitly do *not* try to return Err(...) if this fails -- that's
  603. // fine, it just means the milestone was already created.
  604. log::trace!("Created milestone: {:?}", resp);
  605. let list_url = format!("{}/milestones", self.repository().url());
  606. let milestone_list: Vec<Milestone> = client.json(client.get(&list_url)).await?;
  607. let milestone_no = if let Some(milestone) = milestone_list.iter().find(|v| v.title == title)
  608. {
  609. milestone.number
  610. } else {
  611. anyhow::bail!(
  612. "Despite just creating milestone {} on {}, it does not exist?",
  613. title,
  614. self.repository()
  615. )
  616. };
  617. #[derive(serde::Serialize)]
  618. struct SetMilestone {
  619. milestone: u64,
  620. }
  621. let url = format!("{}/issues/{}", self.repository().url(), self.number);
  622. client
  623. ._send_req(client.patch(&url).json(&SetMilestone {
  624. milestone: milestone_no,
  625. }))
  626. .await
  627. .context("failed to set milestone")?;
  628. Ok(())
  629. }
  630. pub async fn close(&self, client: &GithubClient) -> anyhow::Result<()> {
  631. let edit_url = format!("{}/issues/{}", self.repository().url(), self.number);
  632. #[derive(serde::Serialize)]
  633. struct CloseIssue<'a> {
  634. state: &'a str,
  635. }
  636. client
  637. ._send_req(
  638. client
  639. .patch(&edit_url)
  640. .json(&CloseIssue { state: "closed" }),
  641. )
  642. .await
  643. .context("failed to close issue")?;
  644. Ok(())
  645. }
  646. /// Returns the diff in this event, for Open and Synchronize events for now.
  647. pub async fn diff(&self, client: &GithubClient) -> anyhow::Result<Option<String>> {
  648. let (before, after) = if let (Some(base), Some(head)) = (&self.base, &self.head) {
  649. (base.sha.clone(), head.sha.clone())
  650. } else {
  651. return Ok(None);
  652. };
  653. let mut req = client.get(&format!(
  654. "{}/compare/{}...{}",
  655. self.repository().url(),
  656. before,
  657. after
  658. ));
  659. req = req.header("Accept", "application/vnd.github.v3.diff");
  660. let diff = client.send_req(req).await?;
  661. Ok(Some(String::from(String::from_utf8_lossy(&diff))))
  662. }
  663. pub async fn files(&self, client: &GithubClient) -> anyhow::Result<Vec<PullRequestFile>> {
  664. if !self.is_pr() {
  665. return Ok(vec![]);
  666. }
  667. let req = client.get(&format!(
  668. "{}/pulls/{}/files",
  669. self.repository().url(),
  670. self.number
  671. ));
  672. Ok(client.json(req).await?)
  673. }
  674. }
  675. #[derive(Debug, serde::Deserialize)]
  676. pub struct PullRequestFile {
  677. pub sha: String,
  678. pub filename: String,
  679. pub blob_url: String,
  680. }
  681. #[derive(serde::Serialize)]
  682. struct MilestoneCreateBody<'a> {
  683. title: &'a str,
  684. }
  685. #[derive(Debug, serde::Deserialize)]
  686. pub struct Milestone {
  687. number: u64,
  688. title: String,
  689. }
  690. #[derive(Debug, serde::Deserialize)]
  691. pub struct ChangeInner {
  692. pub from: String,
  693. }
  694. #[derive(Debug, serde::Deserialize)]
  695. pub struct Changes {
  696. pub title: Option<ChangeInner>,
  697. pub body: Option<ChangeInner>,
  698. }
  699. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  700. #[serde(rename_all = "lowercase")]
  701. pub enum PullRequestReviewAction {
  702. Submitted,
  703. Edited,
  704. Dismissed,
  705. }
  706. #[derive(Debug, serde::Deserialize)]
  707. pub struct PullRequestReviewEvent {
  708. pub action: PullRequestReviewAction,
  709. pub pull_request: Issue,
  710. pub review: Comment,
  711. pub changes: Option<Changes>,
  712. pub repository: Repository,
  713. }
  714. #[derive(Debug, serde::Deserialize)]
  715. pub struct PullRequestReviewComment {
  716. pub action: IssueCommentAction,
  717. pub changes: Option<Changes>,
  718. #[serde(rename = "pull_request")]
  719. pub issue: Issue,
  720. pub comment: Comment,
  721. pub repository: Repository,
  722. }
  723. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  724. #[serde(rename_all = "lowercase")]
  725. pub enum IssueCommentAction {
  726. Created,
  727. Edited,
  728. Deleted,
  729. }
  730. #[derive(Debug, serde::Deserialize)]
  731. pub struct IssueCommentEvent {
  732. pub action: IssueCommentAction,
  733. pub changes: Option<Changes>,
  734. pub issue: Issue,
  735. pub comment: Comment,
  736. pub repository: Repository,
  737. }
  738. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  739. #[serde(rename_all = "snake_case")]
  740. pub enum IssuesAction {
  741. Opened,
  742. Edited,
  743. Deleted,
  744. Transferred,
  745. Pinned,
  746. Unpinned,
  747. Closed,
  748. Reopened,
  749. Assigned,
  750. Unassigned,
  751. Labeled,
  752. Unlabeled,
  753. Locked,
  754. Unlocked,
  755. Milestoned,
  756. Demilestoned,
  757. ReviewRequested,
  758. ReviewRequestRemoved,
  759. ReadyForReview,
  760. Synchronize,
  761. ConvertedToDraft,
  762. }
  763. #[derive(Debug, serde::Deserialize)]
  764. pub struct IssuesEvent {
  765. pub action: IssuesAction,
  766. #[serde(alias = "pull_request")]
  767. pub issue: Issue,
  768. pub changes: Option<Changes>,
  769. pub repository: Repository,
  770. /// Some if action is IssuesAction::Labeled, for example
  771. pub label: Option<Label>,
  772. }
  773. #[derive(Debug, serde::Deserialize)]
  774. struct PullRequestEventFields {}
  775. #[derive(Clone, Debug, serde::Deserialize)]
  776. pub struct CommitBase {
  777. sha: String,
  778. }
  779. pub fn files_changed(diff: &str) -> Vec<&str> {
  780. let mut files = Vec::new();
  781. for line in diff.lines() {
  782. // mostly copied from highfive
  783. if line.starts_with("diff --git ") {
  784. files.push(
  785. line[line.find(" b/").unwrap()..]
  786. .strip_prefix(" b/")
  787. .unwrap(),
  788. );
  789. }
  790. }
  791. files
  792. }
  793. #[derive(Debug, serde::Deserialize)]
  794. pub struct IssueSearchResult {
  795. pub total_count: usize,
  796. pub incomplete_results: bool,
  797. pub items: Vec<Issue>,
  798. }
  799. #[derive(Clone, Debug, serde::Deserialize)]
  800. pub struct Repository {
  801. pub full_name: String,
  802. pub default_branch: String,
  803. }
  804. #[derive(Copy, Clone)]
  805. struct Ordering<'a> {
  806. pub sort: &'a str,
  807. pub direction: &'a str,
  808. pub per_page: &'a str,
  809. pub page: usize,
  810. }
  811. impl Repository {
  812. const GITHUB_API_URL: &'static str = "https://api.github.com";
  813. const GITHUB_GRAPHQL_API_URL: &'static str = "https://api.github.com/graphql";
  814. pub fn owner(&self) -> &str {
  815. self.full_name.split_once('/').unwrap().0
  816. }
  817. pub fn name(&self) -> &str {
  818. self.full_name.split_once('/').unwrap().1
  819. }
  820. pub async fn get_issues<'a>(
  821. &self,
  822. client: &GithubClient,
  823. query: &Query<'a>,
  824. ) -> anyhow::Result<Vec<Issue>> {
  825. let Query {
  826. filters,
  827. include_labels,
  828. exclude_labels,
  829. } = query;
  830. let mut ordering = Ordering {
  831. sort: "created",
  832. direction: "asc",
  833. per_page: "100",
  834. page: 1,
  835. };
  836. let filters: Vec<_> = filters
  837. .clone()
  838. .into_iter()
  839. .filter(|(key, val)| {
  840. match *key {
  841. "sort" => ordering.sort = val,
  842. "direction" => ordering.direction = val,
  843. "per_page" => ordering.per_page = val,
  844. _ => return true,
  845. };
  846. false
  847. })
  848. .collect();
  849. // `is: pull-request` indicates the query to retrieve PRs only
  850. let is_pr = filters
  851. .iter()
  852. .any(|&(key, value)| key == "is" && value == "pull-request");
  853. // There are some cases that can only be handled by the search API:
  854. // 1. When using negating label filters (exclude_labels)
  855. // 2. When there's a key parameter key=no
  856. // 3. When the query is to retrieve PRs only and there are label filters
  857. //
  858. // Check https://docs.github.com/en/rest/reference/search#search-issues-and-pull-requests
  859. // for more information
  860. let use_search_api = !exclude_labels.is_empty()
  861. || filters.iter().any(|&(key, _)| key == "no")
  862. || is_pr && !include_labels.is_empty();
  863. // If there are more than `per_page` of issues, we need to paginate
  864. let mut issues = vec![];
  865. loop {
  866. let url = if use_search_api {
  867. self.build_search_issues_url(&filters, include_labels, exclude_labels, ordering)
  868. } else if is_pr {
  869. self.build_pulls_url(&filters, include_labels, ordering)
  870. } else {
  871. self.build_issues_url(&filters, include_labels, ordering)
  872. };
  873. let result = client.get(&url);
  874. if use_search_api {
  875. let result = client
  876. .json::<IssueSearchResult>(result)
  877. .await
  878. .with_context(|| format!("failed to list issues from {}", url))?;
  879. issues.extend(result.items);
  880. if issues.len() < result.total_count {
  881. ordering.page += 1;
  882. continue;
  883. }
  884. } else {
  885. // FIXME: paginate with non-search
  886. issues = client
  887. .json(result)
  888. .await
  889. .with_context(|| format!("failed to list issues from {}", url))?
  890. }
  891. break;
  892. }
  893. Ok(issues)
  894. }
  895. fn build_issues_url(
  896. &self,
  897. filters: &Vec<(&str, &str)>,
  898. include_labels: &Vec<&str>,
  899. ordering: Ordering<'_>,
  900. ) -> String {
  901. self.build_endpoint_url("issues", filters, include_labels, ordering)
  902. }
  903. fn build_pulls_url(
  904. &self,
  905. filters: &Vec<(&str, &str)>,
  906. include_labels: &Vec<&str>,
  907. ordering: Ordering<'_>,
  908. ) -> String {
  909. self.build_endpoint_url("pulls", filters, include_labels, ordering)
  910. }
  911. fn build_endpoint_url(
  912. &self,
  913. endpoint: &str,
  914. filters: &Vec<(&str, &str)>,
  915. include_labels: &Vec<&str>,
  916. ordering: Ordering<'_>,
  917. ) -> String {
  918. let filters = filters
  919. .iter()
  920. .map(|(key, val)| format!("{}={}", key, val))
  921. .chain(std::iter::once(format!(
  922. "labels={}",
  923. include_labels.join(",")
  924. )))
  925. .chain(std::iter::once("filter=all".to_owned()))
  926. .chain(std::iter::once(format!("sort={}", ordering.sort,)))
  927. .chain(std::iter::once(
  928. format!("direction={}", ordering.direction,),
  929. ))
  930. .chain(std::iter::once(format!("per_page={}", ordering.per_page,)))
  931. .collect::<Vec<_>>()
  932. .join("&");
  933. format!(
  934. "{}/repos/{}/{}?{}",
  935. Repository::GITHUB_API_URL,
  936. self.full_name,
  937. endpoint,
  938. filters
  939. )
  940. }
  941. fn build_search_issues_url(
  942. &self,
  943. filters: &Vec<(&str, &str)>,
  944. include_labels: &Vec<&str>,
  945. exclude_labels: &Vec<&str>,
  946. ordering: Ordering<'_>,
  947. ) -> String {
  948. let filters = filters
  949. .iter()
  950. .filter(|&&(key, val)| !(key == "state" && val == "all"))
  951. .map(|(key, val)| format!("{}:{}", key, val))
  952. .chain(
  953. include_labels
  954. .iter()
  955. .map(|label| format!("label:{}", label)),
  956. )
  957. .chain(
  958. exclude_labels
  959. .iter()
  960. .map(|label| format!("-label:{}", label)),
  961. )
  962. .chain(std::iter::once(format!("repo:{}", self.full_name)))
  963. .collect::<Vec<_>>()
  964. .join("+");
  965. format!(
  966. "{}/search/issues?q={}&sort={}&order={}&per_page={}&page={}",
  967. Repository::GITHUB_API_URL,
  968. filters,
  969. ordering.sort,
  970. ordering.direction,
  971. ordering.per_page,
  972. ordering.page,
  973. )
  974. }
  975. }
  976. pub struct Query<'a> {
  977. // key/value filter
  978. pub filters: Vec<(&'a str, &'a str)>,
  979. pub include_labels: Vec<&'a str>,
  980. pub exclude_labels: Vec<&'a str>,
  981. }
  982. fn quote_reply(markdown: &str) -> String {
  983. if markdown.is_empty() {
  984. String::from("*No content*")
  985. } else {
  986. format!("\n\t> {}", markdown.replace("\n", "\n\t> "))
  987. }
  988. }
  989. #[async_trait]
  990. impl<'q> IssuesQuery for Query<'q> {
  991. async fn query<'a>(
  992. &'a self,
  993. repo: &'a Repository,
  994. include_fcp_details: bool,
  995. client: &'a GithubClient,
  996. ) -> anyhow::Result<Vec<crate::actions::IssueDecorator>> {
  997. let issues = repo
  998. .get_issues(&client, self)
  999. .await
  1000. .with_context(|| "Unable to get issues.")?;
  1001. let fcp_map = if include_fcp_details {
  1002. crate::rfcbot::get_all_fcps().await?
  1003. } else {
  1004. HashMap::new()
  1005. };
  1006. let mut issues_decorator = Vec::new();
  1007. for issue in issues {
  1008. let fcp_details = if include_fcp_details {
  1009. let repository_name = if let Some(repo) = issue.repository.get() {
  1010. repo.repository.clone()
  1011. } else {
  1012. let re = regex::Regex::new("https://github.com/rust-lang/|/").unwrap();
  1013. let split = re.split(&issue.html_url).collect::<Vec<&str>>();
  1014. split[1].to_string()
  1015. };
  1016. let key = format!(
  1017. "rust-lang/{}:{}:{}",
  1018. repository_name, issue.number, issue.title,
  1019. );
  1020. if let Some(fcp) = fcp_map.get(&key) {
  1021. let bot_tracking_comment_html_url = format!(
  1022. "{}#issuecomment-{}",
  1023. issue.html_url, fcp.fcp.fk_bot_tracking_comment
  1024. );
  1025. let bot_tracking_comment_content = quote_reply(&fcp.status_comment.body);
  1026. let fk_initiating_comment = fcp.fcp.fk_initiating_comment;
  1027. let init_comment = issue
  1028. .get_comment(&client, fk_initiating_comment.try_into()?)
  1029. .await?;
  1030. Some(crate::actions::FCPDetails {
  1031. bot_tracking_comment_html_url,
  1032. bot_tracking_comment_content,
  1033. initiating_comment_html_url: init_comment.html_url.clone(),
  1034. initiating_comment_content: quote_reply(&init_comment.body),
  1035. })
  1036. } else {
  1037. None
  1038. }
  1039. } else {
  1040. None
  1041. };
  1042. issues_decorator.push(crate::actions::IssueDecorator {
  1043. title: issue.title.clone(),
  1044. number: issue.number,
  1045. html_url: issue.html_url.clone(),
  1046. repo_name: repo.name().to_owned(),
  1047. labels: issue
  1048. .labels
  1049. .iter()
  1050. .map(|l| l.name.as_ref())
  1051. .collect::<Vec<_>>()
  1052. .join(", "),
  1053. assignees: issue
  1054. .assignees
  1055. .iter()
  1056. .map(|u| u.login.as_ref())
  1057. .collect::<Vec<_>>()
  1058. .join(", "),
  1059. updated_at_hts: crate::actions::to_human(issue.updated_at),
  1060. fcp_details,
  1061. });
  1062. }
  1063. Ok(issues_decorator)
  1064. }
  1065. }
  1066. #[derive(Debug, serde::Deserialize)]
  1067. #[serde(rename_all = "snake_case")]
  1068. pub enum CreateKind {
  1069. Branch,
  1070. Tag,
  1071. }
  1072. #[derive(Debug, serde::Deserialize)]
  1073. pub struct CreateEvent {
  1074. pub ref_type: CreateKind,
  1075. repository: Repository,
  1076. sender: User,
  1077. }
  1078. #[derive(Debug, serde::Deserialize)]
  1079. pub struct PushEvent {
  1080. #[serde(rename = "ref")]
  1081. pub git_ref: String,
  1082. repository: Repository,
  1083. sender: User,
  1084. }
  1085. #[derive(Debug)]
  1086. pub enum Event {
  1087. Create(CreateEvent),
  1088. IssueComment(IssueCommentEvent),
  1089. Issue(IssuesEvent),
  1090. Push(PushEvent),
  1091. }
  1092. impl Event {
  1093. pub fn repo(&self) -> &Repository {
  1094. match self {
  1095. Event::Create(event) => &event.repository,
  1096. Event::IssueComment(event) => &event.repository,
  1097. Event::Issue(event) => &event.repository,
  1098. Event::Push(event) => &event.repository,
  1099. }
  1100. }
  1101. pub fn issue(&self) -> Option<&Issue> {
  1102. match self {
  1103. Event::Create(_) => None,
  1104. Event::IssueComment(event) => Some(&event.issue),
  1105. Event::Issue(event) => Some(&event.issue),
  1106. Event::Push(_) => None,
  1107. }
  1108. }
  1109. /// This will both extract from IssueComment events but also Issue events
  1110. pub fn comment_body(&self) -> Option<&str> {
  1111. match self {
  1112. Event::Create(_) => None,
  1113. Event::Issue(e) => Some(&e.issue.body),
  1114. Event::IssueComment(e) => Some(&e.comment.body),
  1115. Event::Push(_) => None,
  1116. }
  1117. }
  1118. /// This will both extract from IssueComment events but also Issue events
  1119. pub fn comment_from(&self) -> Option<&str> {
  1120. match self {
  1121. Event::Create(_) => None,
  1122. Event::Issue(e) => Some(&e.changes.as_ref()?.body.as_ref()?.from),
  1123. Event::IssueComment(e) => Some(&e.changes.as_ref()?.body.as_ref()?.from),
  1124. Event::Push(_) => None,
  1125. }
  1126. }
  1127. pub fn html_url(&self) -> Option<&str> {
  1128. match self {
  1129. Event::Create(_) => None,
  1130. Event::Issue(e) => Some(&e.issue.html_url),
  1131. Event::IssueComment(e) => Some(&e.comment.html_url),
  1132. Event::Push(_) => None,
  1133. }
  1134. }
  1135. pub fn user(&self) -> &User {
  1136. match self {
  1137. Event::Create(e) => &e.sender,
  1138. Event::Issue(e) => &e.issue.user,
  1139. Event::IssueComment(e) => &e.comment.user,
  1140. Event::Push(e) => &e.sender,
  1141. }
  1142. }
  1143. pub fn time(&self) -> Option<chrono::DateTime<FixedOffset>> {
  1144. match self {
  1145. Event::Create(_) => None,
  1146. Event::Issue(e) => Some(e.issue.created_at.into()),
  1147. Event::IssueComment(e) => Some(e.comment.updated_at.into()),
  1148. Event::Push(_) => None,
  1149. }
  1150. }
  1151. }
  1152. trait RequestSend: Sized {
  1153. fn configure(self, g: &GithubClient) -> Self;
  1154. }
  1155. impl RequestSend for RequestBuilder {
  1156. fn configure(self, g: &GithubClient) -> RequestBuilder {
  1157. let mut auth = HeaderValue::from_maybe_shared(format!("token {}", g.token)).unwrap();
  1158. auth.set_sensitive(true);
  1159. self.header(USER_AGENT, "rust-lang-triagebot")
  1160. .header(AUTHORIZATION, &auth)
  1161. }
  1162. }
  1163. /// Finds the token in the user's environment, panicking if no suitable token
  1164. /// can be found.
  1165. pub fn default_token_from_env() -> String {
  1166. match std::env::var("GITHUB_API_TOKEN") {
  1167. Ok(v) => return v,
  1168. Err(_) => (),
  1169. }
  1170. match get_token_from_git_config() {
  1171. Ok(v) => return v,
  1172. Err(_) => (),
  1173. }
  1174. panic!("could not find token in GITHUB_API_TOKEN or .gitconfig/github.oath-token")
  1175. }
  1176. fn get_token_from_git_config() -> anyhow::Result<String> {
  1177. let output = std::process::Command::new("git")
  1178. .arg("config")
  1179. .arg("--get")
  1180. .arg("github.oauth-token")
  1181. .output()?;
  1182. if !output.status.success() {
  1183. anyhow::bail!("error received executing `git`: {:?}", output.status);
  1184. }
  1185. let git_token = String::from_utf8(output.stdout)?.trim().to_string();
  1186. Ok(git_token)
  1187. }
  1188. #[derive(Clone)]
  1189. pub struct GithubClient {
  1190. token: String,
  1191. client: Client,
  1192. }
  1193. impl GithubClient {
  1194. pub fn new(client: Client, token: String) -> Self {
  1195. GithubClient { client, token }
  1196. }
  1197. pub fn new_with_default_token(client: Client) -> Self {
  1198. Self::new(client, default_token_from_env())
  1199. }
  1200. pub fn raw(&self) -> &Client {
  1201. &self.client
  1202. }
  1203. pub async fn raw_file(
  1204. &self,
  1205. repo: &str,
  1206. branch: &str,
  1207. path: &str,
  1208. ) -> anyhow::Result<Option<Vec<u8>>> {
  1209. let url = format!(
  1210. "https://raw.githubusercontent.com/{}/{}/{}",
  1211. repo, branch, path
  1212. );
  1213. let req = self.get(&url);
  1214. let req_dbg = format!("{:?}", req);
  1215. let req = req
  1216. .build()
  1217. .with_context(|| format!("failed to build request {:?}", req_dbg))?;
  1218. let mut resp = self.client.execute(req).await.context(req_dbg.clone())?;
  1219. let status = resp.status();
  1220. match status {
  1221. StatusCode::OK => {
  1222. let mut buf = Vec::with_capacity(resp.content_length().unwrap_or(4) as usize);
  1223. while let Some(chunk) = resp.chunk().await.transpose() {
  1224. let chunk = chunk
  1225. .context("reading stream failed")
  1226. .map_err(anyhow::Error::from)
  1227. .context(req_dbg.clone())?;
  1228. buf.extend_from_slice(&chunk);
  1229. }
  1230. Ok(Some(buf))
  1231. }
  1232. StatusCode::NOT_FOUND => Ok(None),
  1233. status => anyhow::bail!("failed to GET {}: {}", url, status),
  1234. }
  1235. }
  1236. /// Get the raw gist content from the URL of the HTML version of the gist:
  1237. ///
  1238. /// `html_url` looks like `https://gist.github.com/rust-play/7e80ca3b1ec7abe08f60c41aff91f060`.
  1239. ///
  1240. /// `filename` is the name of the file you want the content of.
  1241. pub async fn raw_gist_from_url(
  1242. &self,
  1243. html_url: &str,
  1244. filename: &str,
  1245. ) -> anyhow::Result<String> {
  1246. let url = html_url.replace("github.com", "githubusercontent.com") + "/raw/" + filename;
  1247. let response = self.raw().get(&url).send().await?;
  1248. response.text().await.context("raw gist from url")
  1249. }
  1250. fn get(&self, url: &str) -> RequestBuilder {
  1251. log::trace!("get {:?}", url);
  1252. self.client.get(url).configure(self)
  1253. }
  1254. fn patch(&self, url: &str) -> RequestBuilder {
  1255. log::trace!("patch {:?}", url);
  1256. self.client.patch(url).configure(self)
  1257. }
  1258. fn delete(&self, url: &str) -> RequestBuilder {
  1259. log::trace!("delete {:?}", url);
  1260. self.client.delete(url).configure(self)
  1261. }
  1262. fn post(&self, url: &str) -> RequestBuilder {
  1263. log::trace!("post {:?}", url);
  1264. self.client.post(url).configure(self)
  1265. }
  1266. #[allow(unused)]
  1267. fn put(&self, url: &str) -> RequestBuilder {
  1268. log::trace!("put {:?}", url);
  1269. self.client.put(url).configure(self)
  1270. }
  1271. pub async fn rust_commit(&self, sha: &str) -> Option<GithubCommit> {
  1272. let req = self.get(&format!(
  1273. "https://api.github.com/repos/rust-lang/rust/commits/{}",
  1274. sha
  1275. ));
  1276. match self.json(req).await {
  1277. Ok(r) => Some(r),
  1278. Err(e) => {
  1279. log::error!("Failed to query commit {:?}: {:?}", sha, e);
  1280. None
  1281. }
  1282. }
  1283. }
  1284. /// This does not retrieve all of them, only the last several.
  1285. pub async fn bors_commits(&self) -> Vec<GithubCommit> {
  1286. let req = self.get("https://api.github.com/repos/rust-lang/rust/commits?author=bors");
  1287. match self.json(req).await {
  1288. Ok(r) => r,
  1289. Err(e) => {
  1290. log::error!("Failed to query commit list: {:?}", e);
  1291. Vec::new()
  1292. }
  1293. }
  1294. }
  1295. }
  1296. #[derive(Debug, serde::Deserialize)]
  1297. pub struct GithubCommit {
  1298. pub sha: String,
  1299. pub commit: GitCommit,
  1300. pub parents: Vec<Parent>,
  1301. }
  1302. #[derive(Debug, serde::Deserialize)]
  1303. pub struct GitCommit {
  1304. pub author: GitUser,
  1305. pub message: String,
  1306. }
  1307. #[derive(Debug, serde::Deserialize)]
  1308. pub struct GitUser {
  1309. pub date: DateTime<FixedOffset>,
  1310. }
  1311. #[derive(Debug, serde::Deserialize)]
  1312. pub struct Parent {
  1313. pub sha: String,
  1314. }
  1315. #[async_trait]
  1316. pub trait IssuesQuery {
  1317. async fn query<'a>(
  1318. &'a self,
  1319. repo: &'a Repository,
  1320. include_fcp_details: bool,
  1321. client: &'a GithubClient,
  1322. ) -> anyhow::Result<Vec<crate::actions::IssueDecorator>>;
  1323. }
  1324. #[cfg(test)]
  1325. mod tests {
  1326. use super::*;
  1327. #[test]
  1328. fn display_labels() {
  1329. let x = UnknownLabels {
  1330. labels: vec!["A-bootstrap".into(), "xxx".into()],
  1331. };
  1332. assert_eq!(x.to_string(), "Unknown labels: A-bootstrap, xxx");
  1333. }
  1334. #[test]
  1335. fn extract_one_file() {
  1336. let input = r##"\
  1337. diff --git a/triagebot.toml b/triagebot.toml
  1338. index fb9cee43b2d..b484c25ea51 100644
  1339. --- a/triagebot.toml
  1340. +++ b/triagebot.toml
  1341. @@ -114,6 +114,15 @@ trigger_files = [
  1342. "src/tools/rustdoc-themes",
  1343. ]
  1344. +[autolabel."T-compiler"]
  1345. +trigger_files = [
  1346. + # Source code
  1347. + "compiler",
  1348. +
  1349. + # Tests
  1350. + "src/test/ui",
  1351. +]
  1352. +
  1353. [notify-zulip."I-prioritize"]
  1354. zulip_stream = 245100 # #t-compiler/wg-prioritization/alerts
  1355. topic = "#{number} {title}"
  1356. "##;
  1357. assert_eq!(files_changed(input), vec!["triagebot.toml".to_string()]);
  1358. }
  1359. #[test]
  1360. fn extract_several_files() {
  1361. let input = r##"\
  1362. diff --git a/library/stdarch b/library/stdarch
  1363. index b70ae88ef2a..cfba59fccd9 160000
  1364. --- a/library/stdarch
  1365. +++ b/library/stdarch
  1366. @@ -1 +1 @@
  1367. -Subproject commit b70ae88ef2a6c83acad0a1e83d5bd78f9655fd05
  1368. +Subproject commit cfba59fccd90b3b52a614120834320f764ab08d1
  1369. diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs
  1370. index 1fe4aa9023e..f0330f1e424 100644
  1371. --- a/src/librustdoc/clean/types.rs
  1372. +++ b/src/librustdoc/clean/types.rs
  1373. @@ -2322,3 +2322,4 @@ impl SubstParam {
  1374. if let Self::Lifetime(lt) = self { Some(lt) } else { None }
  1375. }
  1376. }
  1377. +
  1378. diff --git a/src/librustdoc/core.rs b/src/librustdoc/core.rs
  1379. index c58310947d2..3b0854d4a9b 100644
  1380. --- a/src/librustdoc/core.rs
  1381. +++ b/src/librustdoc/core.rs
  1382. @@ -591,3 +591,4 @@ fn from(idx: u32) -> Self {
  1383. ImplTraitParam::ParamIndex(idx)
  1384. }
  1385. }
  1386. +
  1387. "##;
  1388. assert_eq!(
  1389. files_changed(input),
  1390. vec![
  1391. "library/stdarch".to_string(),
  1392. "src/librustdoc/clean/types.rs".to_string(),
  1393. "src/librustdoc/core.rs".to_string(),
  1394. ]
  1395. )
  1396. }
  1397. }