From df7b5c6aa4a00dfca66dc802f1e813f23f27a5b9 Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Tue, 8 Jul 2025 22:21:09 -0600 Subject: [PATCH] add `ent list` filter by assignee I'm not sure about the filter format... There are two independent filters: "state" and "assignee". The "state" filter defaults to including issues whose state is InProgress, Blocked, Backlog, or New. The "assignee" filter defaults to including all issues, assigned or not. The two filters can be independently overridden by the `ent list FILTER` command. FILTER is a string containing chunks separated by ":", like the PATH environment variable. Each chunk is of the form "name=value[,value...]". "name" can be either "state" or "assignee". The "value" arguments to the "state" filter must be one of the valid states, or it's a parse error. The "value" arguments to the "assignee" filter are used to string-compare against the issues "assignee" field, exact matches are accepted and everything else is rejected. A special assignee filter of the empty string matches issues that don't have an assignee. Some examples: * `ent list` shows issues in the states listed above, and don't filter based on assignee at all. * `ent list assignee=seb` shows issues in the states listed above, but only if the assignee is "seb". * `ent list assignee=seb,` shows issues in the states listed above, but only if the assignee is "seb" or if there is no assignee. * `ent list state=done` shows all issues in the Done state. * `ent list state=done:assignee=seb` shows issues in the Done state that are assigned to "seb". --- src/bin/ent/main.rs | 24 ++++++++++++++----- src/lib.rs | 56 +++++++++++++++++++++++++++++++++------------ 2 files changed, 59 insertions(+), 21 deletions(-) diff --git a/src/bin/ent/main.rs b/src/bin/ent/main.rs index 2a8f1f4..2892d6a 100644 --- a/src/bin/ent/main.rs +++ b/src/bin/ent/main.rs @@ -71,18 +71,30 @@ fn handle_command(args: &Args, issues_dir: &std::path::Path) -> anyhow::Result<( Commands::List { filter } => { let issues = entomologist::issues::Issues::new_from_dir(std::path::Path::new(issues_dir))?; - let filter = entomologist::parse_filter(filter)?; + let filter = entomologist::Filter::new_from_str(filter)?; let mut uuids_by_state = std::collections::HashMap::< entomologist::issue::State, Vec<&entomologist::issue::IssueHandle>, >::new(); for (uuid, issue) in issues.issues.iter() { - if filter.include_states.contains(&issue.state) { - uuids_by_state - .entry(issue.state.clone()) - .or_default() - .push(uuid); + if !filter.include_states.contains(&issue.state) { + continue; } + if filter.include_assignees.len() > 0 { + let assignee = match &issue.assignee { + Some(assignee) => assignee, + None => "", + }; + if !filter.include_assignees.contains(assignee) { + continue; + } + } + + // This issue passed all the filters, include it in list. + uuids_by_state + .entry(issue.state.clone()) + .or_default() + .push(uuid); } use entomologist::issue::State; diff --git a/src/lib.rs b/src/lib.rs index 77a00d9..b28fb74 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,24 +17,50 @@ pub enum ParseFilterError { // i'm starting with obvious easy things. Chumsky looks appealing but // more research is needed. #[derive(Debug)] -pub struct Filter { +pub struct Filter<'a> { pub include_states: std::collections::HashSet, + pub include_assignees: std::collections::HashSet<&'a str>, } -// Parses a filter description matching "state=STATE[,STATE*]" -pub fn parse_filter(filter_str: &str) -> Result { - let tokens: Vec<&str> = filter_str.split("=").collect(); - if tokens.len() != 2 { - return Err(ParseFilterError::ParseError); - } - if tokens[0] != "state" { - return Err(ParseFilterError::ParseError); - } +impl<'a> Filter<'a> { + pub fn new_from_str(filter_str: &'a str) -> Result, ParseFilterError> { + use crate::issue::State; + let mut f = Filter { + include_states: std::collections::HashSet::::from([ + State::InProgress, + State::Blocked, + State::Backlog, + State::New, + ]), + include_assignees: std::collections::HashSet::<&'a str>::new(), + }; - let mut include_states = std::collections::HashSet::::new(); - for s in tokens[1].split(",") { - include_states.insert(crate::issue::State::from_str(s)?); - } + for filter_chunk_str in filter_str.split(":") { + let tokens: Vec<&str> = filter_chunk_str.split("=").collect(); + if tokens.len() != 2 { + return Err(ParseFilterError::ParseError); + } - Ok(Filter { include_states }) + match tokens[0] { + "state" => { + f.include_states.clear(); + for s in tokens[1].split(",") { + f.include_states.insert(crate::issue::State::from_str(s)?); + } + } + "assignee" => { + f.include_assignees.clear(); + for s in tokens[1].split(",") { + f.include_assignees.insert(s); + } + } + _ => { + println!("unknown filter chunk '{}'", filter_chunk_str); + return Err(ParseFilterError::ParseError); + } + } + } + + Ok(f) + } }