From ef2a4ae7d628b97ca59fc1f152b480caf00ebda3 Mon Sep 17 00:00:00 2001 From: rami3l Date: Sun, 9 Aug 2026 11:18:05 +0200 Subject: [PATCH 1/2] refactor: extract `Product` APIs --- src/main.rs | 259 ++++++++++++++++++++++++++++++---------------------- src/site.rs | 182 ++++++++++++++++++++---------------- 2 files changed, 254 insertions(+), 187 deletions(-) diff --git a/src/main.rs b/src/main.rs index 1a5bf711..a1e85af8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -242,37 +242,6 @@ impl fmt::Debug for VersionTag { } } -fn get_versions(repo: &Repository) -> Result, Box> { - let tags = repo - .tag_names(None)? - .into_iter() - .flatten() - .map(|v| v.to_owned()) - .collect::>(); - let mut versions = tags - .iter() - .filter_map(|tag| { - Version::parse(tag) - .or_else(|_| Version::parse(&format!("{}.0", tag))) - .ok() - .map(|v| VersionTag { - name: format!("Rust {}", v), - version: v, - raw_tag: tag.clone(), - commit: repo - .revparse_single(tag) - .unwrap() - .peel_to_commit() - .unwrap() - .id(), - in_progress: false, - }) - }) - .collect::>(); - versions.sort(); - Ok(versions) -} - /// Identify the co-authors, if any, of a commit /// /// Co-authors are determined based on the commit message having lines starting @@ -586,98 +555,172 @@ fn up_to_release( Ok(author_map) } -fn generate_thanks() -> Result, Box> { - let path = update_repo("https://github.com/rust-lang/rust.git")?; - let repo = git2::Repository::open(&path)?; - let mailmap = mailmap_from_repo(&repo)?; - let reviewers = Reviewers::new()?; - - let mut versions = get_versions(&repo)?; - let last_full_stable = versions - .iter() - .rfind(|v| v.raw_tag.ends_with(".0")) - .unwrap() - .version - .clone(); - - versions.push(VersionTag { - name: String::from("Beta"), - version: { - let mut last = last_full_stable.clone(); - last.minor += 1; - last - }, - raw_tag: String::from("beta"), - commit: repo - .revparse_single("beta") - .unwrap() - .peel_to_commit() - .unwrap() - .id(), - in_progress: true, - }); - versions.push(VersionTag { - name: String::from("Nightly"), - version: { - // main is plus 1 minor versions off of beta, which we just pushed - let mut last = last_full_stable.clone(); - last.minor += 2; - last - }, - raw_tag: String::from("main"), - commit: repo - .revparse_single("HEAD") - .unwrap() - .peel_to_commit() +#[derive(Clone, Copy, PartialEq, Eq)] +enum Product { + Rust, +} + +impl Product { + const ALL: [Self; 1] = [Self::Rust]; + + fn name(&self) -> &str { + match self { + Self::Rust => "Rust", + } + } + + fn repo(&self) -> &str { + match self { + Self::Rust => "https://github.com/rust-lang/rust.git", + } + } + + fn dummy_versions(&self, repo: &Repository, versions: &[VersionTag]) -> Vec { + let last_full_stable = versions + .iter() + .rfind(|v| v.raw_tag.ends_with(".0")) .unwrap() - .id(), - in_progress: true, - }); + .version + .clone(); + + match self { + Self::Rust => vec![ + VersionTag { + name: String::from("Beta"), + version: { + let mut last = last_full_stable.clone(); + last.minor += 1; + last + }, + raw_tag: String::from("beta"), + commit: repo + .revparse_single("beta") + .unwrap() + .peel_to_commit() + .unwrap() + .id(), + in_progress: true, + }, + VersionTag { + name: String::from("Nightly"), + version: { + // main is plus 1 minor versions off of beta, which we just pushed + let mut last = last_full_stable.clone(); + last.minor += 2; + last + }, + raw_tag: String::from("main"), + commit: repo + .revparse_single("HEAD") + .unwrap() + .peel_to_commit() + .unwrap() + .id(), + in_progress: true, + }, + ], + } + } - let mut version_map = BTreeMap::new(); + fn get_versions( + &self, + repo: &Repository, + ) -> Result, Box> { + let tags = repo + .tag_names(None)? + .into_iter() + .flatten() + .map(|v| v.to_owned()) + .collect::>(); + let mut versions = tags + .iter() + .filter_map(|tag| { + Version::parse(tag) + .or_else(|_| Version::parse(&format!("{}.0", tag))) + .ok() + .map(|v| VersionTag { + name: format!("{name} {v}", name = self.name()), + version: v, + raw_tag: tag.clone(), + commit: repo + .revparse_single(tag) + .unwrap() + .peel_to_commit() + .unwrap() + .id(), + in_progress: false, + }) + }) + .collect::>(); + versions.sort(); + Ok(versions) + } - let mut cache = HashMap::new(); + fn generate_thanks( + &self, + ) -> Result, Box> { + let path = update_repo(self.repo())?; + let repo = git2::Repository::open(&path)?; + let mailmap = mailmap_from_repo(&repo)?; + let reviewers = Reviewers::new()?; - for (idx, version) in versions.iter().enumerate() { - let previous = if let Some(v) = idx.checked_sub(1).map(|idx| &versions[idx]) { - v - } else { - let author_map = build_author_map(&repo, &reviewers, &mailmap, "", &version.raw_tag)?; - version_map.insert(version.clone(), author_map); - continue; - }; + let mut versions = self.get_versions(&repo)?; + versions.extend(self.dummy_versions(&repo, &versions)); - eprintln!("Processing {:?} to {:?}", previous, version); + let mut version_map = BTreeMap::new(); - cache.insert( - version, - up_to_release(&repo, &reviewers, &mailmap, version)?, - ); - let previous = match cache.remove(&previous) { - Some(v) => v, - None => up_to_release(&repo, &reviewers, &mailmap, previous)?, - }; - let current = cache.get(&version).unwrap(); + let mut cache = HashMap::new(); - // Remove commits reachable from the previous release. - let only_current = current.difference(&previous); - version_map.insert(version.clone(), only_current); - } + for (idx, version) in versions.iter().enumerate() { + let previous = if let Some(v) = idx.checked_sub(1).map(|idx| &versions[idx]) { + v + } else { + let author_map = + build_author_map(&repo, &reviewers, &mailmap, "", &version.raw_tag)?; + version_map.insert(version.clone(), author_map); + continue; + }; - Ok(version_map) + eprintln!("Processing {} {:?} to {:?}", self.name(), previous, version); + + cache.insert( + version, + up_to_release(&repo, &reviewers, &mailmap, version)?, + ); + let previous = match cache.remove(&previous) { + Some(v) => v, + None => up_to_release(&repo, &reviewers, &mailmap, previous)?, + }; + let current = cache.get(&version).unwrap(); + + // Remove commits reachable from the previous release. + let only_current = current.difference(&previous); + version_map.insert(version.clone(), only_current); + } + + Ok(version_map) + } } fn run() -> Result<(), Box> { - let by_version = generate_thanks()?; + let products = Product::ALL; - let mut all_time = by_version.values().next().unwrap().clone(); - for map in by_version.values().skip(1) { - all_time.extend(map.clone()); + let mut by_version = Vec::with_capacity(products.len()); + for product in &products { + let thanks = product.generate_thanks()?; + by_version.push(thanks); } - site::render(by_version, all_time)?; + let all_time = + by_version + .iter() + .flat_map(|m| m.values()) + .fold(AuthorMap::new(), |mut acc, map| { + acc.extend(map.clone()); + acc + }); - Ok(()) + site::render(&products, &by_version, &all_time) } fn main() { diff --git a/src/site.rs b/src/site.rs index d35c6d91..6a41c189 100644 --- a/src/site.rs +++ b/src/site.rs @@ -1,4 +1,4 @@ -use crate::{AuthorMap, VersionTag}; +use crate::{AuthorMap, Product, VersionTag}; use handlebars::Handlebars; use std::collections::{BTreeMap, HashMap}; use std::fs; @@ -6,15 +6,20 @@ use std::path::Path; use unicase::UniCase; pub fn render( - by_version: BTreeMap, - all_time_map: AuthorMap, + products: &[Product], + by_version: &[BTreeMap], + all_time_map: &AuthorMap, ) -> Result<(), Box> { copy_public()?; - index(&all_time_map, &by_version)?; about()?; - releases(&by_version, &all_time_map)?; - Ok(()) + let mut index_releases = vec![]; + for (product, by_version) in products.iter().zip(by_version) { + index_releases.extend(product.index_releases(all_time_map, by_version)?); + product.releases(by_version, all_time_map)?; + } + + index(index_releases) } #[derive(serde::Serialize)] @@ -70,40 +75,22 @@ fn copy_public() -> Result<(), Box> { Ok(()) } -fn index( - all_time: &AuthorMap, - by_version: &BTreeMap, -) -> Result<(), Box> { - #[derive(serde::Serialize)] - struct Release { - name: String, - url: String, - people: usize, - commits: usize, - } +#[derive(serde::Serialize)] +pub struct Release { + name: String, + url: String, + people: usize, + commits: usize, +} + +pub fn index(releases: Vec) -> Result<(), Box> { #[derive(serde::Serialize)] - struct Index { + pub struct Index { common: CommonData, releases: Vec, } - let hb = hb()?; - - let mut releases = Vec::new(); - releases.push(Release { - name: "All time".into(), - url: "/rust/all-time/".into(), - people: all_time.iter().count(), - commits: all_time.iter().map(|(_, count)| count).sum(), - }); - for (version, stats) in by_version.iter().rev() { - releases.push(Release { - name: version.name.clone(), - url: format!("/rust/{}/", version.version), - people: stats.iter().count(), - commits: stats.iter().map(|(_, count)| count).sum(), - }); - } + let hb = hb()?; let res = hb.render( "index", &Index { @@ -116,6 +103,36 @@ fn index( Ok(()) } +impl Product { + fn index_releases( + &self, + all_time: &AuthorMap, + by_version: &BTreeMap, + ) -> Result, Box> { + let mut releases = Vec::new(); + if self == &Self::Rust { + releases.push(Release { + name: "All time".into(), + url: "/rust/all-time/".into(), + people: all_time.iter().count(), + commits: all_time.iter().map(|(_, count)| count).sum(), + }); + } + + let product = self.name(); + let product_lower = product.to_lowercase(); + for (version, stats) in by_version.iter().rev() { + releases.push(Release { + name: version.name.clone(), + url: format!("/{product_lower}/{}/", version.version), + people: stats.iter().count(), + commits: stats.iter().map(|(_, count)| count).sum(), + }); + } + Ok(releases) + } +} + fn about() -> Result<(), Box> { #[derive(serde::Serialize)] struct About { @@ -212,53 +229,60 @@ fn deduplicate_scores(entries: Vec) -> Vec { .collect() } -fn releases( - by_version: &BTreeMap, - all_time: &AuthorMap, -) -> Result<(), Box> { - #[derive(serde::Serialize)] - struct Release { - common: CommonData, - release_title: String, - release: String, - count: usize, - scores: Vec, - in_progress: bool, - } - let hb = hb()?; - let scores = author_map_to_scores(all_time); +impl Product { + fn releases( + &self, + by_version: &BTreeMap, + all_time: &AuthorMap, + ) -> Result<(), Box> { + #[derive(serde::Serialize)] + struct Release { + common: CommonData, + release_title: String, + release: String, + count: usize, + scores: Vec, + in_progress: bool, + } + let hb = hb()?; + let scores = author_map_to_scores(all_time); - let res = hb.render( - "stats", - &Release { - common: CommonData::new("All-time Rust Contributors".into()), - release_title: String::from("All-time"), - release: String::from("all of Rust"), - count: scores.len(), - scores, - in_progress: true, - }, - )?; + let product = self.name(); + if self == &Self::Rust { + let res = hb.render( + "stats", + &Release { + common: CommonData::new("All-time Rust Contributors".into()), + release_title: String::from("All-time"), + release: String::from("all of Rust"), + count: scores.len(), + scores, + in_progress: true, + }, + )?; - create_dir("output/rust/all-time")?; - fs::write("output/rust/all-time/index.html", res)?; + create_dir("output/rust/all-time")?; + fs::write("output/rust/all-time/index.html", res)?; + } - for (version, map) in by_version { - let scores = author_map_to_scores(map); - let res = hb.render( - "stats", - &Release { - common: CommonData::new(format!("Rust {} Contributors", version)), - release_title: version.name.clone(), - release: version.to_string(), - count: scores.len(), - scores, - in_progress: version.in_progress, - }, - )?; + let product_lower = product.to_lowercase(); + for (version, map) in by_version { + let scores = author_map_to_scores(map); + let res = hb.render( + "stats", + &Release { + common: CommonData::new(format!("{product} {version} Contributors")), + release_title: version.name.clone(), + release: version.to_string(), + count: scores.len(), + scores, + in_progress: version.in_progress, + }, + )?; - create_dir(format!("output/rust/{}", version))?; - fs::write(format!("output/rust/{}/index.html", version), res)?; + create_dir(format!("output/{product_lower}/{version}"))?; + fs::write(format!("output/{product_lower}/{version}/index.html"), res)?; + } + Ok(()) } - Ok(()) } From 76735c29df30d6c4e86ae72de2d14b2ae2837191 Mon Sep 17 00:00:00 2001 From: rami3l Date: Sun, 9 Aug 2026 12:04:58 +0200 Subject: [PATCH 2/2] feat: add `Product::Rustup` --- src/main.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index a1e85af8..42cd8d8a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -558,20 +558,23 @@ fn up_to_release( #[derive(Clone, Copy, PartialEq, Eq)] enum Product { Rust, + Rustup, } impl Product { - const ALL: [Self; 1] = [Self::Rust]; + const ALL: [Self; 2] = [Self::Rust, Self::Rustup]; fn name(&self) -> &str { match self { Self::Rust => "Rust", + Self::Rustup => "Rustup", } } fn repo(&self) -> &str { match self { Self::Rust => "https://github.com/rust-lang/rust.git", + Self::Rustup => "https://github.com/rust-lang/rustup.git", } } @@ -619,6 +622,22 @@ impl Product { in_progress: true, }, ], + Self::Rustup => vec![VersionTag { + name: String::from("Rustup Nightly"), + version: { + let mut last = last_full_stable.clone(); + last.minor += 1; + last + }, + raw_tag: String::from("main"), + commit: repo + .revparse_single("HEAD") + .unwrap() + .peel_to_commit() + .unwrap() + .id(), + in_progress: true, + }], } }