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