diff --git a/src/actions.rs b/src/actions.rs index 6bf49b95..4c4b35c9 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -3,7 +3,6 @@ use std::collections::HashMap; use std::sync::Arc; use async_trait::async_trait; -use reqwest::Client; use serde::{Deserialize, Serialize}; use tera::{Context, Tera}; @@ -93,7 +92,7 @@ pub fn to_human(d: DateTime) -> String { #[async_trait] impl<'a> Action for Step<'a> { async fn call(&self) -> anyhow::Result { - let gh = GithubClient::new_with_default_token(Client::new()); + let gh = GithubClient::new_from_env(); // retrieve all Rust compiler meetings // from today for 7 days diff --git a/src/github.rs b/src/github.rs index 040da27d..96b29f9e 100644 --- a/src/github.rs +++ b/src/github.rs @@ -118,7 +118,7 @@ impl GithubClient { .client .execute( self.client - .get("https://api.github.com/rate_limit") + .get(&format!("{}/rate_limit", self.api_url)) .configure(self) .build() .unwrap(), @@ -171,7 +171,9 @@ impl GithubClient { impl User { pub async fn current(client: &GithubClient) -> anyhow::Result { - client.json(client.get("https://api.github.com/user")).await + client + .json(client.get(&format!("{}/user", client.api_url))) + .await } pub async fn is_team_member<'a>(&'a self, client: &'a GithubClient) -> anyhow::Result { @@ -419,10 +421,10 @@ impl fmt::Display for IssueRepository { } impl IssueRepository { - fn url(&self) -> String { + fn url(&self, client: &GithubClient) -> String { format!( - "https://api.github.com/repos/{}/{}", - self.organization, self.repository + "{}/repos/{}/{}", + client.api_url, self.organization, self.repository ) } @@ -432,7 +434,7 @@ impl IssueRepository { async fn has_label(&self, client: &GithubClient, label: &str) -> anyhow::Result { #[allow(clippy::redundant_pattern_matching)] - let url = format!("{}/labels/{}", self.url(), label); + let url = format!("{}/labels/{}", self.url(client), label); match client.send_req(client.get(&url)).await { Ok(_) => Ok(true), Err(e) => { @@ -502,7 +504,7 @@ impl Issue { } pub async fn get_comment(&self, client: &GithubClient, id: usize) -> anyhow::Result { - let comment_url = format!("{}/issues/comments/{}", self.repository().url(), id); + let comment_url = format!("{}/issues/comments/{}", self.repository().url(client), id); let comment = client.json(client.get(&comment_url)).await?; Ok(comment) } @@ -511,7 +513,7 @@ impl Issue { pub async fn get_first_comment(&self, client: &GithubClient) -> anyhow::Result> { let comment_url = format!( "{}/issues/{}/comments?page=1&per_page=1", - self.repository().url(), + self.repository().url(client), self.number, ); Ok(client @@ -526,7 +528,7 @@ impl Issue { ) -> anyhow::Result> { let comment_url = format!( "{}/issues/{}/comments?page=1&per_page=100", - self.repository().url(), + self.repository().url(client), self.number, ); Ok(client @@ -535,7 +537,7 @@ impl Issue { } pub async fn edit_body(&self, client: &GithubClient, body: &str) -> anyhow::Result<()> { - let edit_url = format!("{}/issues/{}", self.repository().url(), self.number); + let edit_url = format!("{}/issues/{}", self.repository().url(client), self.number); #[derive(serde::Serialize)] struct ChangedIssue<'a> { body: &'a str, @@ -553,7 +555,7 @@ impl Issue { id: usize, new_body: &str, ) -> anyhow::Result<()> { - let comment_url = format!("{}/issues/comments/{}", self.repository().url(), id); + let comment_url = format!("{}/issues/comments/{}", self.repository().url(client), id); #[derive(serde::Serialize)] struct NewComment<'a> { body: &'a str, @@ -574,8 +576,13 @@ impl Issue { struct PostComment<'a> { body: &'a str, } + let comments_path = self + .comments_url + .strip_prefix("https://api.github.com") + .expect("expected api host"); + let comments_url = format!("{}{comments_path}", client.api_url); client - .send_req(client.post(&self.comments_url).json(&PostComment { body })) + .send_req(client.post(&comments_url).json(&PostComment { body })) .await .context("failed to post comment")?; Ok(()) @@ -586,7 +593,7 @@ impl Issue { // DELETE /repos/:owner/:repo/issues/:number/labels/{name} let url = format!( "{repo_url}/issues/{number}/labels/{name}", - repo_url = self.repository().url(), + repo_url = self.repository().url(client), number = self.number, name = label, ); @@ -618,7 +625,7 @@ impl Issue { // repo_url = https://api.github.com/repos/Codertocat/Hello-World let url = format!( "{repo_url}/issues/{number}/labels", - repo_url = self.repository().url(), + repo_url = self.repository().url(client), number = self.number ); @@ -685,7 +692,7 @@ impl Issue { log::info!("remove {:?} assignees for {}", selection, self.global_id()); let url = format!( "{repo_url}/issues/{number}/assignees", - repo_url = self.repository().url(), + repo_url = self.repository().url(client), number = self.number ); @@ -725,7 +732,7 @@ impl Issue { log::info!("add_assignee {} for {}", user, self.global_id()); let url = format!( "{repo_url}/issues/{number}/assignees", - repo_url = self.repository().url(), + repo_url = self.repository().url(client), number = self.number ); @@ -787,7 +794,7 @@ impl Issue { } pub async fn close(&self, client: &GithubClient) -> anyhow::Result<()> { - let edit_url = format!("{}/issues/{}", self.repository().url(), self.number); + let edit_url = format!("{}/issues/{}", self.repository().url(client), self.number); #[derive(serde::Serialize)] struct CloseIssue<'a> { state: &'a str, @@ -819,7 +826,10 @@ impl Issue { let diff = pr .files_changed .get_or_try_init::(|| async move { - let url = format!("{}/compare/{before}...{after}", self.repository().url()); + let url = format!( + "{}/compare/{before}...{after}", + self.repository().url(client) + ); let mut req = client.get(&url); req = req.header("Accept", "application/vnd.github.v3.diff"); let (diff, _) = client @@ -845,7 +855,7 @@ impl Issue { loop { let req = client.get(&format!( "{}/pulls/{}/commits?page={page}&per_page=100", - self.repository().url(), + self.repository().url(client), self.number )); @@ -867,7 +877,7 @@ impl Issue { let req = client.get(&format!( "{}/pulls/{}/files", - self.repository().url(), + self.repository().url(client), self.number )); Ok(client.json(req).await?) @@ -1055,11 +1065,8 @@ struct Ordering<'a> { } impl Repository { - const GITHUB_API_URL: &'static str = "https://api.github.com"; - const GITHUB_GRAPHQL_API_URL: &'static str = "https://api.github.com/graphql"; - - fn url(&self) -> String { - format!("{}/repos/{}", Repository::GITHUB_API_URL, self.full_name) + fn url(&self, client: &GithubClient) -> String { + format!("{}/repos/{}", client.api_url, self.full_name) } pub fn owner(&self) -> &str { @@ -1121,11 +1128,17 @@ impl Repository { let mut issues = vec![]; loop { let url = if use_search_api { - self.build_search_issues_url(&filters, include_labels, exclude_labels, ordering) + self.build_search_issues_url( + client, + &filters, + include_labels, + exclude_labels, + ordering, + ) } else if is_pr { - self.build_pulls_url(&filters, include_labels, ordering) + self.build_pulls_url(client, &filters, include_labels, ordering) } else { - self.build_issues_url(&filters, include_labels, ordering) + self.build_issues_url(client, &filters, include_labels, ordering) }; let result = client.get(&url); @@ -1154,24 +1167,27 @@ impl Repository { fn build_issues_url( &self, + client: &GithubClient, filters: &Vec<(&str, &str)>, include_labels: &Vec<&str>, ordering: Ordering<'_>, ) -> String { - self.build_endpoint_url("issues", filters, include_labels, ordering) + self.build_endpoint_url(client, "issues", filters, include_labels, ordering) } fn build_pulls_url( &self, + client: &GithubClient, filters: &Vec<(&str, &str)>, include_labels: &Vec<&str>, ordering: Ordering<'_>, ) -> String { - self.build_endpoint_url("pulls", filters, include_labels, ordering) + self.build_endpoint_url(client, "pulls", filters, include_labels, ordering) } fn build_endpoint_url( &self, + client: &GithubClient, endpoint: &str, filters: &Vec<(&str, &str)>, include_labels: &Vec<&str>, @@ -1194,15 +1210,13 @@ impl Repository { .join("&"); format!( "{}/repos/{}/{}?{}", - Repository::GITHUB_API_URL, - self.full_name, - endpoint, - filters + client.api_url, self.full_name, endpoint, filters ) } fn build_search_issues_url( &self, + client: &GithubClient, filters: &Vec<(&str, &str)>, include_labels: &Vec<&str>, exclude_labels: &Vec<&str>, @@ -1227,7 +1241,7 @@ impl Repository { .join("+"); format!( "{}/search/issues?q={}&sort={}&order={}&per_page={}&page={}", - Repository::GITHUB_API_URL, + client.api_url, filters, ordering.sort, ordering.direction, @@ -1247,7 +1261,10 @@ impl Repository { let mut commits = Vec::new(); let mut page = 1; loop { - let url = format!("{}/commits?sha={end}&per_page=100&page={page}", self.url()); + let url = format!( + "{}/commits?sha={end}&per_page=100&page={page}", + self.url(client) + ); let mut this_page: Vec = client .json(client.get(&url)) .await @@ -1265,7 +1282,7 @@ impl Repository { /// Retrieves a git commit for the given SHA. pub async fn git_commit(&self, client: &GithubClient, sha: &str) -> anyhow::Result { - let url = format!("{}/git/commits/{sha}", self.url()); + let url = format!("{}/git/commits/{sha}", self.url(client)); client .json(client.get(&url)) .await @@ -1280,7 +1297,7 @@ impl Repository { parents: &[&str], tree: &str, ) -> anyhow::Result { - let url = format!("{}/git/commits", self.url()); + let url = format!("{}/git/commits", self.url(client)); client .json(client.post(&url).json(&serde_json::json!({ "message": message, @@ -1297,7 +1314,7 @@ impl Repository { client: &GithubClient, refname: &str, ) -> anyhow::Result { - let url = format!("{}/git/ref/{}", self.url(), refname); + let url = format!("{}/git/ref/{}", self.url(client), refname); client .json(client.get(&url)) .await @@ -1311,7 +1328,7 @@ impl Repository { refname: &str, sha: &str, ) -> anyhow::Result { - let url = format!("{}/git/refs/{}", self.url(), refname); + let url = format!("{}/git/refs/{}", self.url(client), refname); client .json(client.patch(&url).json(&serde_json::json!({ "sha": sha, @@ -1362,7 +1379,7 @@ impl Repository { let query = RecentCommits::build(args.clone()); let data = client .json::>( - client.post(Repository::GITHUB_GRAPHQL_API_URL).json(&query), + client.post(&client.graphql_url).json(&query), ) .await .with_context(|| { @@ -1501,7 +1518,7 @@ impl Repository { base_tree: &str, tree: &[GitTreeEntry], ) -> anyhow::Result { - let url = format!("{}/git/trees", self.url()); + let url = format!("{}/git/trees", self.url(client)); client .json(client.post(&url).json(&serde_json::json!({ "base_tree": base_tree, @@ -1526,7 +1543,7 @@ impl Repository { path: &str, refname: Option<&str>, ) -> anyhow::Result { - let mut url = format!("{}/contents/{}", self.url(), path); + let mut url = format!("{}/contents/{}", self.url(client), path); if let Some(refname) = refname { url.push_str("?ref="); url.push_str(refname); @@ -1548,7 +1565,7 @@ impl Repository { base: &str, body: &str, ) -> anyhow::Result { - let url = format!("{}/pulls", self.url()); + let url = format!("{}/pulls", self.url(client)); let mut issue: Issue = client .json(client.post(&url).json(&serde_json::json!({ "title": title, @@ -1571,7 +1588,7 @@ impl Repository { /// /// **Warning**: This will to a force update if there are conflicts. pub async fn merge_upstream(&self, client: &GithubClient, branch: &str) -> anyhow::Result<()> { - let url = format!("{}/merge-upstream", self.url()); + let url = format!("{}/merge-upstream", self.url(client)); let merge_error = match client .send_req(client.post(&url).json(&serde_json::json!({ "branch": branch, @@ -1661,7 +1678,7 @@ impl Repository { } pub async fn get_issue(&self, client: &GithubClient, issue_num: u64) -> anyhow::Result { - let url = format!("{}/pulls/{issue_num}", self.url()); + let url = format!("{}/pulls/{issue_num}", self.url(client)); client .json(client.get(&url)) .await @@ -1939,15 +1956,32 @@ fn get_token_from_git_config() -> anyhow::Result { pub struct GithubClient { token: String, client: Client, + api_url: String, + graphql_url: String, + raw_url: String, } impl GithubClient { - pub fn new(client: Client, token: String) -> Self { - GithubClient { client, token } + pub fn new(token: String, api_url: String, graphql_url: String, raw_url: String) -> Self { + GithubClient { + client: Client::new(), + token, + api_url, + graphql_url, + raw_url, + } } - pub fn new_with_default_token(client: Client) -> Self { - Self::new(client, default_token_from_env()) + pub fn new_from_env() -> Self { + Self::new( + default_token_from_env(), + std::env::var("GITHUB_API_URL") + .unwrap_or_else(|_| "https://api.github.com".to_string()), + std::env::var("GITHUB_GRAPHQL_API_URL") + .unwrap_or_else(|_| "https://api.github.com/graphql".to_string()), + std::env::var("GITHUB_RAW_URL") + .unwrap_or_else(|_| "https://raw.githubusercontent.com".to_string()), + ) } pub fn raw(&self) -> &Client { @@ -1960,10 +1994,7 @@ impl GithubClient { branch: &str, path: &str, ) -> anyhow::Result> { - let url = format!( - "https://raw.githubusercontent.com/{}/{}/{}", - repo, branch, path - ); + let url = format!("{}/{repo}/{branch}/{path}", self.raw_url); let req = self.get(&url); let req_dbg = format!("{:?}", req); let req = req @@ -2025,8 +2056,8 @@ impl GithubClient { pub async fn rust_commit(&self, sha: &str) -> Option { let req = self.get(&format!( - "https://api.github.com/repos/rust-lang/rust/commits/{}", - sha + "{}/repos/rust-lang/rust/commits/{sha}", + self.api_url )); match self.json(req).await { Ok(r) => Some(r), @@ -2039,7 +2070,10 @@ impl GithubClient { /// This does not retrieve all of them, only the last several. pub async fn bors_commits(&self) -> Vec { - let req = self.get("https://api.github.com/repos/rust-lang/rust/commits?author=bors"); + let req = self.get(&format!( + "{}/repos/rust-lang/rust/commits?author=bors", + self.api_url + )); match self.json(req).await { Ok(r) => r, Err(e) => { @@ -2055,13 +2089,10 @@ impl GithubClient { query: &str, vars: serde_json::Value, ) -> anyhow::Result { - self.json( - self.post(Repository::GITHUB_GRAPHQL_API_URL) - .json(&serde_json::json!({ - "query": query, - "variables": vars, - })), - ) + self.json(self.post(&self.graphql_url).json(&serde_json::json!({ + "query": query, + "variables": vars, + }))) .await } @@ -2172,7 +2203,7 @@ impl GithubClient { /// /// The `full_name` should be something like `rust-lang/rust`. pub async fn repository(&self, full_name: &str) -> anyhow::Result { - let req = self.get(&format!("{}/repos/{full_name}", Repository::GITHUB_API_URL)); + let req = self.get(&format!("{}/repos/{full_name}", self.api_url)); self.json(req) .await .with_context(|| format!("{} failed to get repo", full_name)) @@ -2187,10 +2218,7 @@ impl GithubClient { title: &str, state: &str, ) -> anyhow::Result { - let url = format!( - "{}/repos/{full_repo_name}/milestones", - Repository::GITHUB_API_URL - ); + let url = format!("{}/repos/{full_repo_name}/milestones", self.api_url); let resp = self .send_req(self.post(&url).json(&serde_json::json!({ "title": title, @@ -2220,7 +2248,7 @@ impl GithubClient { loop { let url = format!( "{}/repos/{full_repo_name}/milestones?page={page}&state=all", - Repository::GITHUB_API_URL + self.api_url ); let milestones: Vec = self .json(self.get(&url)) @@ -2243,10 +2271,7 @@ impl GithubClient { milestone: &Milestone, issue_num: u64, ) -> anyhow::Result<()> { - let url = format!( - "{}/repos/{full_repo_name}/issues/{issue_num}", - Repository::GITHUB_API_URL - ); + let url = format!("{}/repos/{full_repo_name}/issues/{issue_num}", self.api_url); self.send_req(self.patch(&url).json(&serde_json::json!({ "milestone": milestone.number }))) @@ -2350,7 +2375,7 @@ impl IssuesQuery for LeastRecentlyReviewedPullRequests { }; loop { let query = queries::LeastRecentlyReviewedPullRequests::build(args.clone()); - let req = client.post(Repository::GITHUB_GRAPHQL_API_URL); + let req = client.post(&client.graphql_url); let req = req.json(&query); let data: cynic::GraphQlResponse = @@ -2485,7 +2510,7 @@ async fn project_items_by_status( let mut all_items = vec![]; loop { let query = project_items::Query::build(args.clone()); - let req = client.post(Repository::GITHUB_GRAPHQL_API_URL); + let req = client.post(&client.graphql_url); let req = req.json(&query); let data: cynic::GraphQlResponse = client.json(req).await?; diff --git a/src/handlers/docs_update.rs b/src/handlers/docs_update.rs index 80e5886b..d6a9168f 100644 --- a/src/handlers/docs_update.rs +++ b/src/handlers/docs_update.rs @@ -5,7 +5,6 @@ use crate::jobs::Job; use anyhow::Context; use anyhow::Result; use async_trait::async_trait; -use reqwest::Client; use std::fmt::Write; /// This is the repository where the commits will be created. @@ -66,7 +65,7 @@ impl Job for DocsUpdateJob { } pub async fn docs_update() -> Result> { - let gh = GithubClient::new_with_default_token(Client::new()); + let gh = GithubClient::new_from_env(); let dest_repo = gh.repository(DEST_REPO).await?; let work_repo = gh.repository(WORK_REPO).await?; diff --git a/src/main.rs b/src/main.rs index 9fa5dbf0..444ada99 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,6 @@ use anyhow::Context as _; use futures::future::FutureExt; use futures::StreamExt; use hyper::{header, Body, Request, Response, Server, StatusCode}; -use reqwest::Client; use route_recognizer::Router; use std::{env, net::SocketAddr, sync::Arc}; use tokio::{task, time}; @@ -247,8 +246,7 @@ async fn run_server(addr: SocketAddr) -> anyhow::Result<()> { .await .context("database migrations")?; - let client = Client::new(); - let gh = github::GithubClient::new_with_default_token(client.clone()); + let gh = github::GithubClient::new_from_env(); let oc = octocrab::OctocrabBuilder::new() .personal_token(github::default_token_from_env()) .build() diff --git a/src/team_data.rs b/src/team_data.rs index 4f7e1b15..d58ff408 100644 --- a/src/team_data.rs +++ b/src/team_data.rs @@ -4,7 +4,8 @@ use rust_team_data::v1::{Teams, ZulipMapping, BASE_URL}; use serde::de::DeserializeOwned; async fn by_url(client: &GithubClient, path: &str) -> anyhow::Result { - let url = format!("{}{}", BASE_URL, path); + let base = std::env::var("TEAMS_API_URL").unwrap_or(BASE_URL.to_string()); + let url = format!("{}{}", base, path); for _ in 0i32..3 { let map: Result = client.json(client.raw().get(&url)).await; match map {