Skip to content

Commit f760f2c

Browse files
authored
Support GITHUB_TOKEN (zed-extensions#282)
- Add support for GITHUB_TOKEN/GH_TOKEN env vars in get_latest_versions_from_tag to avoid GitHub API rate limiting (60/hr -> 5000/hr) - Add unit tests for CheckUpdates logic
1 parent 8aa3418 commit f760f2c

7 files changed

Lines changed: 131 additions & 25 deletions

File tree

src/debugger.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,14 +420,15 @@ impl Downloadable for Debugger {
420420
self.plugin_path.is_some()
421421
}
422422

423-
fn fetch_latest_version(&self) -> zed::Result<String> {
423+
fn fetch_latest_version(&self, _worktree: &Worktree) -> zed::Result<String> {
424424
Ok("0.53.2".to_string())
425425
}
426426

427427
fn download(
428428
&mut self,
429429
_version: &str,
430430
language_server_id: &LanguageServerId,
431+
_worktree: &Worktree,
431432
) -> zed::Result<PathBuf> {
432433
self.get_or_download_fork(language_server_id)
433434
}

src/downloadable.rs

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@ pub trait Downloadable {
1111

1212
fn loaded(&self) -> bool;
1313

14-
fn fetch_latest_version(&self) -> zed::Result<String>;
14+
fn fetch_latest_version(&self, worktree: &Worktree) -> zed::Result<String>;
1515

1616
fn download(
1717
&mut self,
1818
version: &str,
1919
language_server_id: &LanguageServerId,
20+
worktree: &Worktree,
2021
) -> zed::Result<PathBuf>;
2122

2223
fn get_or_download(
@@ -36,8 +37,8 @@ pub trait Downloadable {
3637
}
3738

3839
let downloaded = self
39-
.fetch_latest_version()
40-
.and_then(|version| self.download(&version, language_server_id));
40+
.fetch_latest_version(worktree)
41+
.and_then(|version| self.download(&version, language_server_id, worktree));
4142

4243
match downloaded {
4344
Ok(path) => Ok(path),
@@ -64,3 +65,54 @@ pub trait Downloadable {
6465
None
6566
}
6667
}
68+
69+
#[cfg(test)]
70+
mod fallback_tests {
71+
use std::path::PathBuf;
72+
73+
use serde_json::json;
74+
75+
use super::*;
76+
77+
#[test]
78+
fn test_check_updates_always_allows_download() {
79+
let result = should_use_local_or_download(&None, None, "jdtls").unwrap();
80+
assert!(result.is_none(), "Always mode should allow download");
81+
}
82+
83+
#[test]
84+
fn test_check_updates_always_with_local_still_downloads() {
85+
let local = PathBuf::from("/mock/jdtls/1.44.0");
86+
let result = should_use_local_or_download(&None, Some(local), "jdtls").unwrap();
87+
assert!(result.is_none(), "Always mode downloads even with local");
88+
}
89+
90+
#[test]
91+
fn test_check_updates_never_with_local_uses_it() {
92+
let config = Some(json!({"check_updates": "never"}));
93+
let local = PathBuf::from("/mock/jdtls/1.44.0");
94+
let result = should_use_local_or_download(&config, Some(local.clone()), "jdtls").unwrap();
95+
assert_eq!(result, Some(local));
96+
}
97+
98+
#[test]
99+
fn test_check_updates_never_without_local_is_error() {
100+
let config = Some(json!({"check_updates": "never"}));
101+
let result = should_use_local_or_download(&config, None, "jdtls");
102+
assert!(result.is_err());
103+
}
104+
105+
#[test]
106+
fn test_check_updates_once_with_local_uses_it() {
107+
let config = Some(json!({"check_updates": "once"}));
108+
let local = PathBuf::from("/mock/jdtls/1.44.0");
109+
let result = should_use_local_or_download(&config, Some(local.clone()), "jdtls").unwrap();
110+
assert_eq!(result, Some(local));
111+
}
112+
113+
#[test]
114+
fn test_default_is_always() {
115+
let result = should_use_local_or_download(&None, None, "test").unwrap();
116+
assert!(result.is_none(), "Default should be Always (None)");
117+
}
118+
}

src/jdk.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ impl Downloadable for Jdk {
6262
self.cached_path.is_some()
6363
}
6464

65-
fn fetch_latest_version(&self) -> zed::Result<String> {
65+
fn fetch_latest_version(&self, _worktree: &Worktree) -> zed::Result<String> {
6666
Ok(zed::latest_github_release(
6767
CORRETTO_REPO,
6868
zed_extension_api::GithubReleaseOptions {
@@ -80,6 +80,7 @@ impl Downloadable for Jdk {
8080
&mut self,
8181
version: &str,
8282
language_server_id: &LanguageServerId,
83+
_worktree: &Worktree,
8384
) -> zed::Result<PathBuf> {
8485
let jdk_path = get_curr_dir()
8586
.map_err(|err| format!("Failed to get current directory for JDK installation: {err}"))?

src/jdtls.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@ impl Downloadable for Jdtls {
7171
self.cached_path.is_some()
7272
}
7373

74-
fn fetch_latest_version(&self) -> zed::Result<String> {
75-
let (last, _) = get_latest_versions_from_tag(JDTLS_REPO)
74+
fn fetch_latest_version(&self, worktree: &Worktree) -> zed::Result<String> {
75+
let (last, _) = get_latest_versions_from_tag(JDTLS_REPO, worktree)
7676
.map_err(|err| format!("Failed to fetch JDTLS versions from {JDTLS_REPO}: {err}"))?;
7777
Ok(last)
7878
}
@@ -81,13 +81,14 @@ impl Downloadable for Jdtls {
8181
&mut self,
8282
_version: &str,
8383
language_server_id: &LanguageServerId,
84+
worktree: &Worktree,
8485
) -> zed::Result<PathBuf> {
8586
set_language_server_installation_status(
8687
language_server_id,
8788
&LanguageServerInstallationStatus::CheckingForUpdate,
8889
);
8990

90-
let (last, second_last) = get_latest_versions_from_tag(JDTLS_REPO)
91+
let (last, second_last) = get_latest_versions_from_tag(JDTLS_REPO, worktree)
9192
.map_err(|err| format!("Failed to fetch JDTLS versions from {JDTLS_REPO}: {err}"))?;
9293

9394
let (latest_version, latest_version_build) = download_jdtls_milestone(last.as_ref())
@@ -181,8 +182,8 @@ impl Downloadable for Lombok {
181182
self.cached_path.is_some()
182183
}
183184

184-
fn fetch_latest_version(&self) -> zed::Result<String> {
185-
let (latest_version, _) = get_latest_versions_from_tag(LOMBOK_REPO)
185+
fn fetch_latest_version(&self, worktree: &Worktree) -> zed::Result<String> {
186+
let (latest_version, _) = get_latest_versions_from_tag(LOMBOK_REPO, worktree)
186187
.map_err(|err| format!("Failed to fetch Lombok versions from {LOMBOK_REPO}: {err}"))?;
187188
Ok(latest_version)
188189
}
@@ -191,6 +192,7 @@ impl Downloadable for Lombok {
191192
&mut self,
192193
version: &str,
193194
language_server_id: &LanguageServerId,
195+
_worktree: &Worktree,
194196
) -> zed::Result<PathBuf> {
195197
let prefix = LOMBOK_INSTALL_PATH;
196198
let jar_name = format!("lombok-{version}.jar");

src/proxy.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ impl Downloadable for Proxy {
5656
self.cached_path.is_some()
5757
}
5858

59-
fn fetch_latest_version(&self) -> zed::Result<String> {
59+
fn fetch_latest_version(&self, _worktree: &Worktree) -> zed::Result<String> {
6060
// The proxy is built and released together with the extension, so the
6161
// matching release is the one tagged with the extension's own version.
6262
Ok(format!("v{}", env!("CARGO_PKG_VERSION")))
@@ -66,6 +66,7 @@ impl Downloadable for Proxy {
6666
&mut self,
6767
version: &str,
6868
language_server_id: &LanguageServerId,
69+
_worktree: &Worktree,
6970
) -> zed::Result<PathBuf> {
7071
let (name, file_type) = asset_name()?;
7172
let bin_path = format!("{PROXY_INSTALL_PATH}/{version}/{}", proxy_exec());
@@ -127,8 +128,8 @@ impl Downloadable for Proxy {
127128
}
128129

129130
let downloaded = self
130-
.fetch_latest_version()
131-
.and_then(|version| self.download(&version, language_server_id));
131+
.fetch_latest_version(worktree)
132+
.and_then(|version| self.download(&version, language_server_id, worktree));
132133

133134
let download_err = match downloaded {
134135
Ok(path) => return Ok(path),

src/task.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ impl Downloadable for TaskHelper {
4545
self.cached_path.is_some()
4646
}
4747

48-
fn fetch_latest_version(&self) -> zed::Result<String> {
48+
fn fetch_latest_version(&self, _worktree: &Worktree) -> zed::Result<String> {
4949
// The task helper is built and released together with the extension, so
5050
// the matching release is the one tagged with the extension's own version.
5151
Ok(format!("v{}", env!("CARGO_PKG_VERSION")))
@@ -55,6 +55,7 @@ impl Downloadable for TaskHelper {
5555
&mut self,
5656
version: &str,
5757
language_server_id: &LanguageServerId,
58+
_worktree: &Worktree,
5859
) -> zed::Result<PathBuf> {
5960
let (name, file_type) = asset_name()?;
6061
let bin_path = format!(
@@ -119,8 +120,8 @@ impl Downloadable for TaskHelper {
119120
}
120121

121122
let downloaded = self
122-
.fetch_latest_version()
123-
.and_then(|version| self.download(&version, language_server_id));
123+
.fetch_latest_version(worktree)
124+
.and_then(|version| self.download(&version, language_server_id, worktree));
124125

125126
let download_err = match downloaded {
126127
Ok(path) => return Ok(path),

src/util.rs

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -260,16 +260,28 @@ pub fn get_java_major_version(java_executable: &PathBuf) -> zed::Result<u32> {
260260
/// * Could not fetch tags from Github
261261
/// * Failed to deserialize response
262262
/// * Unexpected Github response format
263-
pub fn get_latest_versions_from_tag(repo: &str) -> zed::Result<(String, Option<String>)> {
263+
pub fn get_latest_versions_from_tag(
264+
repo: &str,
265+
worktree: &Worktree,
266+
) -> zed::Result<(String, Option<String>)> {
267+
let mut request = HttpRequest::builder()
268+
.method(HttpMethod::Get)
269+
.url(format!("https://api.github.com/repos/{repo}/tags"));
270+
271+
// Use GITHUB_TOKEN or GH_TOKEN environment variable if available
272+
// to avoid GitHub API rate limiting (60 req/hr unauthenticated vs 5000/hr authenticated).
273+
if let Some(token) = github_token(&worktree.shell_env()) {
274+
request = request.header("Authorization", format!("token {token}"));
275+
}
276+
277+
let request = request
278+
.build()
279+
.map_err(|err| format!("{TAG_RETRIEVAL_ERROR}: {err}"))?;
280+
264281
let tags_response_body = serde_json::from_slice::<Value>(
265-
&fetch(
266-
&HttpRequest::builder()
267-
.method(HttpMethod::Get)
268-
.url(format!("https://api.github.com/repos/{repo}/tags"))
269-
.build()?,
270-
)
271-
.map_err(|err| format!("{TAG_RETRIEVAL_ERROR}: {err}"))?
272-
.body,
282+
&fetch(&request)
283+
.map_err(|err| format!("{TAG_RETRIEVAL_ERROR}: {err}"))?
284+
.body,
273285
)
274286
.map_err(|err| format!("{TAG_RESPONSE_ERROR}: {err}"))?;
275287

@@ -284,6 +296,15 @@ pub fn get_latest_versions_from_tag(repo: &str) -> zed::Result<(String, Option<S
284296
))
285297
}
286298

299+
fn github_token(shell_env: &[(String, String)]) -> Option<&str> {
300+
["GITHUB_TOKEN", "GH_TOKEN"].into_iter().find_map(|name| {
301+
shell_env
302+
.iter()
303+
.find(|(key, value)| key == name && !value.is_empty())
304+
.map(|(_, value)| value.as_str())
305+
})
306+
}
307+
287308
fn get_tag_at(github_tags: &Value, index: usize) -> Option<&str> {
288309
github_tags.as_array().and_then(|tag| {
289310
tag.get(index).and_then(|latest_tag| {
@@ -491,6 +512,33 @@ mod tests {
491512
args: ArgsStringOrList,
492513
}
493514

515+
#[test]
516+
fn github_token_prefers_github_token() {
517+
let env = vec![
518+
("GH_TOKEN".to_string(), "gh".to_string()),
519+
("GITHUB_TOKEN".to_string(), "github".to_string()),
520+
];
521+
522+
assert_eq!(github_token(&env), Some("github"));
523+
}
524+
525+
#[test]
526+
fn github_token_falls_back_to_gh_token() {
527+
let env = vec![
528+
("GITHUB_TOKEN".to_string(), String::new()),
529+
("GH_TOKEN".to_string(), "gh".to_string()),
530+
];
531+
532+
assert_eq!(github_token(&env), Some("gh"));
533+
}
534+
535+
#[test]
536+
fn github_token_ignores_missing_and_empty_tokens() {
537+
let env = vec![("GITHUB_TOKEN".to_string(), String::new())];
538+
539+
assert_eq!(github_token(&env), None);
540+
}
541+
494542
#[test]
495543
fn test_args_list_with_spaces_quotes_elements() {
496544
let json = std::fs::read_to_string("testdata/args_with_spaces.json").unwrap();

0 commit comments

Comments
 (0)