From 6c5d6f44e7554d70ef7eeda75f67b89f1e68179c Mon Sep 17 00:00:00 2001 From: cookie1170 <171882521+cookie1170@users.noreply.github.com> Date: Thu, 19 Feb 2026 17:08:04 +1000 Subject: [PATCH 1/7] Added support for fetching stars --- generate-assets/src/bin/generate.rs | 2 ++ generate-assets/src/github_client.rs | 22 ++++++++++++++ generate-assets/src/gitlab_client.rs | 22 ++++++++++++++ generate-assets/src/lib.rs | 45 +++++++++++++++++++++++----- 4 files changed, 83 insertions(+), 8 deletions(-) diff --git a/generate-assets/src/bin/generate.rs b/generate-assets/src/bin/generate.rs index 8739a4e046..7cb4d83615 100644 --- a/generate-assets/src/bin/generate.rs +++ b/generate-assets/src/bin/generate.rs @@ -128,6 +128,7 @@ struct FrontMatterAssetExtra { licenses: Option>, bevy_versions: Option>, nsfw: Option, + stars: Option, } impl From<&Asset> for FrontMatterAsset { @@ -142,6 +143,7 @@ impl From<&Asset> for FrontMatterAsset { licenses: asset.licenses.clone(), bevy_versions: asset.bevy_versions.clone(), nsfw: asset.nsfw, + stars: asset.stars, }, } } diff --git a/generate-assets/src/github_client.rs b/generate-assets/src/github_client.rs index d7c7c0dd51..243a818fe1 100644 --- a/generate-assets/src/github_client.rs +++ b/generate-assets/src/github_client.rs @@ -14,6 +14,12 @@ struct GithubLicenseResponse { license: GithubLicenseLicense, } +#[derive(Deserialize)] +struct GithubStarsResponse { + #[serde(rename = "stargazers_count")] + stars: u32, +} + #[derive(Deserialize)] struct GithubLicenseLicense { spdx_id: String, @@ -96,6 +102,22 @@ impl GithubClient { } } + /// Gets the star count from a github repo + /// + /// Note that this method requests the whole repository info + /// If any other fields from it are needed, this method may be modified as to not make unnecessary requests + pub fn get_stars(&self, username: &str, repository_name: &str) -> anyhow::Result { + let response: GithubStarsResponse = self + .agent + .get(&format!("{BASE_URL}/repos/{username}/{repository_name}")) + .set("Accept", "application/json") + .set("Authorization", &format!("Bearer {}", self.token)) + .call()? + .into_json()?; + + Ok(response.stars) + } + /// Search file by name pub fn search_file( &self, diff --git a/generate-assets/src/gitlab_client.rs b/generate-assets/src/gitlab_client.rs index 7535718b5d..d0338bac28 100644 --- a/generate-assets/src/gitlab_client.rs +++ b/generate-assets/src/gitlab_client.rs @@ -15,6 +15,12 @@ struct GitlabContentResponse { content: String, } +#[derive(Deserialize)] +struct GitlabStarsResponse { + #[serde(rename = "star_count")] + stars: u32, +} + pub struct GitlabClient { agent: ureq::Agent, // This is not currently used because we have so few assets using gitlab that we don't need it. @@ -76,4 +82,20 @@ impl GitlabClient { bail!("Content is not in base64"); } } + + /// Gets the star count of a gitlab project + /// + /// Note that this requests the whole project info + /// So if any more fields from it are needed, this method may be modified as to not make unnecessary requests + pub fn get_stars(&self, id: usize) -> anyhow::Result { + let response: GitlabStarsResponse = self + .agent + .get(&format!("{BASE_URL}/{id}")) + .set("Accept", "application/json") + // .set("Authorization", &format!("Bearer {}", self.token)) + .call()? + .into_json()?; + + Ok(response.stars) + } } diff --git a/generate-assets/src/lib.rs b/generate-assets/src/lib.rs index 225c09c152..1091cbb21d 100644 --- a/generate-assets/src/lib.rs +++ b/generate-assets/src/lib.rs @@ -29,6 +29,7 @@ pub struct Asset { pub licenses: Option>, pub bevy_versions: Option>, pub nsfw: Option, + pub stars: Option, // this field is not read from the toml file #[serde(skip)] @@ -236,7 +237,10 @@ fn get_extra_metadata( Some("crates.io") => { if let Some(ref mut statement) = metadata_source.get_metadata_from_cratesio_statement { let crate_name = segments[1]; - Some(get_metadata_from_crates_db(crate_name, statement)?) + let metadata = get_metadata_from_crates_db(crate_name, statement)?; + // TODO: Fetch the repo link from crates.io and get the stars from there + // TODO: Fetch downloads from crates.io + Some((metadata.0, metadata.1, None)) } else { None } @@ -271,9 +275,11 @@ fn get_extra_metadata( _ => bail!("Unknown host: {}", asset.link), }; - if let Some((license, version)) = metadata { + // If we get more metadata fields, would it make more sense to make it a struct? + if let Some((license, version, stars)) = metadata { asset.set_license(license); asset.set_bevy_version(version); + asset.stars = stars; } Ok(()) @@ -313,6 +319,7 @@ fn merge_version(version1: Option, version2: Option) -> Option>, -) -> anyhow::Result<(Option, Option)> { +) -> anyhow::Result<(Option, Option, Option)> { + let stars = { + match client.get_stars(username, repository_name) { + Ok(stars) => Some(stars), + Err(err) => { + println!("Error getting stars from github: {err:#}"); + None + } + } + }; + let result = get_metadata_from_github_manifest( client, username, @@ -357,7 +374,7 @@ fn get_metadata_from_github( Ok(cargo_files) => cargo_files, Err(err) => { println!("Error fetching cargo files from github: {err:#}"); - return Ok((license, version)); + return Ok((license, version, stars)); } }; @@ -386,7 +403,7 @@ fn get_metadata_from_github( } Err(err) => { println!("Error getting metadata from other cargo file from github: {err}"); - return Ok((license, version)); + return Ok((license, version, stars)); } } @@ -394,7 +411,7 @@ fn get_metadata_from_github( } } - Ok((license, version)) + Ok((license, version, stars)) } /// Gets metadata from a `Cargo.toml` file in a Github project. @@ -419,12 +436,12 @@ fn get_metadata_from_github_manifest( /// Gets metadata from a Gitlab project. /// -/// This algorithm only looks into the root `Cargo.toml` file. +/// This algorithm looks into the root `Cargo.toml` file and fetches the project info in order to get the star count fn get_metadata_from_gitlab( client: &GitlabClient, repository_name: &str, bevy_crates: &Option>, -) -> anyhow::Result<(Option, Option)> { +) -> anyhow::Result<(Option, Option, Option)> { let search_result = client.search_project_by_name(repository_name)?; let repo = search_result @@ -436,9 +453,21 @@ fn get_metadata_from_gitlab( .context("Failed to get Cargo.toml from gitlab")?; let cargo_manifest = toml::from_str::(&content)?; + + let stars = { + match client.get_stars(repo.id) { + Ok(stars) => Some(stars), + Err(err) => { + println!("Error getting stars from gitlab: {err:#}"); + None + } + } + }; + Ok(( get_license(&cargo_manifest), get_bevy_version_from_manifest(&cargo_manifest, bevy_crates), + stars, )) } From 0ec486abca9e989b4458cd3d3b7aeece92dceb7e Mon Sep 17 00:00:00 2001 From: cookie1170 <171882521+cookie1170@users.noreply.github.com> Date: Thu, 19 Feb 2026 18:15:56 +1000 Subject: [PATCH 2/7] Added a star display to the website --- sass/components/_asset-card.scss | 23 ++++++++++++-------- templates/macros/assets.html | 37 +++++++++++++++++++++++--------- 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/sass/components/_asset-card.scss b/sass/components/_asset-card.scss index 6de0bc80b9..cfdbee6a07 100644 --- a/sass/components/_asset-card.scss +++ b/sass/components/_asset-card.scss @@ -10,6 +10,7 @@ $asset-card-padding: 0.4rem; "desc" "tags"; color: var(--asset-card-text-color); + height: 100%; &__banner { grid-area: banner; @@ -62,7 +63,7 @@ $asset-card-padding: 0.4rem; padding: $asset-card-padding; font-size: 1rem; overflow: ellipsis; - height: 4.2rem; + max-height: 4.2rem; } &__small_description { @@ -81,9 +82,8 @@ $asset-card-padding: 0.4rem; &__tags { grid-area: tags; display: grid; - grid-template-areas: "versions licenses"; + grid-template-areas: "versions right"; padding: $asset-card-padding; - height: 2.2rem; } &__tag-icon { @@ -99,12 +99,7 @@ $asset-card-padding: 0.4rem; &__bevy-versions { grid-area: versions; - } - - &__licenses { - grid-area: licenses; - margin-left: auto; - order: 2; + align-self: end; } &__tag-list { @@ -123,4 +118,14 @@ $asset-card-padding: 0.4rem; color: var(--asset-card-tag-text-color); background: var(--asset-card-tag-bg-color); } + + &__tag-right { + display: flex; + align-items: end; + flex-direction: column; + width: fit-content; + margin-left: auto; + grid-area: right; + gap: 0.2em; + } } diff --git a/templates/macros/assets.html b/templates/macros/assets.html index 2d9bef9409..da411a59d1 100644 --- a/templates/macros/assets.html +++ b/templates/macros/assets.html @@ -2,12 +2,17 @@ {% macro init_svg() %} {% endmacro init_svg %} {% macro card(post) %} @@ -38,16 +43,28 @@ {% for version in post.extra.bevy_versions %}{{ version }}{% endfor %} {% endif %} - {% if post.extra.licenses %} -
-
+
+ {% if post.extra.stars %} +
+
- +
- {% for license in post.extra.licenses %}{{ license }}{% endfor %} + {{ post.extra.stars }}
- {% endif %} + {% endif %} + {% if post.extra.licenses %} +
+
+ + + +
+ {% for license in post.extra.licenses %}{{ license }}{% endfor %} +
+ {% endif %} +
From 5acec64f6bf586cc89c5221b5f161ed63e7a44dc Mon Sep 17 00:00:00 2001 From: cookie1170 <171882521+cookie1170@users.noreply.github.com> Date: Fri, 20 Feb 2026 12:39:38 +1000 Subject: [PATCH 3/7] Fetch stars from repository in crates.io metadata --- generate-assets/src/github_client.rs | 10 ++- generate-assets/src/gitlab_client.rs | 11 ++- generate-assets/src/lib.rs | 128 ++++++++++++++++++++++++++- sass/components/_asset-card.scss | 2 +- 4 files changed, 144 insertions(+), 7 deletions(-) diff --git a/generate-assets/src/github_client.rs b/generate-assets/src/github_client.rs index 243a818fe1..ddb20bbe74 100644 --- a/generate-assets/src/github_client.rs +++ b/generate-assets/src/github_client.rs @@ -107,9 +107,17 @@ impl GithubClient { /// Note that this method requests the whole repository info /// If any other fields from it are needed, this method may be modified as to not make unnecessary requests pub fn get_stars(&self, username: &str, repository_name: &str) -> anyhow::Result { + self.get_stars_from_repo_link(&format!("{username}/{repository_name}")) + } + + /// Gets the star count from a github repo link + /// + /// Note that this method requests the whole repository info + /// If any other fields from it are needed, this method may be modified as to not make unnecessary requests + pub fn get_stars_from_repo_link(&self, link: &str) -> anyhow::Result { let response: GithubStarsResponse = self .agent - .get(&format!("{BASE_URL}/repos/{username}/{repository_name}")) + .get(&format!("{BASE_URL}/repos/{link}")) .set("Accept", "application/json") .set("Authorization", &format!("Bearer {}", self.token)) .call()? diff --git a/generate-assets/src/gitlab_client.rs b/generate-assets/src/gitlab_client.rs index d0338bac28..570ec496dc 100644 --- a/generate-assets/src/gitlab_client.rs +++ b/generate-assets/src/gitlab_client.rs @@ -88,9 +88,18 @@ impl GitlabClient { /// Note that this requests the whole project info /// So if any more fields from it are needed, this method may be modified as to not make unnecessary requests pub fn get_stars(&self, id: usize) -> anyhow::Result { + let url = id.to_string(); + self.get_stars_from_url(&url) + } + + /// Gets the star count of a gitlab project from the url + /// + /// Note that this requests the whole project info + /// So if any more fields from it are needed, this method may be modified as to not make unnecessary requests + pub fn get_stars_from_url(&self, url: &str) -> anyhow::Result { let response: GitlabStarsResponse = self .agent - .get(&format!("{BASE_URL}/{id}")) + .get(&format!("{BASE_URL}/{url}")) .set("Accept", "application/json") // .set("Authorization", &format!("Bearer {}", self.token)) .call()? diff --git a/generate-assets/src/lib.rs b/generate-assets/src/lib.rs index 1091cbb21d..8fe6f17f90 100644 --- a/generate-assets/src/lib.rs +++ b/generate-assets/src/lib.rs @@ -109,6 +109,11 @@ pub struct MetadataSource<'a> { /// Initialized with [`get_metadata_from_cratesio_statement`] at the beginning /// of the algorithm, used by [`get_metadata_from_cratesio`] for each asset. pub get_metadata_from_cratesio_statement: Option>, + /// Prepared statement to retrieve the repository link from crates.io. + /// + /// Initialized with [`get_repo_from_cratesio_statement`] at the beginning + /// of the algorithm, used by [`get_stars_from_crates_db_repo_link`] for each asset. + pub get_repo_from_cratesio_statement: Option>, } /// Entry point the algorithm to find [`Asset`] files inside [`Section`] folders, @@ -140,6 +145,8 @@ pub fn parse_assets( }; metadata_source.get_metadata_from_cratesio_statement = Some(get_metadata_from_cratesio_statement(db, bevy_crates_ids)?); + metadata_source.get_repo_from_cratesio_statement = + Some(get_repo_from_cratesio_statement(db)?); } visit_dirs( @@ -238,9 +245,20 @@ fn get_extra_metadata( if let Some(ref mut statement) = metadata_source.get_metadata_from_cratesio_statement { let crate_name = segments[1]; let metadata = get_metadata_from_crates_db(crate_name, statement)?; - // TODO: Fetch the repo link from crates.io and get the stars from there - // TODO: Fetch downloads from crates.io - Some((metadata.0, metadata.1, None)) + let stars = { + get_stars_from_crates_db_repo_link( + crate_name, + // We should be okay to unwrap here, since `get_repo_from_cratesio_statement` should succeed if `get_stars_from_crates_db_repo_link` does. + metadata_source + .get_repo_from_cratesio_statement + .as_mut() + .expect("repo statement is `None`"), + metadata_source.github_client, + metadata_source.gitlab_client, + ) + .unwrap_or_default() + }; + Some((metadata.0, metadata.1, stars)) } else { None } @@ -618,7 +636,7 @@ pub fn prepare_crates_db() -> anyhow::Result { let db = CratesIODumpLoader::default() .tables(&["crates", "dependencies", "versions"]) .preload(true) - .update()? + // .update()? .open_db()?; db.execute_batch( @@ -676,6 +694,93 @@ fn get_metadata_from_crates_db_by_name( } } +/// Gets the stars of a crate from the crates.io database dump. +/// +/// If the crate is not found, retries with `-` instead of `_` +fn get_stars_from_crates_db_repo_link( + crate_name: &str, + repo_statement: &mut rusqlite::Statement<'_>, + github_client: Option<&GithubClient>, + gitlab_client: Option<&GitlabClient>, +) -> anyhow::Result> { + let url = get_repo_link_from_crates_db(crate_name, repo_statement)?; + + let Some(url) = url else { + return Ok(None); + }; + + let stars = match url.host_str() { + Some("github.com") => { + if let Some(client) = github_client { + match client + .get_stars_from_repo_link(url.path().trim_matches('/').trim_end_matches(".git")) + { + Ok(stars) => Some(stars), + Err(err) => { + println!("Failed to get stars from github repo: {err}"); + None + } + } + } else { + None + } + } + Some("gitlab.com") => { + if let Some(client) = gitlab_client { + let path = url.path(); + let path = path.trim_matches('/').replace("/", "%2F"); + // `gitlab.com/api/v4/projects/me/project` 404s, but `gitlab.com/api/v4/project/me%2Fproject` doesn't. + match client.get_stars_from_url(&path) { + Ok(stars) => Some(stars), + Err(err) => { + println!("Failed to get stars from gitlab repo: {err}"); + None + } + } + } else { + None + } + } + _ => None, + }; + + Ok(stars) +} + +/// Gets the repository link of a crate from the crates.io database dump. +/// +/// If the crate is not found, retries with `-` instead of `_` +fn get_repo_link_from_crates_db( + crate_name: &str, + statement: &mut rusqlite::Statement<'_>, +) -> anyhow::Result> { + if let Ok(link) = get_repo_link_from_crates_db_by_name(crate_name, statement) { + Ok(link) + } else if let Ok(link) = + get_repo_link_from_crates_db_by_name(&crate_name.replace('_', "-"), statement) + { + Ok(link) + } else { + bail!("Failed to get repo from crates.io db for {crate_name}") + } +} + +/// Gets the repository link of a crate from the crates.io database dump by the exact name. +fn get_repo_link_from_crates_db_by_name( + name: &str, + statement: &mut rusqlite::Statement<'_>, +) -> anyhow::Result> { + let row = statement.query_row([name], |row| Ok(row.get_unwrap::<_, Option>(0)))?; + let Some(row) = row else { + return Ok(None); + }; + if row.is_empty() { + return Ok(None); + } + + Ok(Some(Url::parse(&row)?)) +} + /// Gets at list of the official bevy crates from the crates.io database dump, /// in lexicographic order. fn get_official_bevy_crates_from_crates_io_db( @@ -749,6 +854,21 @@ pub fn get_latest_bevy_version(db: &CratesIoDb) -> anyhow::Result Result, rusqlite::Error> { + db.prepare( + "\ + SELECT repository \ + FROM crates \ + WHERE name = ? \ + ", + ) +} + /// Get a prepared statement to get license and version for a crate from the /// crates.io database dump. /// diff --git a/sass/components/_asset-card.scss b/sass/components/_asset-card.scss index cfdbee6a07..73bcd97b62 100644 --- a/sass/components/_asset-card.scss +++ b/sass/components/_asset-card.scss @@ -84,6 +84,7 @@ $asset-card-padding: 0.4rem; display: grid; grid-template-areas: "versions right"; padding: $asset-card-padding; + align-items: end; } &__tag-icon { @@ -99,7 +100,6 @@ $asset-card-padding: 0.4rem; &__bevy-versions { grid-area: versions; - align-self: end; } &__tag-list { From 7b4232073d5806e683e5804fca89be41189c585d Mon Sep 17 00:00:00 2001 From: cookie1170 <171882521+cookie1170@users.noreply.github.com> Date: Fri, 20 Feb 2026 12:45:46 +1000 Subject: [PATCH 4/7] Added a `--no-update` cli arg to the generate binary to skip updating the db --- generate-assets/src/bin/generate.rs | 3 ++- generate-assets/src/lib.rs | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/generate-assets/src/bin/generate.rs b/generate-assets/src/bin/generate.rs index 7cb4d83615..7f64dcd80d 100644 --- a/generate-assets/src/bin/generate.rs +++ b/generate-assets/src/bin/generate.rs @@ -14,8 +14,9 @@ fn main() -> anyhow::Result<()> { let asset_dir = std::env::args().nth(1).unwrap(); let content_dir = std::env::args().nth(2).unwrap(); + let no_update = std::env::args().nth(3) == Some(String::from("--no-update")); - let db = prepare_crates_db()?; + let db = prepare_crates_db(!no_update)?; let github_client = { // This should be configured in CI, but it's not mandatory if running locally diff --git a/generate-assets/src/lib.rs b/generate-assets/src/lib.rs index 8fe6f17f90..15957c946f 100644 --- a/generate-assets/src/lib.rs +++ b/generate-assets/src/lib.rs @@ -620,7 +620,7 @@ fn get_bevy_manifest_dependency_version(dep: &cargo_toml::Dependency) -> Option< } /// Downloads the crates.io database dump and open a connection to the db. -pub fn prepare_crates_db() -> anyhow::Result { +pub fn prepare_crates_db(should_update: bool) -> anyhow::Result { let cache_dir = { let mut current_dir = std::env::current_dir()?; current_dir.push("data"); @@ -633,11 +633,18 @@ pub fn prepare_crates_db() -> anyhow::Result { println!("Downloading crates.io data dump"); } - let db = CratesIODumpLoader::default() + let mut db = CratesIODumpLoader::default(); + let mut db = db .tables(&["crates", "dependencies", "versions"]) - .preload(true) - // .update()? - .open_db()?; + .preload(true); + + if should_update { + db = db.update()?; + } else { + println!("Skipping `db.update()`"); + } + + let db = db.open_db()?; db.execute_batch( "\ From 4866972d29540cb1221a6afa79f777b8a860ddfa Mon Sep 17 00:00:00 2001 From: cookie1170 <171882521+cookie1170@users.noreply.github.com> Date: Fri, 20 Feb 2026 14:19:01 +1000 Subject: [PATCH 5/7] Cleaned up the star fetching code --- generate-assets/src/lib.rs | 212 +++++++++++++++++++++---------------- 1 file changed, 122 insertions(+), 90 deletions(-) diff --git a/generate-assets/src/lib.rs b/generate-assets/src/lib.rs index 15957c946f..4eb81778e9 100644 --- a/generate-assets/src/lib.rs +++ b/generate-assets/src/lib.rs @@ -3,6 +3,7 @@ use cratesio_dbdump_csvtab::rusqlite; use cratesio_dbdump_csvtab::CratesIODumpLoader; use github_client::GithubClient; use gitlab_client::GitlabClient; +use regex::Regex; use serde::Deserialize; use std::cmp::Ordering; use std::{fs, path::PathBuf, str::FromStr}; @@ -94,7 +95,7 @@ impl AssetNode { } #[derive(Default)] -/// Where to find metadata (bevy version and license) for assets. +/// Where to find metadata (bevy version and license) and stars for assets. pub struct MetadataSource<'a> { /// Connection to the crates.io database sqlite dump. pub crates_io_db: Option<&'a CratesIoDb>, @@ -112,7 +113,7 @@ pub struct MetadataSource<'a> { /// Prepared statement to retrieve the repository link from crates.io. /// /// Initialized with [`get_repo_from_cratesio_statement`] at the beginning - /// of the algorithm, used by [`get_stars_from_crates_db_repo_link`] for each asset. + /// of the algorithm, used by [`get_repo_stars_from_crates_db`] for each asset. pub get_repo_from_cratesio_statement: Option>, } @@ -220,6 +221,10 @@ fn visit_dirs( eprintln!("ERROR: {err:?}"); } + if let Err(err) = get_stars(&mut asset, metadata_source) { + eprintln!("Failed to get stars for {}: {err:?}", asset.name); + } + section.content.push(AssetNode::Asset(asset)); } } @@ -227,7 +232,7 @@ fn visit_dirs( Ok(()) } -/// Tries to get bevy supported version and license information from various external sources. +/// Tries to get bevy supported version, license information from various external sources. fn get_extra_metadata( asset: &mut Asset, metadata_source: &mut MetadataSource, @@ -245,20 +250,7 @@ fn get_extra_metadata( if let Some(ref mut statement) = metadata_source.get_metadata_from_cratesio_statement { let crate_name = segments[1]; let metadata = get_metadata_from_crates_db(crate_name, statement)?; - let stars = { - get_stars_from_crates_db_repo_link( - crate_name, - // We should be okay to unwrap here, since `get_repo_from_cratesio_statement` should succeed if `get_stars_from_crates_db_repo_link` does. - metadata_source - .get_repo_from_cratesio_statement - .as_mut() - .expect("repo statement is `None`"), - metadata_source.github_client, - metadata_source.gitlab_client, - ) - .unwrap_or_default() - }; - Some((metadata.0, metadata.1, stars)) + Some(metadata) } else { None } @@ -293,11 +285,9 @@ fn get_extra_metadata( _ => bail!("Unknown host: {}", asset.link), }; - // If we get more metadata fields, would it make more sense to make it a struct? - if let Some((license, version, stars)) = metadata { + if let Some((license, version)) = metadata { asset.set_license(license); asset.set_bevy_version(version); - asset.stars = stars; } Ok(()) @@ -337,7 +327,6 @@ fn merge_version(version1: Option, version2: Option) -> Option>, -) -> anyhow::Result<(Option, Option, Option)> { - let stars = { - match client.get_stars(username, repository_name) { - Ok(stars) => Some(stars), - Err(err) => { - println!("Error getting stars from github: {err:#}"); - None - } - } - }; - +) -> anyhow::Result<(Option, Option)> { let result = get_metadata_from_github_manifest( client, username, @@ -392,7 +371,7 @@ fn get_metadata_from_github( Ok(cargo_files) => cargo_files, Err(err) => { println!("Error fetching cargo files from github: {err:#}"); - return Ok((license, version, stars)); + return Ok((license, version)); } }; @@ -421,7 +400,7 @@ fn get_metadata_from_github( } Err(err) => { println!("Error getting metadata from other cargo file from github: {err}"); - return Ok((license, version, stars)); + return Ok((license, version)); } } @@ -429,7 +408,7 @@ fn get_metadata_from_github( } } - Ok((license, version, stars)) + Ok((license, version)) } /// Gets metadata from a `Cargo.toml` file in a Github project. @@ -454,12 +433,12 @@ fn get_metadata_from_github_manifest( /// Gets metadata from a Gitlab project. /// -/// This algorithm looks into the root `Cargo.toml` file and fetches the project info in order to get the star count +/// This algorithm only looks into the root `Cargo.toml` file fn get_metadata_from_gitlab( client: &GitlabClient, repository_name: &str, bevy_crates: &Option>, -) -> anyhow::Result<(Option, Option, Option)> { +) -> anyhow::Result<(Option, Option)> { let search_result = client.search_project_by_name(repository_name)?; let repo = search_result @@ -472,20 +451,9 @@ fn get_metadata_from_gitlab( let cargo_manifest = toml::from_str::(&content)?; - let stars = { - match client.get_stars(repo.id) { - Ok(stars) => Some(stars), - Err(err) => { - println!("Error getting stars from gitlab: {err:#}"); - None - } - } - }; - Ok(( get_license(&cargo_manifest), get_bevy_version_from_manifest(&cargo_manifest, bevy_crates), - stars, )) } @@ -701,54 +669,111 @@ fn get_metadata_from_crates_db_by_name( } } +/// Gets the star count for an asset from the linked GitHub or GitLab repository, or the repository linked on the crates.io page +fn get_stars(asset: &mut Asset, source: &mut MetadataSource) -> anyhow::Result<()> { + let url = Url::parse(&asset.link)?; + let url_stars = get_stars_from_url(url, source); + if let Ok(stars) = url_stars { + asset.stars = Some(stars); + return Ok(()); + }; + + if let Some(crate_name) = &asset.crate_name { + let Some(repo_statement) = &mut source.get_repo_from_cratesio_statement else { + bail!("Can't get stars from crates.io repository - no get repo from crates.io statement provided"); + }; + + asset.stars = Some(get_repo_stars_from_crates_db( + crate_name, + repo_statement, + source.github_client, + source.gitlab_client, + )?); + Ok(()) + } else { + // Return `url_stars`s error + url_stars.map(|_| ()) + } +} + +/// Gets the star count from a url and [`MetadataSource`] +fn get_stars_from_url(url: Url, metadata_source: &mut MetadataSource) -> anyhow::Result { + let segments = url.path_segments().map(|c| c.collect::>()).unwrap(); + + let stars = match url.host_str() { + Some("crates.io") => { + let Some(ref mut statement) = metadata_source.get_repo_from_cratesio_statement else { + bail!("Can't get stars from crates.io repository - no get repo from crates.io statement provided"); + }; + + let crate_name = segments[1]; + + get_repo_stars_from_crates_db( + crate_name, + statement, + metadata_source.github_client, + metadata_source.gitlab_client, + ) + .unwrap_or_default() + } + Some("github.com") | Some("gitlab.com") => get_stars_from_repo_url( + metadata_source.github_client, + metadata_source.gitlab_client, + url, + )?, + Some(_) => bail!("Unknown host: {url}"), + None => bail!("No host"), + }; + + Ok(stars) +} + /// Gets the stars of a crate from the crates.io database dump. /// /// If the crate is not found, retries with `-` instead of `_` -fn get_stars_from_crates_db_repo_link( +fn get_repo_stars_from_crates_db( crate_name: &str, repo_statement: &mut rusqlite::Statement<'_>, github_client: Option<&GithubClient>, gitlab_client: Option<&GitlabClient>, -) -> anyhow::Result> { - let url = get_repo_link_from_crates_db(crate_name, repo_statement)?; +) -> anyhow::Result { + let url = get_repo_url_from_crates_db(crate_name, repo_statement)?; - let Some(url) = url else { - return Ok(None); - }; + get_stars_from_repo_url(github_client, gitlab_client, url) +} +/// Gets the star count from a url to a repository +fn get_stars_from_repo_url( + github_client: Option<&GithubClient>, + gitlab_client: Option<&GitlabClient>, + url: Url, +) -> anyhow::Result { let stars = match url.host_str() { Some("github.com") => { - if let Some(client) = github_client { - match client - .get_stars_from_repo_link(url.path().trim_matches('/').trim_end_matches(".git")) - { - Ok(stars) => Some(stars), - Err(err) => { - println!("Failed to get stars from github repo: {err}"); - None - } - } - } else { - None - } + let Some(client) = github_client else { + bail!("Can't get stars from GitHub - no GitHub client"); + }; + let mut path = url.path().trim_matches('/').trim_end_matches(".git"); + + let replaced = Regex::new(r#"(.*)\/tree\/.*"#) + .unwrap() + .replace(path, r#"$1"#); + + path = replaced.as_ref(); + + client.get_stars_from_repo_link(path)? } Some("gitlab.com") => { - if let Some(client) = gitlab_client { - let path = url.path(); - let path = path.trim_matches('/').replace("/", "%2F"); - // `gitlab.com/api/v4/projects/me/project` 404s, but `gitlab.com/api/v4/project/me%2Fproject` doesn't. - match client.get_stars_from_url(&path) { - Ok(stars) => Some(stars), - Err(err) => { - println!("Failed to get stars from gitlab repo: {err}"); - None - } - } - } else { - None - } + let Some(client) = gitlab_client else { + bail!("Can't get stars from GitLab - no GitLab client") + }; + + // `gitlab.com/api/v4/projects/me/project` 404s, but `gitlab.com/api/v4/project/me%2Fproject` doesn't. + let path = url.path().trim_matches('/').replace("/", "%2F"); + client.get_stars_from_url(&path)? } - _ => None, + Some(_) => bail!("Unknown repository host: {url}"), + _ => bail!("No repository host"), }; Ok(stars) @@ -757,18 +782,25 @@ fn get_stars_from_crates_db_repo_link( /// Gets the repository link of a crate from the crates.io database dump. /// /// If the crate is not found, retries with `-` instead of `_` -fn get_repo_link_from_crates_db( +fn get_repo_url_from_crates_db( crate_name: &str, statement: &mut rusqlite::Statement<'_>, -) -> anyhow::Result> { - if let Ok(link) = get_repo_link_from_crates_db_by_name(crate_name, statement) { - Ok(link) - } else if let Ok(link) = - get_repo_link_from_crates_db_by_name(&crate_name.replace('_', "-"), statement) - { +) -> anyhow::Result { + let link: anyhow::Result> = + if let Ok(link) = get_repo_link_from_crates_db_by_name(crate_name, statement) { + Ok(link) + } else if let Ok(link) = + get_repo_link_from_crates_db_by_name(&crate_name.replace('_', "-"), statement) + { + Ok(link) + } else { + bail!("Failed to get repository link from crates.io db for {crate_name}") + }; + + if let Some(link) = link? { Ok(link) } else { - bail!("Failed to get repo from crates.io db for {crate_name}") + bail!("No repository link on crates.io"); } } From d9fb8121f884817159d8d0904967e3146f800478 Mon Sep 17 00:00:00 2001 From: cookie1170 <171882521+cookie1170@users.noreply.github.com> Date: Fri, 20 Feb 2026 14:50:43 +1000 Subject: [PATCH 6/7] Sorting now takes stars into account --- generate-assets/src/bin/generate.rs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/generate-assets/src/bin/generate.rs b/generate-assets/src/bin/generate.rs index 7f64dcd80d..eeb4c170c9 100644 --- a/generate-assets/src/bin/generate.rs +++ b/generate-assets/src/bin/generate.rs @@ -62,6 +62,7 @@ fn main() -> anyhow::Result<()> { /// Sort the assets in the section so that: /// - Assets that have been manually assigned an order in `bevy-assets` are first /// - Assets that are semver compatible with Bevy are next +/// - Assets that have more stars are first /// - If all else is equal, sort randomly fn sort_section(nodes: &mut [AssetNode], latest_bevy_version: &semver::Version) { for node in nodes.iter_mut() { @@ -74,18 +75,26 @@ fn sort_section(nodes: &mut [AssetNode], latest_bevy_version: &semver::Version) for node in nodes { let is_semver_compat = node_semver_compat_with(node, latest_bevy_version); - let existing_order = match node { - AssetNode::Asset(asset) => asset.order.unwrap_or(usize::MAX), + let (existing_order, stars) = match node { + AssetNode::Asset(asset) => { + (asset.order.unwrap_or(usize::MAX), asset.stars.unwrap_or(0)) + } _ => continue, }; let random: u32 = rand::random(); - to_sort.push((node, existing_order, !is_semver_compat, random)); + to_sort.push(( + node, + existing_order, + !is_semver_compat, + -(stars as i32), + random, + )); } - to_sort.sort_by_key(|sorts| (sorts.1, sorts.2, sorts.3)); + to_sort.sort_by_key(|sorts| (sorts.1, sorts.2, sorts.3, sorts.4)); - for (i, (node, _, _, _)) in to_sort.into_iter().enumerate() { + for (i, (node, ..)) in to_sort.into_iter().enumerate() { let AssetNode::Asset(asset) = node else { continue; }; From 4050d316978ba7284774c71af4a45bcb59e219b3 Mon Sep 17 00:00:00 2001 From: cookie1170 <171882521+cookie1170@users.noreply.github.com> Date: Fri, 20 Feb 2026 15:16:38 +1000 Subject: [PATCH 7/7] final cleanup --- generate-assets/src/github_client.rs | 10 +--------- generate-assets/src/gitlab_client.rs | 9 --------- generate-assets/src/lib.rs | 16 ++++++++-------- 3 files changed, 9 insertions(+), 26 deletions(-) diff --git a/generate-assets/src/github_client.rs b/generate-assets/src/github_client.rs index ddb20bbe74..d412f67f50 100644 --- a/generate-assets/src/github_client.rs +++ b/generate-assets/src/github_client.rs @@ -102,19 +102,11 @@ impl GithubClient { } } - /// Gets the star count from a github repo - /// - /// Note that this method requests the whole repository info - /// If any other fields from it are needed, this method may be modified as to not make unnecessary requests - pub fn get_stars(&self, username: &str, repository_name: &str) -> anyhow::Result { - self.get_stars_from_repo_link(&format!("{username}/{repository_name}")) - } - /// Gets the star count from a github repo link /// /// Note that this method requests the whole repository info /// If any other fields from it are needed, this method may be modified as to not make unnecessary requests - pub fn get_stars_from_repo_link(&self, link: &str) -> anyhow::Result { + pub fn get_stars_from_url(&self, link: &str) -> anyhow::Result { let response: GithubStarsResponse = self .agent .get(&format!("{BASE_URL}/repos/{link}")) diff --git a/generate-assets/src/gitlab_client.rs b/generate-assets/src/gitlab_client.rs index 570ec496dc..10a5817f4f 100644 --- a/generate-assets/src/gitlab_client.rs +++ b/generate-assets/src/gitlab_client.rs @@ -83,15 +83,6 @@ impl GitlabClient { } } - /// Gets the star count of a gitlab project - /// - /// Note that this requests the whole project info - /// So if any more fields from it are needed, this method may be modified as to not make unnecessary requests - pub fn get_stars(&self, id: usize) -> anyhow::Result { - let url = id.to_string(); - self.get_stars_from_url(&url) - } - /// Gets the star count of a gitlab project from the url /// /// Note that this requests the whole project info diff --git a/generate-assets/src/lib.rs b/generate-assets/src/lib.rs index 4eb81778e9..f439aeb4e9 100644 --- a/generate-assets/src/lib.rs +++ b/generate-assets/src/lib.rs @@ -232,7 +232,7 @@ fn visit_dirs( Ok(()) } -/// Tries to get bevy supported version, license information from various external sources. +/// Tries to get bevy supported version and license information from various external sources. fn get_extra_metadata( asset: &mut Asset, metadata_source: &mut MetadataSource, @@ -433,7 +433,7 @@ fn get_metadata_from_github_manifest( /// Gets metadata from a Gitlab project. /// -/// This algorithm only looks into the root `Cargo.toml` file +/// This algorithm only looks into the root `Cargo.toml` file. fn get_metadata_from_gitlab( client: &GitlabClient, repository_name: &str, @@ -761,7 +761,7 @@ fn get_stars_from_repo_url( path = replaced.as_ref(); - client.get_stars_from_repo_link(path)? + client.get_stars_from_url(path)? } Some("gitlab.com") => { let Some(client) = gitlab_client else { @@ -787,14 +787,14 @@ fn get_repo_url_from_crates_db( statement: &mut rusqlite::Statement<'_>, ) -> anyhow::Result { let link: anyhow::Result> = - if let Ok(link) = get_repo_link_from_crates_db_by_name(crate_name, statement) { + if let Ok(link) = get_repo_url_from_crates_db_by_name(crate_name, statement) { Ok(link) } else if let Ok(link) = - get_repo_link_from_crates_db_by_name(&crate_name.replace('_', "-"), statement) + get_repo_url_from_crates_db_by_name(&crate_name.replace('_', "-"), statement) { Ok(link) } else { - bail!("Failed to get repository link from crates.io db for {crate_name}") + bail!("Failed to get repository link from crates.io db"); }; if let Some(link) = link? { @@ -805,7 +805,7 @@ fn get_repo_url_from_crates_db( } /// Gets the repository link of a crate from the crates.io database dump by the exact name. -fn get_repo_link_from_crates_db_by_name( +fn get_repo_url_from_crates_db_by_name( name: &str, statement: &mut rusqlite::Statement<'_>, ) -> anyhow::Result> { @@ -895,7 +895,7 @@ pub fn get_latest_bevy_version(db: &CratesIoDb) -> anyhow::Result Result, rusqlite::Error> {