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 {
83 *digest = stripped.to_owned();
84 }
85 });
86 Ok(release)
87}
88
89pub async fn get_release_by_tag_name(
90 repo_name_with_owner: &str,
91 tag: &str,
92 http: Arc<dyn HttpClient>,
93) -> anyhow::Result<GithubRelease> {
94 let mut response = http
95 .get(
96 &format!("https://api.github.com/repos/{repo_name_with_owner}/releases/tags/{tag}"),
97 Default::default(),
98 true,
99 )
100 .await
101 .context("error fetching latest release")?;
102
103 let mut body = Vec::new();
104 let status = response.status();
105 response
106 .body_mut()
107 .read_to_end(&mut body)
108 .await
109 .context("error reading latest release")?;
110
111 if status.is_client_error() {
112 let text = String::from_utf8_lossy(body.as_slice());
113 bail!(
114 "status error {}, response: {text:?}",
115 response.status().as_u16()
116 );
117 }
118
119 let release = serde_json::from_slice::<GithubRelease>(body.as_slice()).map_err(|err| {
120 log::error!("Error deserializing: {err:?}");
121 log::error!(
122 "GitHub API response text: {:?}",
123 String::from_utf8_lossy(body.as_slice())
124 );
125 anyhow!("error deserializing GitHub release: {err:?}")
126 })?;
127
128 Ok(release)
129}
130
131#[derive(Debug, PartialEq, Eq, Clone, Copy)]
132pub enum AssetKind {
133 TarGz,
134 Gz,
135 Zip,
136}
137
138pub fn build_asset_url(repo_name_with_owner: &str, tag: &str, kind: AssetKind) -> Result<String> {
139 let mut url = Url::parse(&format!(
140 "https://github.com/{repo_name_with_owner}/archive/refs/tags",
141 ))?;
142 // We're pushing this here, because tags may contain `/` and other characters
143 // that need to be escaped.
144 let asset_filename = format!(
145 "{tag}.{extension}",
146 extension = match kind {
147 AssetKind::TarGz => "tar.gz",
148 AssetKind::Gz => "gz",
149 AssetKind::Zip => "zip",
150 }
151 );
152 url.path_segments_mut()
153 .map_err(|()| anyhow!("cannot modify url path segments"))?
154 .push(&asset_filename);
155 Ok(url.to_string())
156}
157
158#[cfg(test)]
159mod tests {
160 use crate::github::{AssetKind, build_asset_url};
161
162 #[test]
163 fn test_build_asset_url() {
164 let tag = "release/2.3.5";
165 let repo_name_with_owner = "microsoft/vscode-eslint";
166
167 let tarball = build_asset_url(repo_name_with_owner, tag, AssetKind::TarGz).unwrap();
168 assert_eq!(
169 tarball,
170 "https://github.com/microsoft/vscode-eslint/archive/refs/tags/release%2F2.3.5.tar.gz"
171 );
172
173 let zip = build_asset_url(repo_name_with_owner, tag, AssetKind::Zip).unwrap();
174 assert_eq!(
175 zip,
176 "https://github.com/microsoft/vscode-eslint/archive/refs/tags/release%2F2.3.5.zip"
177 );
178 }
179}