feat: hide PRs you can neither act on nor learn from
Overview and the PRs tab listed open PRs from every repo, including ones cloned for reference (e.g. pterodactyl) whose PRs you can't merge and didn't author — pure noise. A PR is now surfaced only when you authored it but can't merge it (your contribution elsewhere) or someone else authored it and you can (awaiting your review); your own PR in a repo you control, and strangers' PRs you can't touch, are filtered out. Providers report viewer merge permission (RepoMeta.viewer_can_merge, from GitHub/Gitea repo permissions and GitLab access level), stamped onto each PR at refresh. The cache keeps every PR; the filter is applied where the hub builds state.prs, so the per-repo detail view is unchanged.
This commit is contained in:
1 parent
4a3fa58745
commit
abb1e92064
8 files changed
+68
-5
No files matched your search
@@ -11,6 +11,34 @@ use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
use super::state::HubState;
|
||||
|
||||
// ── PR relevance ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// Account logins to treat as "you" when deciding PR authorship.
|
||||
pub fn account_logins(accounts: &[crate::auth::Account]) -> Vec<String> {
|
||||
accounts.iter().map(|a| a.username.clone()).filter(|u| !u.is_empty()).collect()
|
||||
}
|
||||
|
||||
/// Whether a PR is worth surfacing in the hub. Shown only when you authored
|
||||
/// it but can't merge it yourself (your contribution to a repo you don't
|
||||
/// control) or someone else authored it and you can (it's awaiting your
|
||||
/// review). Your own PR in a repo you can merge in, and strangers' PRs you
|
||||
/// can't act on, are both noise and filtered out. `logins` are your account
|
||||
/// usernames; an empty `logins` shows everything (identity unknown).
|
||||
pub fn pr_is_relevant(pr: &crate::providers::PrDetail, logins: &[String]) -> bool {
|
||||
if logins.is_empty() { return true; }
|
||||
let mine = logins.iter().any(|u| u.eq_ignore_ascii_case(&pr.author));
|
||||
mine != pr.viewer_can_merge
|
||||
}
|
||||
|
||||
/// Filters a cached PR list down to the relevant ones (see `pr_is_relevant`).
|
||||
pub fn relevant_prs(
|
||||
prs: impl IntoIterator<Item = crate::providers::PrDetail>,
|
||||
accounts: &[crate::auth::Account],
|
||||
) -> Vec<crate::providers::PrDetail> {
|
||||
let logins = account_logins(accounts);
|
||||
prs.into_iter().filter(|p| pr_is_relevant(p, &logins)).collect()
|
||||
}
|
||||
|
||||
// ── sync flow ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Runs `action` ("pull", "commit_push", or anything else meaning "push")
|
||||
|
||||
@@ -157,7 +157,7 @@ pub fn poll_refresh(state: &mut HubState) {
|
||||
for (name, i) in issues { fetched_iss += i.len(); cache.set_issues(&name, i); }
|
||||
let _ = crate::cache::save(&cache);
|
||||
|
||||
state.prs = cache.all_prs().into_iter().cloned().collect();
|
||||
state.prs = super::actions::relevant_prs(cache.all_prs().into_iter().cloned(), &state.accounts);
|
||||
state.issues = cache.all_issues().into_iter().cloned().collect();
|
||||
if closed_counts.is_some() { state.closed_counts = closed_counts; }
|
||||
|
||||
|
||||
+7
-3
@@ -91,14 +91,18 @@ pub async fn run_overview_tui(
|
||||
if r.visibility.is_none() { r.visibility = m.visibility.clone(); }
|
||||
}
|
||||
}
|
||||
let cached_prs: Vec<PrDetail> = meta_cache.all_prs().into_iter().cloned().collect();
|
||||
let cached_issues: Vec<IssueDetail> = meta_cache.all_issues().into_iter().cloned().collect();
|
||||
|
||||
let debug_mode = crate::debug::is_enabled();
|
||||
let debug_log = crate::debug::take();
|
||||
let accounts = crate::auth::load_accounts_path(&cfg.data_dir).unwrap_or_default();
|
||||
let templates = load_templates();
|
||||
|
||||
// The cache keeps every PR; the hub only surfaces the ones you can act on
|
||||
// (yours-elsewhere or awaiting-your-review — see actions::pr_is_relevant),
|
||||
// so PRs from repos you merely cloned don't clutter Overview and the tab.
|
||||
let cached_prs: Vec<PrDetail> =
|
||||
actions::relevant_prs(meta_cache.all_prs().into_iter().cloned(), &accounts);
|
||||
let cached_issues: Vec<IssueDetail> = meta_cache.all_issues().into_iter().cloned().collect();
|
||||
|
||||
let mut state = HubState {
|
||||
repos, projects, project_map, todos,
|
||||
prs: cached_prs,
|
||||
|
||||
@@ -816,6 +816,7 @@ pub fn spawn_api_refresh(state: &mut HubState) {
|
||||
.await;
|
||||
|
||||
for (name, slug, meta, p, i, ci) in results {
|
||||
let can_merge = meta.as_ref().map(|m| m.viewer_can_merge).unwrap_or(false);
|
||||
if let Some(meta) = meta {
|
||||
let slug_lc = slug.to_lowercase();
|
||||
let dep = dependabot
|
||||
@@ -837,7 +838,10 @@ pub fn spawn_api_refresh(state: &mut HubState) {
|
||||
fetched_at: chrono::Utc::now().timestamp(),
|
||||
}));
|
||||
}
|
||||
if let Some(p) = p { prs.push((name.clone(), p)); }
|
||||
if let Some(mut p) = p {
|
||||
for pr in &mut p { pr.viewer_can_merge = can_merge; }
|
||||
prs.push((name.clone(), p));
|
||||
}
|
||||
if let Some(i) = i { issues.push((name, i)); }
|
||||
}
|
||||
|
||||
|
||||
@@ -199,6 +199,10 @@ impl Provider for GiteaProvider {
|
||||
stars: v["stars_count"].as_u64().unwrap_or(0) as u32,
|
||||
description: v["description"].as_str().filter(|s| !s.is_empty()).map(String::from),
|
||||
topics: vec![],
|
||||
viewer_can_merge: {
|
||||
let p = &v["permissions"];
|
||||
p["push"].as_bool().unwrap_or(false) || p["admin"].as_bool().unwrap_or(false)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -243,12 +243,20 @@ impl Provider for GitHubProvider {
|
||||
.map(|arr| arr.iter().filter_map(|t| t.as_str().map(String::from)).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
// `permissions` is present only on authenticated requests; write,
|
||||
// maintain or admin all imply the ability to merge PRs.
|
||||
let perms = &v["permissions"];
|
||||
let viewer_can_merge = perms["push"].as_bool().unwrap_or(false)
|
||||
|| perms["maintain"].as_bool().unwrap_or(false)
|
||||
|| perms["admin"].as_bool().unwrap_or(false);
|
||||
|
||||
Ok(RepoMeta {
|
||||
open_issues: v["open_issues_count"].as_u64().unwrap_or(0) as u32,
|
||||
open_prs,
|
||||
stars: v["stargazers_count"].as_u64().unwrap_or(0) as u32,
|
||||
description,
|
||||
topics,
|
||||
viewer_can_merge,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -399,6 +407,9 @@ impl Provider for GitHubProvider {
|
||||
url: pr["html_url"].as_str().unwrap_or("").to_string(),
|
||||
draft: pr["draft"].as_bool().unwrap_or(false),
|
||||
labels,
|
||||
// Stamped later from the repo's viewer permission (list_pull_requests
|
||||
// has no repo-level permission field); false until then.
|
||||
viewer_can_merge: false,
|
||||
}
|
||||
}).collect()).unwrap_or_default();
|
||||
Ok(prs)
|
||||
|
||||
@@ -196,6 +196,7 @@ impl Provider for GitLabProvider {
|
||||
stars: v["star_count"].as_u64().unwrap_or(0) as u32,
|
||||
description: v["description"].as_str().filter(|s| !s.is_empty()).map(String::from),
|
||||
topics: v["topics"].as_array().map(|a| a.iter().filter_map(|t| t.as_str().map(String::from)).collect()).unwrap_or_default(),
|
||||
viewer_can_merge: v["permissions"]["project_access"]["access_level"].as_u64().unwrap_or(0) >= 30,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,10 @@ pub struct RepoMeta {
|
||||
pub stars: u32,
|
||||
pub description: Option<String>,
|
||||
pub topics: Vec<String>,
|
||||
/// Whether the authenticated user can merge PRs here (write/maintain/admin
|
||||
/// permission). Lets the hub hide PRs it can neither review-merge nor
|
||||
/// learn anything from. Providers that don't expose this leave it false.
|
||||
pub viewer_can_merge: bool,
|
||||
}
|
||||
|
||||
/// One pull/merge request, as shown in the hub's PRs tab and the repo
|
||||
@@ -69,6 +73,13 @@ pub struct PrDetail {
|
||||
pub url: String,
|
||||
pub draft: bool,
|
||||
pub labels: Vec<String>,
|
||||
/// Whether you can merge this PR (from the repo's viewer permission,
|
||||
/// stamped at refresh). Combined with `author` it decides relevance:
|
||||
/// the hub shows a PR only when you authored it but can't merge it
|
||||
/// (your contribution elsewhere) or someone else authored it and you
|
||||
/// can (awaiting your review) — never your own PR in your own repo.
|
||||
#[serde(default)]
|
||||
pub viewer_can_merge: bool,
|
||||
}
|
||||
|
||||
/// One issue, as shown in the hub's Issues tab and the repo detail view's
|
||||
|
||||
Reference in new issue
Block a user