hive-forge: lint stale-branches reports each branch's merge outcome
This commit is contained in:
parent
b35063cf46
commit
6e05863f02
3 changed files with 54 additions and 16 deletions
|
|
@ -15,8 +15,8 @@ use std::collections::BTreeMap;
|
|||
use anyhow::{Result, bail};
|
||||
use clap::{Args as ClapArgs, Subcommand, ValueEnum};
|
||||
use forgejo_api::structs::{
|
||||
Issue, IssueListIssuesQuery, IssueListIssuesQueryState, IssueListIssuesQueryType,
|
||||
RepoListPullRequestsQuery, RepoListPullRequestsQueryState,
|
||||
Issue, IssueListIssuesQuery, IssueListIssuesQueryState, IssueListIssuesQueryType, PullRequest,
|
||||
RepoListPullRequestsQuery, RepoListPullRequestsQueryState, StateType,
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
use time::OffsetDateTime;
|
||||
|
|
@ -43,8 +43,8 @@ enum Sub {
|
|||
Unassigned(UnassignedArgs),
|
||||
/// 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.
|
||||
/// List remote branches with no commits in N days, each with its
|
||||
/// merge outcome (skips branches that are heads of open PRs).
|
||||
StaleBranches(StaleBranchesArgs),
|
||||
/// Group open issues + PRs by assignee.
|
||||
Assignments(AssignmentsArgs),
|
||||
|
|
@ -281,12 +281,18 @@ fn run_stale_branches(client: &Client, args: StaleBranchesArgs) -> Result<()> {
|
|||
break;
|
||||
}
|
||||
}
|
||||
// Collect active PR head refs to skip — a branch with an open PR
|
||||
// isn't "stale", it's "in review".
|
||||
let mut active_heads: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||
// One pass over every PR, any state, doing double duty: an open
|
||||
// head isn't "stale", it's "in review" (skip-list, as before), and
|
||||
// a closed one tells a surviving stale branch its actual fate —
|
||||
// merged (branch is a leftover copy, safe to delete) vs. closed
|
||||
// unmerged / never had a PR (the branch is the only copy). Because
|
||||
// hyperhive squash-merges, a merged branch's tip is never an
|
||||
// ancestor of main, so ancestry can't tell these apart; this is the
|
||||
// only signal that can.
|
||||
let mut prs = Vec::new();
|
||||
for page in 1..=MAX_PAGES {
|
||||
let query = RepoListPullRequestsQuery {
|
||||
state: Some(RepoListPullRequestsQueryState::Open),
|
||||
state: Some(RepoListPullRequestsQueryState::All),
|
||||
..Default::default()
|
||||
};
|
||||
let (_, batch) = client
|
||||
|
|
@ -296,15 +302,30 @@ fn run_stale_branches(client: &Client, args: StaleBranchesArgs) -> Result<()> {
|
|||
.page_size(PAGE_LIMIT)
|
||||
.send()?;
|
||||
let short = batch.len() < PAGE_LIMIT as usize;
|
||||
active_heads.extend(
|
||||
batch
|
||||
.iter()
|
||||
.filter_map(|p| p.head.as_ref().and_then(|h| h.r#ref.clone())),
|
||||
);
|
||||
prs.extend(batch);
|
||||
if short {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let mut active_heads: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||
// Highest-numbered (most recent) PR per head branch, in case a
|
||||
// branch name was reused across more than one PR over its life.
|
||||
let mut latest_pr_by_head: std::collections::HashMap<String, &PullRequest> =
|
||||
std::collections::HashMap::new();
|
||||
for pr in &prs {
|
||||
let Some(head_ref) = pr.head.as_ref().and_then(|h| h.r#ref.clone()) else {
|
||||
continue;
|
||||
};
|
||||
if pr.state == Some(StateType::Open) {
|
||||
active_heads.insert(head_ref.clone());
|
||||
}
|
||||
let is_newer = latest_pr_by_head
|
||||
.get(&head_ref)
|
||||
.is_none_or(|existing| pr.number.unwrap_or(0) > existing.number.unwrap_or(0));
|
||||
if is_newer {
|
||||
latest_pr_by_head.insert(head_ref, pr);
|
||||
}
|
||||
}
|
||||
|
||||
let today = OffsetDateTime::now_utc().date();
|
||||
let mut stale: Vec<Value> = Vec::new();
|
||||
|
|
@ -320,17 +341,31 @@ fn run_stale_branches(client: &Client, args: StaleBranchesArgs) -> Result<()> {
|
|||
// date-granular, matching the old YYYY-MM-DD prefix math.
|
||||
let age_days = (today - ts.date()).whole_days();
|
||||
if age_days >= args.days {
|
||||
// A branch surviving to here can only have a *closed* PR
|
||||
// (an open one would already be in `active_heads`), so
|
||||
// `merged` is unambiguous when a PR exists at all.
|
||||
let pr = latest_pr_by_head.get(branch_name);
|
||||
stale.push(json!({
|
||||
"name": branch_name,
|
||||
"last_commit": rfc3339(Some(ts)),
|
||||
"age_days": age_days,
|
||||
"pr": pr.and_then(|p| p.number),
|
||||
"merged": pr.and_then(|p| p.merged),
|
||||
}));
|
||||
}
|
||||
}
|
||||
emit(client, &stale, |it| {
|
||||
let name = it.get("name").and_then(Value::as_str).unwrap_or("?");
|
||||
let age = it.get("age_days").and_then(Value::as_i64).unwrap_or(0);
|
||||
format!("{name} ({age}d)")
|
||||
let verdict = match (
|
||||
it.get("pr").and_then(Value::as_i64),
|
||||
it.get("merged").and_then(Value::as_bool),
|
||||
) {
|
||||
(Some(pr), Some(true)) => format!("PR #{pr} merged"),
|
||||
(Some(pr), Some(false)) => format!("PR #{pr} closed, not merged"),
|
||||
_ => "no PR".to_string(),
|
||||
};
|
||||
format!("{name} ({age}d) — {verdict}")
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue