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".
This commit is contained in:
Sebastian Kuzminsky 2025-07-08 22:21:09 -06:00
parent 86a22f88f3
commit df7b5c6aa4
2 changed files with 59 additions and 21 deletions

View file

@ -71,19 +71,31 @@ 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) {
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;
for state in [

View file

@ -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<crate::issue::State>,
pub include_assignees: std::collections::HashSet<&'a str>,
}
// Parses a filter description matching "state=STATE[,STATE*]"
pub fn parse_filter(filter_str: &str) -> Result<Filter, ParseFilterError> {
let tokens: Vec<&str> = filter_str.split("=").collect();
impl<'a> Filter<'a> {
pub fn new_from_str(filter_str: &'a str) -> Result<Filter<'a>, ParseFilterError> {
use crate::issue::State;
let mut f = Filter {
include_states: std::collections::HashSet::<crate::issue::State>::from([
State::InProgress,
State::Blocked,
State::Backlog,
State::New,
]),
include_assignees: std::collections::HashSet::<&'a str>::new(),
};
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);
}
if tokens[0] != "state" {
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);
}
let mut include_states = std::collections::HashSet::<crate::issue::State>::new();
for s in tokens[1].split(",") {
include_states.insert(crate::issue::State::from_str(s)?);
}
}
Ok(Filter { include_states })
Ok(f)
}
}