use std::collections::HashMap;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
pub mod load;
use crate::api::browse::{
Advisory, Blame, Blob, BranchInfo, CheckOutcome, Checks, CommitDetail, CommitInfo, Comparison,
DeploymentList, DiscussionDetail, DiscussionList, DiscussionsOf, Gist, GistSummary,
IssueDetail, Job, JobLog, MilestoneDetail, MilestoneList, PrActivity, Profile, Readme, Refs,
Release, RepoOverview, RepoSort, RepoSummary, Results, RunSummary, SearchKind, SearchResults,
StateCounts, TagInfo, TeamDetail, TeamSummary, TreeEntry, UserList, UserSummary, WikiPage,
Workflow, WorkflowRun,
};
use crate::api::commit_comments::CommitComment;
use crate::api::create::NewForm;
use crate::api::edit::Sidebar;
use crate::api::explore::{Topic, Topics};
use crate::api::housekeeping::{BranchRules, FileLook, LabelInfo, Owners, Templates};
use crate::api::insights::{Activity, Dependencies, Traffic};
use crate::api::lists::{ListPage, MyLists, StarList};
use crate::api::manage::{Categories, Target};
use crate::api::mention::Mentionable;
use crate::api::model::{Capped, PrDetail, PrRef, RepoId};
use crate::api::notifications::{Inbox, Unread};
use crate::api::packages::{PackageDetail, PackageList, PackageRef, PackagesOf};
use crate::api::project::{InProject, Project, ProjectRef, ProjectSummary, ProjectsOf};
use crate::api::release::TagLook;
use crate::api::replies::SavedReply;
use crate::api::security::{Alert, AlertKind, AlertList, AlertRef};
use crate::api::settings::{Settings, SettingsPage};
use crate::api::timeline::Review;
use crate::api::watch::{Subscription, Watched};
use crate::create::HeadState;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum DataKey {
Repo(RepoId),
Readme(RepoId),
Tree(RepoId, String, String),
Blob(RepoId, String, String),
Search(SearchKind, String),
StateCounts(String),
Issue(RepoId, u64),
Pr(PrRef),
PrActivity(PrRef),
InProjects(RepoId, u64),
Lines(RepoId, String, String),
CommitComments(RepoId, String),
Reviews(PrRef),
Profile(String),
Files(RepoId, String),
Refs(RepoId),
RefIn(RepoId, String),
LastCommits(RepoId, String, String),
Commit(RepoId, String),
History(RepoId, String, String),
PrChecks(PrRef),
Users(UserList),
Forks(RepoId),
Releases(RepoId),
Release(RepoId, String),
Tags(RepoId),
Branches(RepoId),
Wiki(RepoId, Option<String>),
Advisories(Option<RepoId>),
Advisory(Option<RepoId>, String),
Teams(String),
Team(String, String),
Gist(String),
Gists(String),
Blame(RepoId, String, String),
Compare(RepoId, String),
Deployments(RepoId, Option<String>),
Milestones(RepoId, bool),
Milestone(RepoId, u64),
Discussions(DiscussionsOf, Option<String>),
Discussion(DiscussionsOf, u64),
Run(RepoId, u64, Option<u64>),
Job(RepoId, u64),
JobLog(RepoId, u64),
Workflow(RepoId, String),
Runs(RepoId, String),
CommitChecks(RepoId, String),
OwnerRepos(String, RepoSort),
Stars(String),
Notifications(String),
Sidebar(RepoId, u64),
NewForm(RepoId),
Projects(ProjectsOf),
Project(ProjectRef, Option<u64>),
Alerts(RepoId, AlertKind, String),
Alert(AlertRef),
Subscription(Watched),
Settings(RepoId, SettingsPage),
Activity(RepoId),
Traffic(RepoId),
Dependencies(RepoId),
Featured,
Topic(String),
StarLists(String),
StarList(String, String),
MyLists(RepoId),
Packages(PackagesOf),
Package(PackageRef),
SavedReplies,
Labels(RepoId),
Owners,
Templates,
OpenIssues(RepoId),
MoveTargets(String),
Categories(RepoId),
Clone(RepoId),
Head {
repo: RepoId,
base: String,
head: String,
},
TagLook(RepoId, String),
BranchRules(RepoId, String),
FileLook(RepoId, String, String),
Mentionable(RepoId),
Unread,
Suggest(String),
}
impl DataKey {
pub fn kept(&self) -> bool {
!matches!(
self,
DataKey::Blob(..)
| DataKey::JobLog(..)
| DataKey::Files(..)
| DataKey::Lines(..)
| DataKey::Wiki(..)
| DataKey::CommitComments(..)
| DataKey::RefIn(..)
| DataKey::InProjects(..)
| DataKey::Sidebar(..)
| DataKey::NewForm(..)
| DataKey::Subscription(..)
| DataKey::MyLists(..)
| DataKey::Owners
| DataKey::Templates
| DataKey::OpenIssues(_)
| DataKey::MoveTargets(_)
| DataKey::Categories(_)
| DataKey::TagLook(..)
| DataKey::BranchRules(..)
| DataKey::FileLook(..)
| DataKey::Mentionable(_)
| DataKey::Unread
| DataKey::Suggest(_)
| DataKey::Clone(_)
| DataKey::Head { .. }
)
}
pub fn cache_first(&self) -> bool {
!matches!(self, DataKey::Pr(_))
}
fn cache_id(&self) -> Option<String> {
let key = serde_json::to_string(self).ok().filter(|_| self.kept())?;
Some(format!("data:{key}"))
}
}
pub trait Picked {
fn pick(data: &Data) -> Option<&Self>;
}
impl Picked for Data {
fn pick(data: &Data) -> Option<&Self> {
Some(data)
}
}
macro_rules! data {
($($(#[$meta:meta])* $variant:ident($($field:ty),+) $(=> $picked:ty)?,)*) => {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Data {
$($(#[$meta])* $variant($($field),+),)*
}
$($(
impl Picked for $picked {
fn pick(data: &Data) -> Option<&Self> {
match data {
Data::$variant(x) => Some(std::borrow::Borrow::borrow(x)),
_ => None,
}
}
}
)?)*
};
}
data! {
Repo(Box<RepoOverview>) => RepoOverview,
Readme(Option<Box<Readme>>) => Option<Box<Readme>>,
Tree(Vec<TreeEntry>) => [TreeEntry],
Blob(Box<Blob>) => Blob,
Search(Box<SearchResults>) => SearchResults,
StateCounts(StateCounts) => StateCounts,
Issue(Option<Box<IssueDetail>>) => Option<Box<IssueDetail>>,
Pr(Box<PrDetail>) => PrDetail,
PrActivity(Box<PrActivity>) => PrActivity,
Reviews(Box<Capped<Review>>) => Capped<Review>,
CommitComments(Box<Capped<CommitComment>>) => Capped<CommitComment>,
Lines(Arc<Vec<String>>) => Vec<String>,
InProjects(Box<Capped<InProject>>) => Capped<InProject>,
Profile(Box<Profile>) => Profile,
Files(Arc<Vec<String>>, bool),
Refs(Box<Refs>) => Refs,
RefIn(Option<String>) => Option<String>,
LastCommits(Arc<HashMap<String, CommitInfo>>) => HashMap<String, CommitInfo>,
Commit(Box<CommitDetail>) => CommitDetail,
History(Box<Results<CommitInfo>>) => Results<CommitInfo>,
Checks(Box<Checks>) => Checks,
Users(Box<Results<UserSummary>>) => Results<UserSummary>,
RepoPage(Box<Results<RepoSummary>>) => Results<RepoSummary>,
Releases(Box<Results<Release>>) => Results<Release>,
Discussions(Box<DiscussionList>) => DiscussionList,
Discussion(Box<DiscussionDetail>) => DiscussionDetail,
Run(Box<WorkflowRun>) => WorkflowRun,
Job(Box<Job>) => Job,
#[serde(skip)]
Log(Arc<JobLog>) => JobLog,
Workflow(Box<Workflow>) => Workflow,
Runs(Box<Results<RunSummary>>) => Results<RunSummary>,
Release(Box<Release>) => Release,
Tags(Box<Results<TagInfo>>) => Results<TagInfo>,
Branches(Box<Results<BranchInfo>>) => Results<BranchInfo>,
Wiki(Box<WikiPage>) => WikiPage,
Advisories(Box<Results<Advisory>>) => Results<Advisory>,
Advisory(Box<Advisory>) => Advisory,
Teams(Box<Results<TeamSummary>>) => Results<TeamSummary>,
Team(Box<TeamDetail>) => TeamDetail,
Gist(Box<Gist>) => Gist,
Gists(Box<Results<GistSummary>>) => Results<GistSummary>,
Blame(Box<Blame>) => Blame,
Compare(Box<Comparison>) => Comparison,
Deployments(Box<DeploymentList>) => DeploymentList,
Milestones(Box<MilestoneList>) => MilestoneList,
Milestone(Box<MilestoneDetail>) => MilestoneDetail,
Notifications(Box<Inbox>) => Inbox,
#[serde(skip)]
Sidebar(Box<Sidebar>) => Sidebar,
#[serde(skip)]
NewForm(Box<NewForm>) => NewForm,
Projects(Box<Results<ProjectSummary>>) => Results<ProjectSummary>,
Project(Box<Project>) => Project,
Alerts(Box<AlertList>) => AlertList,
Alert(Box<Alert>) => Alert,
#[serde(skip)]
Subscription(Box<Subscription>) => Subscription,
Settings(Box<Settings>) => Settings,
Activity(Box<Activity>) => Activity,
Traffic(Box<Traffic>) => Traffic,
Dependencies(Box<Dependencies>) => Dependencies,
Topics(Box<Topics>) => Topics,
Topic(Box<Topic>) => Topic,
StarLists(Box<Capped<StarList>>) => Capped<StarList>,
StarList(Box<ListPage>) => ListPage,
MyLists(Box<MyLists>) => MyLists,
Packages(Box<PackageList>) => PackageList,
Package(Box<PackageDetail>) => PackageDetail,
SavedReplies(Box<Capped<SavedReply>>) => Capped<SavedReply>,
Labels(Box<Results<LabelInfo>>) => Results<LabelInfo>,
Owners(Box<Owners>) => Owners,
Templates(Box<Templates>) => Templates,
#[serde(skip)]
OpenIssues(Box<Capped<(u64, String)>>) => Capped<(u64, String)>,
#[serde(skip)]
MoveTargets(Box<Capped<Target>>) => Capped<Target>,
#[serde(skip)]
Categories(Box<Categories>) => Categories,
#[serde(skip)]
Clone(Option<Box<crate::git::local::Clone>>) => Option<Box<crate::git::local::Clone>>,
#[serde(skip)]
Head(Box<HeadState>) => HeadState,
#[serde(skip)]
TagLook(Box<TagLook>) => TagLook,
#[serde(skip)]
BranchRules(Box<BranchRules>) => BranchRules,
#[serde(skip)]
FileLook(Box<FileLook>) => FileLook,
#[serde(skip)]
Mentionable(Box<Mentionable>) => Mentionable,
#[serde(skip)]
Unread(Unread) => Unread,
#[serde(skip)]
Suggestions(Vec<RepoSummary>) => [RepoSummary],
}
impl Data {
pub fn running(&self) -> bool {
match self {
Data::Log(log) => log.running,
Data::Job(job) => {
job.outcome == CheckOutcome::Pending || job.run.outcome == CheckOutcome::Pending
}
Data::Run(run) => run.outcome == CheckOutcome::Pending,
Data::Checks(c) => c.items.iter().any(|i| i.outcome == CheckOutcome::Pending),
Data::Runs(r) => r.items.iter().any(|r| r.outcome == CheckOutcome::Pending),
_ => false,
}
}
pub fn next_cursor(&self) -> Option<&str> {
match self {
Data::Search(r) => r.next(),
Data::History(r) => r.next.as_deref(),
Data::Users(r) => r.next.as_deref(),
Data::RepoPage(r) => r.next.as_deref(),
Data::Releases(r) => r.next.as_deref(),
Data::Runs(r) => r.next.as_deref(),
Data::Discussions(d) => d.results.next.as_deref(),
Data::Tags(r) => r.next.as_deref(),
Data::Branches(r) => r.next.as_deref(),
Data::Teams(r) => r.next.as_deref(),
Data::Gists(r) => r.next.as_deref(),
Data::Deployments(d) => d.results.next.as_deref(),
Data::Milestones(m) => m.results.next.as_deref(),
Data::Milestone(m) => m.items.next.as_deref(),
Data::Advisories(r) => r.next.as_deref(),
Data::Notifications(i) => i.list.next.as_deref(),
Data::Projects(r) => r.next.as_deref(),
Data::Project(p) => p.items.next.as_deref(),
Data::Alerts(a) => a.results.next.as_deref(),
Data::StarList(l) => l.repos.next.as_deref(),
Data::Package(p) => p.versions.next.as_deref(),
Data::Labels(l) => l.next.as_deref(),
_ => None,
}
}
pub fn append(&mut self, more: Data) {
match (self, more) {
(Data::Search(a), Data::Search(b)) => append(a, *b),
(Data::History(a), Data::History(b)) => extend(a, *b, |c| c.oid.clone()),
(Data::Users(a), Data::Users(b)) => extend(a, *b, |u| u.login.clone()),
(Data::RepoPage(a), Data::RepoPage(b)) => extend(a, *b, |r| r.repo.clone()),
(Data::Releases(a), Data::Releases(b)) => extend(a, *b, |r| r.tag.clone()),
(Data::Runs(a), Data::Runs(b)) => extend(a, *b, |r| r.id),
(Data::Discussions(a), Data::Discussions(b)) => {
extend(&mut a.results, b.results, |d| d.number);
}
(Data::Tags(a), Data::Tags(b)) => extend(a, *b, |t| t.name.clone()),
(Data::Branches(a), Data::Branches(b)) => extend(a, *b, |b| b.name.clone()),
(Data::Teams(a), Data::Teams(b)) => extend(a, *b, |t| t.slug.clone()),
(Data::Gists(a), Data::Gists(b)) => extend(a, *b, |g| g.id.clone()),
(Data::Deployments(a), Data::Deployments(b)) => {
extend(&mut a.results, b.results, |d| {
(d.created_at.clone(), d.environment.clone(), d.oid.clone())
});
}
(Data::Milestones(a), Data::Milestones(b)) => {
extend(&mut a.results, b.results, |m| m.number);
}
(Data::Milestone(a), Data::Milestone(b)) => {
extend(&mut a.items, b.items, |i| (i.repo.clone(), i.number));
}
(Data::Advisories(a), Data::Advisories(b)) => extend(a, *b, |a| a.ghsa.clone()),
(Data::Notifications(a), Data::Notifications(b)) => {
extend(&mut a.list, b.list, |n| n.id.clone());
a.searched += b.searched;
a.cut = b.cut;
}
(Data::Projects(a), Data::Projects(b)) => extend(a, *b, |p| p.number),
(Data::Project(a), Data::Project(b)) => extend(&mut a.items, b.items, |i| i.id.clone()),
(Data::Alerts(a), Data::Alerts(b)) => extend(&mut a.results, b.results, |a| a.number),
(Data::StarList(a), Data::StarList(b)) => {
extend(&mut a.repos, b.repos, |r| r.repo.clone());
}
(Data::Package(a), Data::Package(b)) => extend(&mut a.versions, b.versions, |v| v.id),
(Data::Labels(a), Data::Labels(b)) => extend(a, *b, |l| l.name.clone()),
_ => {}
}
}
}
fn extend<T, K: Eq + std::hash::Hash>(a: &mut Results<T>, b: Results<T>, id: impl Fn(&T) -> K) {
let mut seen: std::collections::HashSet<K> = a.items.iter().map(&id).collect();
a.items
.extend(b.items.into_iter().filter(|item| seen.insert(id(item))));
a.next = b.next;
a.total = b.total.max(a.items.len() as u64);
}
fn append(results: &mut SearchResults, more: SearchResults) {
match (results, more) {
(SearchResults::Repos(a), SearchResults::Repos(b)) => extend(a, b, |r| r.repo.clone()),
(SearchResults::Issues(a), SearchResults::Issues(b)) => {
extend(a, b, |i| (i.repo.clone(), i.number));
}
(SearchResults::Users(a), SearchResults::Users(b)) => extend(a, b, |u| u.login.clone()),
(SearchResults::Discussions(a), SearchResults::Discussions(b)) => {
extend(a, b, |d| (d.repo.clone(), d.summary.number));
}
(SearchResults::Commits(a), SearchResults::Commits(b)) => {
extend(a, b, |c| (c.repo.clone(), c.commit.oid.clone()));
}
(SearchResults::Code(a), SearchResults::Code(b)) => {
extend(a, b, |c| (c.repo.clone(), c.path.clone()));
}
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_next_page_leaves_out_what_is_shown() {
let branch = |n: u32| BranchInfo {
name: format!("b{n}"),
..crate::fixtures::branches().items[0].clone()
};
let page = |names: std::ops::Range<u32>, next: bool| Results {
total: 5,
items: names.map(branch).collect(),
next: next.then(|| "c".to_owned()),
};
let mut shown = Data::Branches(Box::new(page(0..3, true)));
shown.append(Data::Branches(Box::new(page(2..5, false))));
let Data::Branches(r) = shown else { panic!() };
let names: Vec<&str> = r.items.iter().map(|b| b.name.as_str()).collect();
assert_eq!(names, ["b0", "b1", "b2", "b3", "b4"]);
assert_eq!(r.next, None);
}
}