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