pub mod github;
pub mod local;
pub mod model;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
pub use github::GhState;
pub use model::Checks;
pub use model::WorktreeMembership;
use model::{BranchInfo, Commit, Issue, PrDetail, PullRequest, RepoInfo, RepoStatus};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Section {
Commits,
Flow,
Branches,
Prs,
Issues,
Status,
}
impl Section {
pub const ALL: [Section; 6] = [
Section::Commits,
Section::Flow,
Section::Branches,
Section::Prs,
Section::Issues,
Section::Status,
];
fn index(self) -> usize {
Self::ALL.iter().position(|s| *s == self).unwrap_or(0)
}
pub fn from_index(i: usize) -> Section {
Self::ALL[i % Self::ALL.len()]
}
pub fn next(self) -> Section {
Self::from_index(self.index() + 1)
}
pub fn prev(self) -> Section {
Self::from_index(self.index() + Self::ALL.len() - 1)
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Scope {
ThisRepo,
MyWork,
}
impl Scope {
pub fn toggle(self) -> Scope {
match self {
Scope::ThisRepo => Scope::MyWork,
Scope::MyWork => Scope::ThisRepo,
}
}
}
#[derive(Clone, Default)]
pub enum Load<T> {
#[default]
Idle,
Loading,
Loaded(T),
Error(String),
}
pub enum GitPayload {
Status(Result<RepoStatus, String>),
Info(Result<RepoInfo, String>),
Branches(Result<Vec<BranchInfo>, String>),
Commits(Result<Vec<Commit>, String>),
Gh(GhState),
Prs(Result<Vec<PullRequest>, String>),
Issues(Result<Vec<Issue>, String>),
PrDetail(Box<Result<PrDetail, String>>),
}
fn next_id() -> u64 {
static NEXT: AtomicU64 = AtomicU64::new(1);
NEXT.fetch_add(1, Ordering::Relaxed)
}
pub struct GitView {
pub id: u64,
pub repo_root: PathBuf,
pub repo_name: String,
pub section: Section,
pub cursor: usize,
pub scroll: usize,
pub filter: String,
pub filtering: bool,
pub scope: Scope,
pub gh: GhState,
pub status: Load<RepoStatus>,
pub info: Load<RepoInfo>,
pub branches: Load<Vec<BranchInfo>>,
pub commits: Load<Vec<Commit>>,
pub prs: Load<Vec<PullRequest>>,
pub issues: Load<Vec<Issue>>,
pub prev_pr_checks: HashMap<u64, Checks>,
pub open_pr: Option<u64>,
pub detail: Load<PrDetail>,
}
impl GitView {
pub fn new(repo_root: PathBuf) -> GitView {
let repo_name = repo_root
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("repo")
.to_string();
GitView {
id: next_id(),
repo_root,
repo_name,
section: Section::Commits,
cursor: 0,
scroll: 0,
filter: String::new(),
filtering: false,
scope: Scope::ThisRepo,
gh: GhState::Missing,
status: Load::Loading,
info: Load::Loading,
branches: Load::Loading,
commits: Load::Loading,
prs: Load::Idle,
issues: Load::Idle,
prev_pr_checks: HashMap::new(),
open_pr: None,
detail: Load::Idle,
}
}
pub fn apply(&mut self, payload: GitPayload) {
match payload {
GitPayload::Status(r) => self.status = into_load(r),
GitPayload::Info(r) => self.info = into_load(r),
GitPayload::Branches(r) => self.branches = into_load(r),
GitPayload::Commits(r) => self.commits = into_load(r),
GitPayload::Gh(s) => {
self.gh = s;
if s == GhState::Ready {
if matches!(self.prs, Load::Idle) {
self.prs = Load::Loading;
}
if matches!(self.issues, Load::Idle) {
self.issues = Load::Loading;
}
}
}
GitPayload::Prs(r) => self.prs = into_load(r),
GitPayload::Issues(r) => self.issues = into_load(r),
GitPayload::PrDetail(r) => {
if self.open_pr.is_some() {
self.detail = into_load(*r);
}
}
}
}
}
fn into_load<T>(r: Result<T, String>) -> Load<T> {
match r {
Ok(v) => Load::Loaded(v),
Err(e) => Load::Error(e),
}
}
pub fn filtered_branches<'a>(
v: &'a [BranchInfo],
filter: &'a str,
) -> impl Iterator<Item = &'a BranchInfo> {
let f = filter.to_lowercase();
v.iter().filter(move |b| {
f.is_empty() || b.name.to_lowercase().contains(&f) || b.subject.to_lowercase().contains(&f)
})
}
pub fn filtered_commits<'a>(v: &'a [Commit], filter: &'a str) -> impl Iterator<Item = &'a Commit> {
let f = filter.to_lowercase();
v.iter().filter(move |c| {
f.is_empty()
|| c.subject.to_lowercase().contains(&f)
|| c.author.to_lowercase().contains(&f)
})
}
pub fn filtered_prs<'a>(
v: &'a [PullRequest],
filter: &'a str,
) -> impl Iterator<Item = &'a PullRequest> {
let f = filter.to_lowercase();
v.iter().filter(move |p| {
f.is_empty()
|| p.title.to_lowercase().contains(&f)
|| p.author.to_lowercase().contains(&f)
|| p.head.to_lowercase().contains(&f)
})
}
pub fn filtered_issues<'a>(v: &'a [Issue], filter: &'a str) -> impl Iterator<Item = &'a Issue> {
let f = filter.to_lowercase();
v.iter().filter(move |i| {
f.is_empty()
|| i.title.to_lowercase().contains(&f)
|| i.author.to_lowercase().contains(&f)
|| i.labels.iter().any(|l| l.to_lowercase().contains(&f))
})
}