hive-forge: lint no-reviewer checks actual requested-reviewers, not a text mention
This commit is contained in:
parent
a155ea7b7d
commit
9b29c6a172
3 changed files with 38 additions and 46 deletions
|
|
@ -19,10 +19,12 @@ dimension directly.
|
|||
whatever your repo's own label taxonomy uses - there's nothing
|
||||
hardcoded, so check `repo-labels` first if you don't already know the
|
||||
scopes in use.
|
||||
- **PRs with no reviewer engagement** - `hive-forge lint no-reviewer
|
||||
--reviewer <name>` flags PRs where the named reviewer hasn't left a
|
||||
formal review or mention. The reviewer name is per-repo/per-team, not
|
||||
a fixed value.
|
||||
- **PRs with no formally requested reviewer** - `hive-forge lint
|
||||
no-reviewer` flags PRs with zero requested reviewers at all; add
|
||||
`--reviewer <name>` to instead flag PRs where that specific person
|
||||
isn't among the requested reviewers. This checks the forge's actual
|
||||
reviewer-request state, not a text `@name` mention anywhere in the
|
||||
thread.
|
||||
- **Assignment load** - `hive-forge lint assignments` groups open
|
||||
issues + PRs by assignee, useful for spotting an overloaded or
|
||||
neglected owner.
|
||||
|
|
|
|||
|
|
@ -69,7 +69,8 @@ hive-forge branches deployed/ # filter branches by pattern
|
|||
hive-forge tree-sha main # git tree SHA for a ref
|
||||
hive-forge -r other-org/other-repo pr 7 # target a different repo
|
||||
hive-forge lint unassigned # open issues/PRs with no assignee
|
||||
hive-forge lint no-reviewer --reviewer argus # PRs missing a reviewer comment from argus
|
||||
hive-forge lint no-reviewer # PRs with zero formally requested reviewers
|
||||
hive-forge lint no-reviewer --reviewer argus # PRs where argus specifically isn't a requested reviewer
|
||||
hive-forge lint stale-branches --days 14 # branches with no recent activity
|
||||
hive-forge lint assignments # per-assignee open item count
|
||||
hive-forge lint unlabeled --scope type # open issues/PRs with no exclusive type/* label (any scope works, e.g. --scope area)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
//!
|
||||
//! Sub-commands:
|
||||
//! - `unassigned [--type issues|pulls|all] [--state open|closed|all]`
|
||||
//! - `no-reviewer --reviewer NAME [--state open|closed|all]`
|
||||
//! - `no-reviewer [--reviewer NAME] [--state open|closed|all]`
|
||||
//! - `stale-branches [--days N]`
|
||||
//! - `assignments [--user NAME]`
|
||||
//! - `unlabeled --scope NAME [--type issues|pulls|all] [--state open|closed|all]`
|
||||
|
|
@ -15,8 +15,8 @@ use std::collections::BTreeMap;
|
|||
use anyhow::{Result, bail};
|
||||
use clap::{Args as ClapArgs, Subcommand, ValueEnum};
|
||||
use forgejo_api::structs::{
|
||||
Issue, IssueGetCommentsQuery, IssueListIssuesQuery, IssueListIssuesQueryState,
|
||||
IssueListIssuesQueryType, RepoListPullRequestsQuery, RepoListPullRequestsQueryState,
|
||||
Issue, IssueListIssuesQuery, IssueListIssuesQueryState, IssueListIssuesQueryType,
|
||||
RepoListPullRequestsQuery, RepoListPullRequestsQueryState,
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
use time::OffsetDateTime;
|
||||
|
|
@ -41,7 +41,7 @@ pub struct Args {
|
|||
enum Sub {
|
||||
/// List issues or PRs without an assignee.
|
||||
Unassigned(UnassignedArgs),
|
||||
/// List PRs with no `@reviewer` mention in any comment.
|
||||
/// List PRs with no formally requested reviewer.
|
||||
NoReviewer(NoReviewerArgs),
|
||||
/// List remote branches with no commits in N days.
|
||||
/// Skips branches that are heads of open PRs.
|
||||
|
|
@ -115,10 +115,11 @@ struct NoReviewerArgs {
|
|||
/// Filter by PR state.
|
||||
#[arg(long, value_enum, default_value_t = State::Open)]
|
||||
state: State,
|
||||
/// Reviewer login to look for (matches `@<reviewer>` in the PR body
|
||||
/// or any comment).
|
||||
/// Reviewer login to check for. Omit to flag any PR with zero
|
||||
/// formally requested reviewers; pass it to instead flag PRs where
|
||||
/// this specific login isn't among the requested reviewers.
|
||||
#[arg(long)]
|
||||
reviewer: String,
|
||||
reviewer: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(ClapArgs)]
|
||||
|
|
@ -228,49 +229,37 @@ fn run_no_reviewer(client: &Client, args: NoReviewerArgs) -> Result<()> {
|
|||
break;
|
||||
}
|
||||
}
|
||||
let needle = format!("@{}", args.reviewer);
|
||||
let mut missing: Vec<Value> = Vec::new();
|
||||
for pr in &pulls {
|
||||
let Some(number) = pr.number.filter(|n| *n > 0) else {
|
||||
continue;
|
||||
};
|
||||
// Check PR body itself first — saves a comment-fetch on freshly-opened PRs
|
||||
// that already @reviewer in the description.
|
||||
if pr.body.as_deref().unwrap_or("").contains(&needle) {
|
||||
continue;
|
||||
}
|
||||
// Paginate so PRs with >50 comments don't yield false positives
|
||||
// (flagged in review). Same 1000-comment ceiling as elsewhere.
|
||||
let mut mentioned = false;
|
||||
for page in 1..=MAX_PAGES {
|
||||
let (_, comments) = client
|
||||
.api()
|
||||
.issue_get_comments(owner, name, number, IssueGetCommentsQuery::default())
|
||||
.page(page)
|
||||
.page_size(PAGE_LIMIT)
|
||||
.send()?;
|
||||
let short = comments.len() < PAGE_LIMIT as usize;
|
||||
mentioned = comments
|
||||
.iter()
|
||||
.any(|c| c.body.as_deref().is_some_and(|body| body.contains(&needle)));
|
||||
if mentioned || short {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !mentioned {
|
||||
missing.push(json!({
|
||||
let missing: Vec<Value> = pulls
|
||||
.iter()
|
||||
.filter(|pr| !has_requested_reviewer(pr, args.reviewer.as_deref()))
|
||||
.map(|pr| {
|
||||
json!({
|
||||
"number": pr.number,
|
||||
"title": pr.title,
|
||||
"state": pr.state,
|
||||
"url": pr.html_url,
|
||||
"is_pr": true,
|
||||
"assignees": logins(pr.assignees.as_deref()),
|
||||
}));
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
emit(client, &missing, |it| format!("#{} {}", num(it), title(it)))
|
||||
}
|
||||
|
||||
/// True if the PR's *formal* `requested_reviewers` list (Forgejo's own
|
||||
/// review-request state, set by `pr assign-reviewer` — not a text
|
||||
/// `@name` mention anywhere in the body/comments) already satisfies the
|
||||
/// check: with `wanted` set, that specific login must be among the
|
||||
/// requested reviewers; with `wanted` absent, any requested reviewer at
|
||||
/// all counts.
|
||||
fn has_requested_reviewer(pr: &forgejo_api::structs::PullRequest, wanted: Option<&str>) -> bool {
|
||||
let requested = pr.requested_reviewers.as_deref().unwrap_or_default();
|
||||
match wanted {
|
||||
Some(login) => requested.iter().any(|u| u.login.as_deref() == Some(login)),
|
||||
None => !requested.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────── stale-branches ───────────────────────
|
||||
|
||||
fn run_stale_branches(client: &Client, args: StaleBranchesArgs) -> Result<()> {
|
||||
|
|
|
|||
Loading…
Reference in a new issue