Phases 0-1 + Sprint A: module split, CLI layer, org-refresh fix, hygiene

- LICENSE (MIT) + README truth-pass; docs/cli.md CLI contract
- CLI subcommands (src/cli_cmds/) per "TUI for users, CLI for users+AI"
- Hub/detail refactor: keymap dispatch, modal split, action enum, BgMsg bus
- Fix: org repos were never refreshed (owner==username match); refresh now
  parallel — 49 repos in ~11s, surfaced 70 previously invisible PRs
- Fix: 3 date bugs in detail/git.rs (format-local, Local::now, 1y month math)
- Stash indicator + [m] actions, session persistence, gh CLI token fallback
- Legacy UI modules removed; plan/ docs (roadmap, audit, debate, handoff)
This commit is contained in:
dualfroz committed 2026-07-06 14:42:52 +02:00
1 parent 5787b14880
commit 0468c032c6
100 files changed
+11614 -9840

No files matched your search

+60 -2
View File
@@ -1,3 +1,8 @@
//! `Provider` — the trait every hosting backend (GitHub, GitLab, Gitea/
//! Forgejo) implements, plus the data types its methods exchange and the
//! `make_provider`/`default_account` helpers that pick and construct the
//! right implementation for a given `Account`.
mod gitea;
mod github;
mod gitlab;
@@ -11,6 +16,7 @@ use anyhow::Result;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
/// Result of creating a remote repository: its clone URLs.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RepoInfo {
pub name: String,
@@ -18,12 +24,26 @@ pub struct RepoInfo {
pub ssh_url: String,
}
/// Which categories of remote repos to include in a bulk import. `owner` is
/// always true in practice (the user's own repos) — kept as a field rather
/// than implied so all three read the same way at the call site.
#[derive(Debug, Clone, Default)]
pub struct RepoAffiliation {
pub owner: bool,
pub collaborator: bool,
pub org_member: bool,
}
/// Result of creating a pull/merge request or issue: just enough to link
/// back to it (full detail, when needed, comes from `list_pull_requests`).
#[derive(Debug, Clone)]
pub struct PrInfo {
pub number: u64,
pub url: String,
}
/// Basic repo metadata fetched for the Settings tab and the hub's cached
/// stars/issues/PRs counts.
#[derive(Debug, Clone, Default)]
#[allow(dead_code)]
pub struct RepoMeta {
@@ -34,6 +54,8 @@ pub struct RepoMeta {
pub topics: Vec<String>,
}
/// One pull/merge request, as shown in the hub's PRs tab and the repo
/// detail view's PRs tab.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PrDetail {
pub repo: String,
@@ -49,6 +71,8 @@ pub struct PrDetail {
pub labels: Vec<String>,
}
/// One issue, as shown in the hub's Issues tab and the repo detail view's
/// Issues tab.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct IssueDetail {
pub repo: String,
@@ -62,14 +86,35 @@ pub struct IssueDetail {
pub comments: u32,
}
/// A hosting backend's API surface. Methods that not every provider
/// implements (`list_orgs`, `list_repos`, optional repo settings) have a
/// default that either returns an empty result or bails with a clear
/// "not supported" error, so callers can call any method on any provider
/// without matching on which one it is.
#[async_trait]
#[allow(dead_code)]
pub trait Provider: Send + Sync {
/// Create a new remote repository.
/// Create a new remote repository. `owner` is `Some(org)` to create it
/// under an organization/group instead of the authenticated user's
/// personal account.
async fn create_repo(
&self, name: &str, private: bool, description: &str,
&self, name: &str, private: bool, description: &str, owner: Option<&str>,
) -> Result<RepoInfo>;
/// Organizations/groups the authenticated user can create repos under.
/// Empty by default — providers without a sensible equivalent just don't
/// offer the picker.
async fn list_orgs(&self) -> Result<Vec<String>> {
Ok(vec![])
}
/// List repos visible to the authenticated user matching the given
/// affiliation flags. Empty by default — providers without a sensible
/// equivalent just don't offer bulk import.
async fn list_repos(&self, _affiliation: &RepoAffiliation) -> Result<Vec<RepoInfo>> {
Ok(vec![])
}
/// Delete a remote repository.
async fn delete_repo(&self, name: &str) -> Result<()>;
@@ -84,6 +129,12 @@ pub trait Provider: Send + Sync {
&self, repo: &str, title: &str, body: &str, head: &str, base: &str,
) -> Result<PrInfo>;
/// Open a new issue. Not every provider's API is wired up yet — default
/// bails the same way other not-yet-implemented methods on this trait do.
async fn create_issue(&self, _repo: &str, _title: &str, _body: &str) -> Result<PrInfo> {
anyhow::bail!("create_issue not supported by this provider")
}
/// Request reviewers for an open pull request / merge request.
async fn request_reviewers(
&self, _repo: &str, _pr_number: u64, _reviewers: &[String],
@@ -119,6 +170,13 @@ pub trait Provider: Send + Sync {
Ok(vec![])
}
/// Total hit count for a provider-side issue/PR search query (e.g.
/// "is:pr is:merged user:X") — used by the Overview tab's merged/closed
/// counters. Providers without a search API report nothing.
async fn count_search(&self, _query: &str) -> Result<u64> {
anyhow::bail!("count_search not supported by this provider")
}
/// Verify the token works, return the authenticated username.
async fn whoami(&self) -> Result<String>;