Skip to content

Commit 3088e5a

Browse files
committed
Prefer repo-named assets
Signed-off-by: Max Howell <mxcl@me.com>
1 parent 4c798e0 commit 3088e5a

1 file changed

Lines changed: 101 additions & 9 deletions

File tree

src/lib.rs

Lines changed: 101 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ fn github_api_base() -> String {
283283

284284
fn resolve_release_info(client: &Client, owner: &str, repo: &str) -> Result<ReleaseInfo> {
285285
let release = fetch_latest_release(client, owner, repo)?;
286-
let asset = pick_asset(&release.assets)?;
286+
let asset = pick_asset(&release.assets, repo)?;
287287
let tag = release.tag_name.as_deref().unwrap_or("unknown").to_string();
288288

289289
Ok(ReleaseInfo {
@@ -310,7 +310,7 @@ fn fetch_latest_release(client: &Client, owner: &str, repo: &str) -> Result<Rele
310310
.with_context(|| format!("parse release for {owner}/{repo}"))
311311
}
312312

313-
fn pick_asset(assets: &[Asset]) -> Result<Asset> {
313+
fn pick_asset(assets: &[Asset], repo_name: &str) -> Result<Asset> {
314314
if assets.is_empty() {
315315
bail!("release has no assets")
316316
}
@@ -323,21 +323,38 @@ fn pick_asset(assets: &[Asset]) -> Result<Asset> {
323323
candidates = assets.iter().collect();
324324
}
325325

326+
let mut prefer_shorter = false;
327+
let repo_tokens = tokenize_name(repo_name);
328+
if !repo_tokens.is_empty() {
329+
let repo_candidates: Vec<&Asset> = candidates
330+
.iter()
331+
.copied()
332+
.filter(|asset| asset_matches_repo(&asset.name, &repo_tokens))
333+
.collect();
334+
if !repo_candidates.is_empty() {
335+
candidates = repo_candidates;
336+
prefer_shorter = true;
337+
}
338+
}
339+
326340
let os_tokens = os_tokens();
327341
let arch_tokens = arch_tokens();
328342

329-
let mut best: Option<(&Asset, i32)> = None;
343+
let mut best: Option<(&Asset, i32, usize)> = None;
330344
for asset in candidates {
331345
let score = asset_score(&asset.name, &os_tokens, &arch_tokens);
346+
let stem_len = asset_stem(&asset.name).len();
332347
if best
333-
.map(|(_, best_score)| score > best_score)
348+
.map(|(_, best_score, best_len)| {
349+
score > best_score || (prefer_shorter && score == best_score && stem_len < best_len)
350+
})
334351
.unwrap_or(true)
335352
{
336-
best = Some((asset, score));
353+
best = Some((asset, score, stem_len));
337354
}
338355
}
339356

340-
best.map(|(asset, _)| asset.clone())
357+
best.map(|(asset, _, _)| asset.clone())
341358
.context("no suitable assets")
342359
}
343360

@@ -364,6 +381,35 @@ fn contains_any(haystack: &str, tokens: &[&str]) -> bool {
364381
tokens.iter().any(|token| haystack.contains(token))
365382
}
366383

384+
fn tokenize_name(name: &str) -> Vec<String> {
385+
name.to_lowercase()
386+
.split(|c: char| !c.is_ascii_alphanumeric())
387+
.filter(|token| !token.is_empty())
388+
.map(|token| token.to_string())
389+
.collect()
390+
}
391+
392+
fn asset_matches_repo(asset_name: &str, repo_tokens: &[String]) -> bool {
393+
let stem = asset_stem(asset_name);
394+
let tokens = tokenize_name(stem);
395+
tokens_match_sequence(&tokens, repo_tokens)
396+
}
397+
398+
fn tokens_match_sequence(haystack: &[String], needle: &[String]) -> bool {
399+
if needle.is_empty() {
400+
return false;
401+
}
402+
if needle.len() > haystack.len() {
403+
return false;
404+
}
405+
for start in 0..=(haystack.len() - needle.len()) {
406+
if haystack[start..start + needle.len()] == *needle {
407+
return true;
408+
}
409+
}
410+
false
411+
}
412+
367413
fn os_tokens() -> Vec<&'static str> {
368414
match env::consts::OS {
369415
"macos" => vec!["darwin", "macos", "osx", "mac", "apple-darwin"],
@@ -409,6 +455,32 @@ fn is_gzip_name(name: &str) -> bool {
409455
lower.ends_with(".gz") && !lower.ends_with(".tar.gz")
410456
}
411457

458+
fn asset_stem(name: &str) -> &str {
459+
let lower = name.to_lowercase();
460+
if lower.ends_with(".tar.gz") {
461+
return &name[..name.len().saturating_sub(7)];
462+
}
463+
if lower.ends_with(".tar.xz") {
464+
return &name[..name.len().saturating_sub(7)];
465+
}
466+
if lower.ends_with(".tar.bz2") {
467+
return &name[..name.len().saturating_sub(8)];
468+
}
469+
if lower.ends_with(".zip") {
470+
return &name[..name.len().saturating_sub(4)];
471+
}
472+
if lower.ends_with(".tgz") {
473+
return &name[..name.len().saturating_sub(4)];
474+
}
475+
if lower.ends_with(".gz") {
476+
return &name[..name.len().saturating_sub(3)];
477+
}
478+
if lower.ends_with(".exe") {
479+
return &name[..name.len().saturating_sub(4)];
480+
}
481+
name
482+
}
483+
412484
fn download_asset(client: &Client, url: &str, dest: &Path) -> Result<()> {
413485
let mut response = client
414486
.get(url)
@@ -1078,7 +1150,7 @@ mod tests {
10781150
browser_download_url: "http://example.com/tool-best".to_string(),
10791151
},
10801152
];
1081-
let picked = pick_asset(&assets).expect("pick asset");
1153+
let picked = pick_asset(&assets, "tool").expect("pick asset");
10821154
assert_eq!(picked.name, best_name);
10831155
assert!(is_ignored_asset("foo.sha256"));
10841156
assert!(is_archive_name("foo.tar.gz"));
@@ -1088,7 +1160,7 @@ mod tests {
10881160

10891161
#[test]
10901162
fn pick_asset_errors_on_empty_assets() {
1091-
assert!(pick_asset(&[]).is_err());
1163+
assert!(pick_asset(&[], "tool").is_err());
10921164
}
10931165

10941166
#[test]
@@ -1103,10 +1175,30 @@ mod tests {
11031175
browser_download_url: "http://example.com/tool.sig".to_string(),
11041176
},
11051177
];
1106-
let picked = pick_asset(&assets).expect("pick asset");
1178+
let picked = pick_asset(&assets, "tool").expect("pick asset");
11071179
assert!(picked.name.ends_with(".sha256") || picked.name.ends_with(".sig"));
11081180
}
11091181

1182+
#[test]
1183+
fn pick_asset_prefers_repo_stem() {
1184+
let os = os_tokens();
1185+
let arch = arch_tokens();
1186+
let name = format!("bun-{}-{}.zip", os[0], arch[0]);
1187+
let profile = format!("bun-profile-{}-{}.zip", os[0], arch[0]);
1188+
let assets = vec![
1189+
Asset {
1190+
name: profile.clone(),
1191+
browser_download_url: "http://example.com/bun-profile".to_string(),
1192+
},
1193+
Asset {
1194+
name: name.clone(),
1195+
browser_download_url: "http://example.com/bun".to_string(),
1196+
},
1197+
];
1198+
let picked = pick_asset(&assets, "bun").expect("pick asset");
1199+
assert_eq!(picked.name, name);
1200+
}
1201+
11101202
#[test]
11111203
fn asset_score_counts_exe() {
11121204
assert_eq!(asset_score("tool.exe", &[], &[]), 1);

0 commit comments

Comments
 (0)