1use crate::HttpClient;
2use anyhow::{Context as _, Result, anyhow, bail};
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 pub digest: Option<String>,
12}
13
14#[derive(Deserialize, Debug)]
15pub struct GithubRelease {
16 pub tag_name: String,
17 #[serde(rename = "prerelease")]
18 pub pre_release: bool,
19 pub assets: Vec<GithubReleaseAsset>,
20 pub tarball_url: String,
21 pub zipball_url: String,
22}
23
24#[derive(Deserialize, Debug)]
25pub struct GithubReleaseAsset {
26 pub name: String,
27 pub browser_download_url: String,
28 pub digest: Option<String>,
29}
30
31pub async fn latest_github_release(
32 repo_name_with_owner: &str,
33 require_assets: bool,
34 pre_release: bool,
35 http: Arc<dyn HttpClient>,
36) -> anyhow::Result<GithubRelease> {
37 let mut response = http
38 .get(
39 format!("https://api.github.com/repos/{repo_name_with_owner}/releases").as_str(),
40 Default::default(),
41 true,
42 )
43 .await
44 .context("error fetching latest release")?;
45
46 let mut body = Vec::new();
47 response
48 .body_mut()
49 .read_to_end(&mut body)
50 .await
51 .context("error reading latest release")?;
52
53 if response.status().is_client_error() {
54 let text = String::from_utf8_lossy(body.as_slice());
55 bail!(
56 "status error {}, response: {text:?}",
57 response.status().as_u16()
58 );
59 }
60
61 let releases = match serde_json::from_slice::<Vec<GithubRelease>>(body.as_slice()) {
62 Ok(releases) => releases,
63
64 Err(err) => {
65 log::error!("Error deserializing: {err:?}");
66 log::error!(
67 "GitHub API response text: {:?}",
68 String::from_utf8_lossy(body.as_slice())
69 );
70 anyhow::bail!("error deserializing latest release: {err:?}");
71 }
72 };
73
74 let mut release = releases
75 .into_iter()
76 .filter(|release| !require_assets || !release.assets.is_empty())
77 .find(|release| release.pre_release == pre_release)
78 .context("finding a prerelease")?;
79 release.assets.iter_mut().for_each(|asset| {
80 if let Some(digest) = &mut asset.digest
81 && let Some(stripped) = digest.strip_prefix("sha256:") {
82 *digest = stripped.to_owned();
83 }
84 });
85 Ok(release)
86}
87
88pub async fn get_release_by_tag_name(
89 repo_name_with_owner: &str,
90 tag: &str,
91 http: Arc<dyn HttpClient>,
92) -> anyhow::Result<GithubRelease> {
93 let mut response = http
94 .get(
95 &format!("https://api.github.com/repos/{repo_name_with_owner}/releases/tags/{tag}"),
96 Default::default(),
97 true,
98 )
99 .await
100 .context("error fetching latest release")?;
101
102 let mut body = Vec::new();
103 let status = response.status();
104 response
105 .body_mut()
106 .read_to_end(&mut body)
107 .await
108 .context("error reading latest release")?;
109
110 if status.is_client_error() {
111 let text = String::from_utf8_lossy(body.as_slice());
112 bail!(
113 "status error {}, response: {text:?}",
114 response.status().as_u16()
115 );
116 }
117
118 let release = serde_json::from_slice::<GithubRelease>(body.as_slice()).map_err(|err| {
119 log::error!("Error deserializing: {err:?}");
120 log::error!(
121 "GitHub API response text: {:?}",
122 String::from_utf8_lossy(body.as_slice())
123 );
124 anyhow!("error deserializing GitHub release: {err:?}")
125 })?;
126
127 Ok(release)
128}
129
130#[derive(Debug, PartialEq, Eq, Clone, Copy)]
131pub enum AssetKind {
132 TarGz,
133 Gz,
134 Zip,
135}
136
137pub fn build_asset_url(repo_name_with_owner: &str, tag: &str, kind: AssetKind) -> Result<String> {
138 let mut url = Url::parse(&format!(
139 "https://github.com/{repo_name_with_owner}/archive/refs/tags",
140 ))?;
141 // We're pushing this here, because tags may contain `/` and other characters
142 // that need to be escaped.
143 let asset_filename = format!(
144 "{tag}.{extension}",
145 extension = match kind {
146 AssetKind::TarGz => "tar.gz",
147 AssetKind::Gz => "gz",
148 AssetKind::Zip => "zip",
149 }
150 );
151 url.path_segments_mut()
152 .map_err(|()| anyhow!("cannot modify url path segments"))?
153 .push(&asset_filename);
154 Ok(url.to_string())
155}
156
157#[cfg(test)]
158mod tests {
159 use crate::github::{AssetKind, build_asset_url};
160
161 #[test]
162 fn test_build_asset_url() {
163 let tag = "release/2.3.5";
164 let repo_name_with_owner = "microsoft/vscode-eslint";
165
166 let tarball = build_asset_url(repo_name_with_owner, tag, AssetKind::TarGz).unwrap();
167 assert_eq!(
168 tarball,
169 "https://github.com/microsoft/vscode-eslint/archive/refs/tags/release%2F2.3.5.tar.gz"
170 );
171
172 let zip = build_asset_url(repo_name_with_owner, tag, AssetKind::Zip).unwrap();
173 assert_eq!(
174 zip,
175 "https://github.com/microsoft/vscode-eslint/archive/refs/tags/release%2F2.3.5.zip"
176 );
177 }
178}