1use crate::http::HttpClient;
2use anyhow::{anyhow, bail, Context, Result};
3use futures::AsyncReadExt;
4use serde::Deserialize;
5use std::sync::Arc;
6use url::Url;
7
8pub struct GitHubLspBinaryVersion {
9 pub name: String,
10 pub url: String,
11}
12
13#[derive(Deserialize, Debug)]
14pub struct GithubRelease {
15 pub tag_name: String,
16 #[serde(rename = "prerelease")]
17 pub pre_release: bool,
18 pub assets: Vec<GithubReleaseAsset>,
19 pub tarball_url: String,
20 pub zipball_url: String,
21}
22
23#[derive(Deserialize, Debug)]
24pub struct GithubReleaseAsset {
25 pub name: String,
26 pub browser_download_url: String,
27}
28
29pub async fn latest_github_release(
30 repo_name_with_owner: &str,
31 require_assets: bool,
32 pre_release: bool,
33 http: Arc<dyn HttpClient>,
34) -> Result<GithubRelease, anyhow::Error> {
35 let mut response = http
36 .get(
37 &format!("https://api.github.com/repos/{repo_name_with_owner}/releases"),
38 Default::default(),
39 true,
40 )
41 .await
42 .context("error fetching latest release")?;
43
44 let mut body = Vec::new();
45 response
46 .body_mut()
47 .read_to_end(&mut body)
48 .await
49 .context("error reading latest release")?;
50
51 if response.status().is_client_error() {
52 let text = String::from_utf8_lossy(body.as_slice());
53 bail!(
54 "status error {}, response: {text:?}",
55 response.status().as_u16()
56 );
57 }
58
59 let releases = match serde_json::from_slice::<Vec<GithubRelease>>(body.as_slice()) {
60 Ok(releases) => releases,
61
62 Err(err) => {
63 log::error!("Error deserializing: {:?}", err);
64 log::error!(
65 "GitHub API response text: {:?}",
66 String::from_utf8_lossy(body.as_slice())
67 );
68 return Err(anyhow!("error deserializing latest release"));
69 }
70 };
71
72 releases
73 .into_iter()
74 .filter(|release| !require_assets || !release.assets.is_empty())
75 .find(|release| release.pre_release == pre_release)
76 .ok_or(anyhow!("Failed to find a release"))
77}
78
79pub async fn github_release_with_tag(
80 repo_name_with_owner: &str,
81 tag: &str,
82 http: Arc<dyn HttpClient>,
83) -> Result<GithubRelease, anyhow::Error> {
84 let url = build_tagged_release_url(repo_name_with_owner, tag)?;
85 let mut response = http
86 .get(&url, Default::default(), true)
87 .await
88 .with_context(|| format!("error fetching release {} of {}", tag, repo_name_with_owner))?;
89
90 let mut body = Vec::new();
91 response
92 .body_mut()
93 .read_to_end(&mut body)
94 .await
95 .with_context(|| {
96 format!(
97 "error reading response body for release {} of {}",
98 tag, repo_name_with_owner
99 )
100 })?;
101
102 if response.status().is_client_error() {
103 let text = String::from_utf8_lossy(body.as_slice());
104 bail!(
105 "status error {}, response: {text:?}",
106 response.status().as_u16()
107 );
108 }
109
110 match serde_json::from_slice::<GithubRelease>(body.as_slice()) {
111 Ok(release) => Ok(release),
112
113 Err(err) => {
114 log::error!("Error deserializing: {:?}", err);
115 log::error!(
116 "GitHub API response text: {:?}",
117 String::from_utf8_lossy(body.as_slice())
118 );
119 Err(anyhow!(
120 "error deserializing release {} of {}",
121 tag,
122 repo_name_with_owner
123 ))
124 }
125 }
126}
127
128fn build_tagged_release_url(repo_name_with_owner: &str, tag: &str) -> Result<String> {
129 let mut url = Url::parse(&format!(
130 "https://api.github.com/repos/{repo_name_with_owner}/releases/tags"
131 ))?;
132 // We're pushing this here, because tags may contain `/` and other characters
133 // that need to be escaped.
134 url.path_segments_mut()
135 .map_err(|_| anyhow!("cannot modify url path segments"))?
136 .push(tag);
137 Ok(url.to_string())
138}
139
140#[cfg(test)]
141mod tests {
142 use super::build_tagged_release_url;
143
144 #[test]
145 fn test_build_tagged_release_url() {
146 let tag = "release/2.2.20-Insider";
147 let repo_name_with_owner = "microsoft/vscode-eslint";
148
149 let have = build_tagged_release_url(repo_name_with_owner, tag).unwrap();
150
151 assert_eq!(have, "https://api.github.com/repos/microsoft/vscode-eslint/releases/tags/release%2F2.2.20-Insider");
152 }
153}