//! In-memory stub data mimicking a lore.kernel.org archive. //! //! Nothing here performs I/O. The real backend will fetch from //! or shell out to `b4`/`lei`. use std::collections::HashMap; use crate::models::{Author, MailingList, Message, MessageId, Thread, ThreadId}; use super::Backend; /// Static "mailing list content" the mock rotates through. struct ThreadSeed { /// Series prefix, e.g. `PATCH v4`; empty for plain discussions. prefix: &'static str, subject: &'static str, author_name: &'static str, author_email: &'static str, /// Total message count, including the cover letter when > 1. message_count: usize, /// Short display date for the thread list. date: &'static str, } const THREAD_SEEDS: [ThreadSeed; 10] = [ ThreadSeed { prefix: "PATCH v4", subject: "refs: introduce a reftable backend for packed refs", author_name: "Patrick Steinhardt", author_email: "ps@pks.im", message_count: 6, date: "Sep 10", }, ThreadSeed { prefix: "PATCH v2", subject: "t/perf: add benchmarks for ref operations", author_name: "Victoria Dye", author_email: "vdye@github.com", message_count: 3, date: "Sep 10", }, ThreadSeed { prefix: "RFC PATCH", subject: "log --format: add a reftable dump backend", author_name: "Karthik Nayak", author_email: "karthik.188@gmail.com", message_count: 2, date: "Sep 09", }, ThreadSeed { prefix: "PATCH", subject: "tests: convert remaining scripts to test-tool helpers", author_name: "Ævar Arnfjörð Bjarmason", author_email: "avarab@gmail.com", message_count: 4, date: "Sep 09", }, ThreadSeed { prefix: "", subject: "git maintenance: schedule tasks on macOS via launchd", author_name: "Derrick Stolee", author_email: "stolee@gmail.com", message_count: 1, date: "Sep 08", }, ThreadSeed { prefix: "PATCH v3", subject: "bundle-uri: support sparse filters in clones", author_name: "Junio C Hamano", author_email: "gitster@pobox.com", message_count: 5, date: "Sep 08", }, ThreadSeed { prefix: "PATCH", subject: "rebase --update-refs: silence reflog message noise", author_name: "Phillip Wood", author_email: "phillip.wood123@gmail.com", message_count: 2, date: "Sep 07", }, ThreadSeed { prefix: "", subject: "Git v2.48.0 released", author_name: "Junio C Hamano", author_email: "gitster@pobox.com", message_count: 1, date: "Sep 07", }, ThreadSeed { prefix: "PATCH v6", subject: "worktree list: add --porcelain output", author_name: "Elijah Newren", author_email: "newren@gmail.com", message_count: 7, date: "Sep 06", }, ThreadSeed { prefix: "PATCH", subject: "submodule: parallelize fetch with --jobs", author_name: "Taylor Blau", author_email: "me@ttaylorr.com", message_count: 3, date: "Sep 06", }, ]; /// Fixed pool of reviewers assigned as `Cc:` on mock messages. const REVIEWERS: [(&str, &str); 6] = [ ("Junio C Hamano", "gitster@pobox.com"), ("Taylor Blau", "me@ttaylorr.com"), ("Ævar Arnfjörð Bjarmason", "avarab@gmail.com"), ("Victoria Dye", "vdye@github.com"), ("Elijah Newren", "newren@gmail.com"), ("Phillip Wood", "phillip.wood123@gmail.com"), ]; /// In-memory data source. [`Backend::refresh`] rotates the visible window /// over [`THREAD_SEEDS`] so `r` visibly changes the data. #[derive(Debug)] pub struct MockBackend { list: MailingList, /// Refresh counter: each refresh rotates the thread window. generation: usize, threads: Vec, messages: HashMap>, } impl MockBackend { /// Build a backend for `list` with an initial snapshot. /// /// The starting generation is derived from the list name so different /// lists show different (but deterministic) thread windows. #[must_use] pub fn new(list: &MailingList) -> Self { let mut backend = Self { list: list.clone(), generation: fnv1a(list.name.as_bytes()) % THREAD_SEEDS.len(), threads: Vec::new(), messages: HashMap::new(), }; backend.rebuild(); backend } /// Visible thread count varies with the generation so refreshes differ. const fn visible_thread_count(generation: usize) -> usize { THREAD_SEEDS.len().saturating_sub(2) + generation % 3 } /// Regenerate the snapshot from the seed pool, rotated by `generation`. fn rebuild(&mut self) { self.threads.clear(); self.messages.clear(); let visible = Self::visible_thread_count(self.generation); let start = self.generation % THREAD_SEEDS.len(); for offset in 0..visible { let seed = &THREAD_SEEDS[(start + offset) % THREAD_SEEDS.len()]; let thread_id = ThreadId(offset); self.threads.push(Thread { id: thread_id, subject: series_subject(seed), author: seed_author(seed), date: seed.date.to_owned(), message_count: seed.message_count, }); let list = (0..seed.message_count) .map(|index| message_from_seed(seed, &self.list, offset, index)) .collect(); self.messages.insert(thread_id, list); } } } impl Backend for MockBackend { fn list_name(&self) -> &str { &self.list.name } fn threads(&self) -> &[Thread] { &self.threads } fn messages(&self, thread_id: ThreadId) -> &[Message] { self.messages.get(&thread_id).map_or(&[], Vec::as_slice) } fn refresh(&mut self) -> anyhow::Result<()> { self.generation += 1; self.rebuild(); Ok(()) } } /// Deterministic FNV-1a hash used to vary mock data per list. fn fnv1a(bytes: &[u8]) -> usize { let mut hash: usize = 0x811c_9dc5; let mut index = 0; while index < bytes.len() { hash ^= usize::from(bytes[index]); hash = hash.wrapping_mul(0x0100_0193); index += 1; } hash } fn seed_author(seed: &ThreadSeed) -> Author { Author { name: seed.author_name.to_owned(), email: seed.author_email.to_owned(), } } /// Thread subject: `[ 0/] ` for series, plain otherwise. fn series_subject(seed: &ThreadSeed) -> String { if seed.prefix.is_empty() || seed.message_count <= 1 { seed.subject.to_owned() } else { format!( "[{} 0/{}] {}", seed.prefix, seed.message_count - 1, seed.subject ) } } fn message_from_seed( seed: &ThreadSeed, list: &MailingList, thread_offset: usize, index: usize, ) -> Message { let subject = if seed.prefix.is_empty() || seed.message_count <= 1 { seed.subject.to_owned() } else { format!( "[{} {}/{}] {}", seed.prefix, index, seed.message_count - 1, seed.subject ) }; let to = vec![Author { name: list.name.clone(), email: list.address.clone(), }]; let count = 1 + (thread_offset + index) % 2; let start = (thread_offset * 7 + index) % REVIEWERS.len(); let cc = (0..count) .map(|i| { let (name, email) = REVIEWERS[(start + i) % REVIEWERS.len()]; Author { name: name.to_owned(), email: email.to_owned(), } }) .collect(); Message { id: MessageId(index), subject, author: seed_author(seed), to, cc, date: message_date(thread_offset, index), body: message_body(seed, index), } } fn message_date(thread_offset: usize, index: usize) -> String { format!( "Fri, 11 Sep 2026 {:02}:{:02}:{:02} +0200", 8 + thread_offset % 9, index * 7 % 60, (index * 13 + thread_offset) % 60 ) } fn message_body(seed: &ThreadSeed, index: usize) -> String { let total = seed.message_count.saturating_sub(1); let intro = if seed.message_count <= 1 { format!( "This is a standalone message about {}.\n\nDetails and rationale follow; review and\nfeedback are welcome.", seed.subject ) } else if index == 0 { format!( "Cover letter for the {} series: {}.\n\nThe full series follows. See the individual\npatches for the actual changes.", seed.prefix, seed.subject ) } else { format!( "Patch {index}/{total} of the {} series.\n\nOne focused change:\n- {} (summary stub)\n- tests updated accordingly", seed.prefix, seed.subject ) }; if index == 0 && seed.message_count > 1 { // Cover letters carry no diff. return intro; } let diff = format!( "---\n diff --git a/refs/reftable/backend.c b/refs/reftable/backend.c\n\ index abcdef0..fedcba1 100644\n\ --- a/refs/reftable/backend.c\n\ +++ b/refs/reftable/backend.c\n\ @@ -1{index},3 +1{index},3 @@\n\ -// old behavior (removed)\n\ +// new behavior (lazymail placeholder diff)" ); format!("{intro}\n\n{diff}") }