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
|
whatever your repo's own label taxonomy uses - there's nothing
|
||||||
hardcoded, so check `repo-labels` first if you don't already know the
|
hardcoded, so check `repo-labels` first if you don't already know the
|
||||||
scopes in use.
|
scopes in use.
|
||||||
- **PRs with no reviewer engagement** - `hive-forge lint no-reviewer
|
- **PRs with no formally requested reviewer** - `hive-forge lint
|
||||||
--reviewer <name>` flags PRs where the named reviewer hasn't left a
|
no-reviewer` flags PRs with zero requested reviewers at all; add
|
||||||
formal review or mention. The reviewer name is per-repo/per-team, not
|
`--reviewer <name>` to instead flag PRs where that specific person
|
||||||
a fixed value.
|
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
|
- **Assignment load** - `hive-forge lint assignments` groups open
|
||||||
issues + PRs by assignee, useful for spotting an overloaded or
|
issues + PRs by assignee, useful for spotting an overloaded or
|
||||||
neglected owner.
|
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 tree-sha main # git tree SHA for a ref
|
||||||
hive-forge -r other-org/other-repo pr 7 # target a different repo
|
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 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 stale-branches --days 14 # branches with no recent activity
|
||||||
hive-forge lint assignments # per-assignee open item count
|
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)
|
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:
|
//! Sub-commands:
|
||||||
//! - `unassigned [--type issues|pulls|all] [--state open|closed|all]`
|
//! - `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]`
|
//! - `stale-branches [--days N]`
|
||||||
//! - `assignments [--user NAME]`
|
//! - `assignments [--user NAME]`
|
||||||
//! - `unlabeled --scope NAME [--type issues|pulls|all] [--state open|closed|all]`
|
//! - `unlabeled --scope NAME [--type issues|pulls|all] [--state open|closed|all]`
|
||||||
|
|
@ -15,8 +15,8 @@ use std::collections::BTreeMap;
|
||||||
use anyhow::{Result, bail};
|
use anyhow::{Result, bail};
|
||||||
use clap::{Args as ClapArgs, Subcommand, ValueEnum};
|
use clap::{Args as ClapArgs, Subcommand, ValueEnum};
|
||||||
use forgejo_api::structs::{
|
use forgejo_api::structs::{
|
||||||
Issue, IssueGetCommentsQuery, IssueListIssuesQuery, IssueListIssuesQueryState,
|
Issue, IssueListIssuesQuery, IssueListIssuesQueryState, IssueListIssuesQueryType,
|
||||||
IssueListIssuesQueryType, RepoListPullRequestsQuery, RepoListPullRequestsQueryState,
|
RepoListPullRequestsQuery, RepoListPullRequestsQueryState,
|
||||||
};
|
};
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|
@ -41,7 +41,7 @@ pub struct Args {
|
||||||
enum Sub {
|
enum Sub {
|
||||||
/// List issues or PRs without an assignee.
|
/// List issues or PRs without an assignee.
|
||||||
Unassigned(UnassignedArgs),
|
Unassigned(UnassignedArgs),
|
||||||
/// List PRs with no `@reviewer` mention in any comment.
|
/// List PRs with no formally requested reviewer.
|
||||||
NoReviewer(NoReviewerArgs),
|
NoReviewer(NoReviewerArgs),
|
||||||
/// List remote branches with no commits in N days.
|
/// List remote branches with no commits in N days.
|
||||||
/// Skips branches that are heads of open PRs.
|
/// Skips branches that are heads of open PRs.
|
||||||
|
|
@ -115,10 +115,11 @@ struct NoReviewerArgs {
|
||||||
/// Filter by PR state.
|
/// Filter by PR state.
|
||||||
#[arg(long, value_enum, default_value_t = State::Open)]
|
#[arg(long, value_enum, default_value_t = State::Open)]
|
||||||
state: State,
|
state: State,
|
||||||
/// Reviewer login to look for (matches `@<reviewer>` in the PR body
|
/// Reviewer login to check for. Omit to flag any PR with zero
|
||||||
/// or any comment).
|
/// formally requested reviewers; pass it to instead flag PRs where
|
||||||
|
/// this specific login isn't among the requested reviewers.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
reviewer: String,
|
reviewer: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(ClapArgs)]
|
#[derive(ClapArgs)]
|
||||||
|
|
@ -228,49 +229,37 @@ fn run_no_reviewer(client: &Client, args: NoReviewerArgs) -> Result<()> {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let needle = format!("@{}", args.reviewer);
|
let missing: Vec<Value> = pulls
|
||||||
let mut missing: Vec<Value> = Vec::new();
|
.iter()
|
||||||
for pr in &pulls {
|
.filter(|pr| !has_requested_reviewer(pr, args.reviewer.as_deref()))
|
||||||
let Some(number) = pr.number.filter(|n| *n > 0) else {
|
.map(|pr| {
|
||||||
continue;
|
json!({
|
||||||
};
|
|
||||||
// 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!({
|
|
||||||
"number": pr.number,
|
"number": pr.number,
|
||||||
"title": pr.title,
|
"title": pr.title,
|
||||||
"state": pr.state,
|
"state": pr.state,
|
||||||
"url": pr.html_url,
|
"url": pr.html_url,
|
||||||
"is_pr": true,
|
"is_pr": true,
|
||||||
"assignees": logins(pr.assignees.as_deref()),
|
"assignees": logins(pr.assignees.as_deref()),
|
||||||
}));
|
})
|
||||||
}
|
})
|
||||||
}
|
.collect();
|
||||||
emit(client, &missing, |it| format!("#{} {}", num(it), title(it)))
|
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 ───────────────────────
|
// ─────────────────────── stale-branches ───────────────────────
|
||||||
|
|
||||||
fn run_stale_branches(client: &Client, args: StaleBranchesArgs) -> Result<()> {
|
fn run_stale_branches(client: &Client, args: StaleBranchesArgs) -> Result<()> {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue