hive-forge: add lint unlabeled --scope subcommand
This commit is contained in:
parent
5c4a637941
commit
c0c8e2a2d6
3 changed files with 54 additions and 1 deletions
|
|
@ -70,6 +70,7 @@ 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 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)
|
||||
hive-forge pr-status --pr 42 # PR health: mergeable, CI, reviews, last comment (exit 0 = ready)
|
||||
hive-forge pr-status --sha <sha> # CI-only fast path for an explicit commit sha
|
||||
hive-forge pr-merge 42 # merge (refuses unless mergeable + CI not red + no changes-requested); deletes head branch
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ enum Verb {
|
|||
RepoLabels(verbs::repo_labels::Args),
|
||||
/// Search the forge for repositories by keyword, topic, or description.
|
||||
RepoSearch(verbs::repo_search::Args),
|
||||
/// Triage lint queries (unassigned / no-reviewer / stale-branches / assignments).
|
||||
/// Triage lint queries (unassigned / no-reviewer / stale-branches / assignments / unlabeled).
|
||||
Lint(verbs::lint::Args),
|
||||
/// List issues / PRs with filters (`--kind`, `--state`, `--assignee`,
|
||||
/// `--author`, `--label`, `--limit`). Pretty rows by default; pass
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
//! - `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]`
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
|
|
@ -47,6 +48,11 @@ enum Sub {
|
|||
StaleBranches(StaleBranchesArgs),
|
||||
/// Group open issues + PRs by assignee.
|
||||
Assignments(AssignmentsArgs),
|
||||
/// List issues/PRs with no exclusive scoped label in `--scope`
|
||||
/// (e.g. `--scope type` flags items missing any `type/*` label).
|
||||
/// Generic — the scope is whatever the repo's label taxonomy
|
||||
/// actually uses, nothing hardcoded here.
|
||||
Unlabeled(UnlabeledArgs),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, ValueEnum)]
|
||||
|
|
@ -129,12 +135,29 @@ struct AssignmentsArgs {
|
|||
user: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(ClapArgs)]
|
||||
struct UnlabeledArgs {
|
||||
/// Label scope to check for — the part of a scoped label's name
|
||||
/// before the `/` (e.g. `type` for `type/bug`, `type/feature`).
|
||||
/// Required: this command has no built-in notion of a repo's label
|
||||
/// taxonomy, so there's no sane default to fall back to.
|
||||
#[arg(long)]
|
||||
scope: String,
|
||||
/// Filter by item kind.
|
||||
#[arg(long, value_enum, default_value_t = Kind::All)]
|
||||
r#type: Kind,
|
||||
/// Filter by item state.
|
||||
#[arg(long, value_enum, default_value_t = State::Open)]
|
||||
state: State,
|
||||
}
|
||||
|
||||
pub fn run(client: &Client, args: Args) -> Result<()> {
|
||||
match args.sub {
|
||||
Sub::Unassigned(a) => run_unassigned(client, a),
|
||||
Sub::NoReviewer(a) => run_no_reviewer(client, a),
|
||||
Sub::StaleBranches(a) => run_stale_branches(client, a),
|
||||
Sub::Assignments(a) => run_assignments(client, a),
|
||||
Sub::Unlabeled(a) => run_unlabeled(client, a),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -370,6 +393,35 @@ fn run_assignments(client: &Client, args: AssignmentsArgs) -> Result<()> {
|
|||
}
|
||||
}
|
||||
|
||||
// ───────────────────────── unlabeled ──────────────────────────
|
||||
|
||||
fn run_unlabeled(client: &Client, args: UnlabeledArgs) -> Result<()> {
|
||||
if args.scope.trim().is_empty() {
|
||||
bail!("hive-forge lint unlabeled: --scope must not be empty");
|
||||
}
|
||||
let prefix = format!("{}/", args.scope);
|
||||
let items = fetch_issues(client, args.r#type.query_type(), args.state.issue_state())?;
|
||||
let filtered: Vec<Value> = items
|
||||
.iter()
|
||||
.filter(|it| !has_scoped_label(it, &prefix))
|
||||
.map(trim_issue)
|
||||
.collect();
|
||||
emit(client, &filtered, |it| {
|
||||
format!("#{} [{}] {}", num(it), kind_label(it), title(it))
|
||||
})
|
||||
}
|
||||
|
||||
/// True if `it` carries an *exclusive* scoped label whose name starts
|
||||
/// with `prefix` (e.g. `"type/"`). Exclusivity is Forgejo's actual
|
||||
/// scoped-label marker — a plain label that merely happens to contain
|
||||
/// a `/` in its name doesn't count, so this can't misfire on an
|
||||
/// unrelated label naming convention.
|
||||
fn has_scoped_label(it: &Issue, prefix: &str) -> bool {
|
||||
it.labels.as_deref().unwrap_or_default().iter().any(|l| {
|
||||
l.exclusive == Some(true) && l.name.as_deref().is_some_and(|n| n.starts_with(prefix))
|
||||
})
|
||||
}
|
||||
|
||||
// ───────────────────────── shared helpers ─────────────────────
|
||||
|
||||
/// Assignee logins from a typed user list (missing logins dropped).
|
||||
|
|
|
|||
Loading…
Reference in a new issue