fix(forge): validate list's label + milestone filters, and paginate both
A filter value the forge cannot resolve is DISCARDED, not rejected, so a typo does not narrow the result set -- it returns the unfiltered one. That does not waste a query, it inverts the answer: "is anything open in this milestone" comes back as every open issue and reads as yes, and a duplicate check gets a list that never narrowed. `list` now resolves both before querying. Labels reuse the write side's resolver; the ids are discarded because this endpoint filters by name, so resolution here is a spell-check rather than a lookup -- reusing it keeps the message identical to the one the write side has always produced. Milestones accept a title or an id and are checked against the ALL-state set: filtering on a closed milestone is a normal query, and validating against open-only would reject exactly the retrospective ones. Both fetchers paginate. `repo_labels` asked for one page of 100 and treated it as the population -- the inverse of the trailer bug, same root: a valid label past the cut fails to resolve, and the error then prints an "available labels" list that is itself truncated, so the message argues for the typo. `--assignee` / `--author` stay unvalidated on purpose: someone who has left still legitimately appears on old issues, so a login that is not a current member is not necessarily a typo. Also drops the docs paragraph claiming unknown labels are silently dropped on the write side; that has not been true since the resolver landed.
This commit is contained in:
parent
433b294099
commit
0aa9a854bc
4 changed files with 206 additions and 36 deletions
|
|
@ -24,7 +24,7 @@ use forgejo_api::structs::{
|
|||
use serde_json::Value;
|
||||
|
||||
use crate::client::Client;
|
||||
use crate::verbs::print_json;
|
||||
use crate::verbs::{labels, milestone, print_json};
|
||||
|
||||
/// What kind of items to return. Maps onto Forgejo's `type` query
|
||||
/// parameter: `issues` / `pulls`, or no filter at all for `both`
|
||||
|
|
@ -89,12 +89,15 @@ pub struct Args {
|
|||
#[arg(long)]
|
||||
mention: Option<String>,
|
||||
/// Filter to items carrying any of these label names. Repeatable.
|
||||
/// Validated client-side: a name the forge can't resolve is dropped
|
||||
/// from the filter rather than rejected, which returns MORE results
|
||||
/// than asked for, not fewer.
|
||||
#[arg(long = "label")]
|
||||
labels: Vec<String>,
|
||||
/// Filter to items in any of these milestones, by name or id.
|
||||
/// Repeatable. A name that doesn't exist is discarded by the forge
|
||||
/// rather than rejected — so a typo returns the UNFILTERED list, not
|
||||
/// an empty one. Check the spelling against `milestone list`.
|
||||
/// Filter to items in any of these milestones, by title or id.
|
||||
/// Repeatable. Validated client-side against the repo's milestones
|
||||
/// (closed ones included), since the forge would silently discard a
|
||||
/// name it can't resolve and return the UNFILTERED list.
|
||||
#[arg(long = "milestone")]
|
||||
milestones: Vec<String>,
|
||||
/// Full-text search over title AND body, server-side. Composes with
|
||||
|
|
@ -117,6 +120,29 @@ pub struct Args {
|
|||
|
||||
pub fn run(client: &Client, args: Args) -> Result<()> {
|
||||
let (owner, name) = client.owner_repo()?;
|
||||
// Validate the name-based filters BEFORE querying. The forge
|
||||
// *discards* a label or milestone it can't resolve instead of
|
||||
// rejecting it, so a typo doesn't narrow the result set — it returns
|
||||
// the UNFILTERED one. That doesn't waste a query, it inverts the
|
||||
// answer: "is anything open in this milestone" comes back as every
|
||||
// open issue and reads as yes, and a duplicate check gets a list that
|
||||
// never narrowed and concludes there isn't one.
|
||||
//
|
||||
// The ids are discarded on purpose — this endpoint filters by name,
|
||||
// so resolution here is a spell-check, not a lookup. `resolve_ids` is
|
||||
// reused rather than reimplemented so the message stays identical to
|
||||
// the one the write side has always produced.
|
||||
//
|
||||
// Costs one extra round-trip per filtered invocation, and only when
|
||||
// the filter is actually used.
|
||||
if !args.labels.is_empty() {
|
||||
let all = labels::repo_labels(client)?;
|
||||
labels::resolve_ids(&all, &args.labels)?;
|
||||
}
|
||||
if !args.milestones.is_empty() {
|
||||
let all = milestone::repo_milestones(client, "all")?;
|
||||
milestone::ensure_filters_resolve(&all, &args.milestones)?;
|
||||
}
|
||||
let query = IssueListIssuesQuery {
|
||||
state: Some(args.state.query_state()),
|
||||
// The forge parses `labels` as a comma-separated list of names.
|
||||
|
|
|
|||
Loading…
Reference in a new issue