From 20c17f281b656ec135af0526783ee1acd2ce5ce7 Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Wed, 16 Jul 2025 21:29:42 -0600 Subject: [PATCH] `ent list FILTER`: the filter now takes multiple strings This is instead of a single big string with chunks separated by ":". ":" is used in RFC 3339 date-time strings (like "2025-07-16 21:23:44 -06:00"), so it's inconvenient to reserve ":" to be the chunk separator. I'm not super wedded to this new Vec way of doing the filter, but it seems fine and convenient for now. --- src/bin/ent/main.rs | 73 +++++++++++++++++++++++++-------------- src/lib.rs | 84 +++++++++++++++++++++++---------------------- 2 files changed, 91 insertions(+), 66 deletions(-) diff --git a/src/bin/ent/main.rs b/src/bin/ent/main.rs index b211383..7499a15 100644 --- a/src/bin/ent/main.rs +++ b/src/bin/ent/main.rs @@ -24,22 +24,24 @@ struct Args { enum Commands { /// List issues. List { - /// Filter string, describes issues to include in the list. - /// The filter string is composed of chunks separated by ":". - /// Each chunk is of the form "name=condition". The supported - /// names and their matching conditions are: + /// Filter strings, describes issues to include in the list. + /// Each filter string is of the form "name=condition". + /// The supported names and their matching conditions are: /// /// "state": Comma-separated list of states to list. + /// Example: "state=new,backlog". Defaults to + /// "new,backlog,blocked,inprogress". /// - /// "assignee": Comma-separated list of assignees to list. - /// Defaults to all assignees if not set. + /// "assignee": Comma-separated list of assignees to include in + /// the list. The empty string includes issues with no assignee. + /// Example: "assignee=seb," lists issues assigned to "seb" and + /// issues without an assignee. Defaults to include all issues. /// - /// "tag": Comma-separated list of tags to include or exclude - /// (if prefixed with "-"). If omitted, defaults to including - /// all tags and excluding none. - /// - #[arg(default_value_t = String::from("state=New,Backlog,Blocked,InProgress"))] - filter: String, + /// "tag": Comma-separated list of tags to include, or exclude + /// if prefixed with "-". Example: "tag=bug,-docs" shows issues + /// that are tagged "bug" and not tagged "docs". Defaults to + /// including all tags and excluding none. + filter: Vec, }, /// Create a new issue. @@ -93,7 +95,14 @@ fn handle_command( match &args.command { Commands::List { filter } => { let issues = entomologist::database::read_issues_database(issues_database_source)?; - let filter = entomologist::Filter::new_from_str(filter)?; + let filter = { + let mut f = entomologist::Filter::new(); + for filter_str in filter { + f.parse(filter_str)?; + } + f + }; + let mut uuids_by_state = std::collections::HashMap::< entomologist::issue::State, Vec<&entomologist::issue::IssueHandle>, @@ -191,8 +200,10 @@ fn handle_command( } Commands::New { description } => { - let issues_database = - entomologist::database::make_issues_database(issues_database_source, entomologist::database::IssuesDatabaseAccess::ReadWrite)?; + let issues_database = entomologist::database::make_issues_database( + issues_database_source, + entomologist::database::IssuesDatabaseAccess::ReadWrite, + )?; match entomologist::issue::Issue::new(&issues_database.dir, description) { Err(entomologist::issue::IssueError::EmptyDescription) => { println!("no new issue created"); @@ -209,8 +220,10 @@ fn handle_command( } Commands::Edit { uuid } => { - let issues_database = - entomologist::database::make_issues_database(issues_database_source, entomologist::database::IssuesDatabaseAccess::ReadWrite)?; + let issues_database = entomologist::database::make_issues_database( + issues_database_source, + entomologist::database::IssuesDatabaseAccess::ReadWrite, + )?; let mut issues = entomologist::issues::Issues::new_from_dir(&issues_database.dir)?; if let Some(issue) = issues.get_mut_issue(uuid) { match issue.edit_description() { @@ -279,8 +292,10 @@ fn handle_command( new_state, } => match new_state { Some(new_state) => { - let issues_database = - entomologist::database::make_issues_database(issues_database_source, entomologist::database::IssuesDatabaseAccess::ReadWrite)?; + let issues_database = entomologist::database::make_issues_database( + issues_database_source, + entomologist::database::IssuesDatabaseAccess::ReadWrite, + )?; let mut issues = entomologist::issues::Issues::new_from_dir(&issues_database.dir)?; match issues.issues.get_mut(issue_id) { Some(issue) => { @@ -312,8 +327,10 @@ fn handle_command( issue_id, description, } => { - let issues_database = - entomologist::database::make_issues_database(issues_database_source, entomologist::database::IssuesDatabaseAccess::ReadWrite)?; + let issues_database = entomologist::database::make_issues_database( + issues_database_source, + entomologist::database::IssuesDatabaseAccess::ReadWrite, + )?; let mut issues = entomologist::issues::Issues::new_from_dir(&issues_database.dir)?; let Some(issue) = issues.get_mut_issue(issue_id) else { return Err(anyhow::anyhow!("issue {} not found", issue_id)); @@ -338,9 +355,13 @@ fn handle_command( } Commands::Sync { remote } => { - if let entomologist::database::IssuesDatabaseSource::Branch(branch) = issues_database_source { - let issues_database = - entomologist::database::make_issues_database(issues_database_source, entomologist::database::IssuesDatabaseAccess::ReadWrite)?; + if let entomologist::database::IssuesDatabaseSource::Branch(branch) = + issues_database_source + { + let issues_database = entomologist::database::make_issues_database( + issues_database_source, + entomologist::database::IssuesDatabaseAccess::ReadWrite, + )?; entomologist::git::sync(&issues_database.dir, remote, branch)?; println!("synced {:?} with {:?}", branch, remote); } else { @@ -440,7 +461,9 @@ fn main() -> anyhow::Result<()> { // println!("{:?}", args); let issues_database_source = match (&args.issues_dir, &args.issues_branch) { - (Some(dir), None) => entomologist::database::IssuesDatabaseSource::Dir(std::path::Path::new(dir)), + (Some(dir), None) => { + entomologist::database::IssuesDatabaseSource::Dir(std::path::Path::new(dir)) + } (None, Some(branch)) => entomologist::database::IssuesDatabaseSource::Branch(branch), (None, None) => entomologist::database::IssuesDatabaseSource::Branch("entomologist-data"), (Some(_), Some(_)) => { diff --git a/src/lib.rs b/src/lib.rs index 17104ea..dbb19e5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,12 @@ use std::str::FromStr; pub mod comment; +pub mod database; pub mod git; pub mod issue; pub mod issues; -pub mod database; + +use crate::issue::State; #[derive(Debug, thiserror::Error)] pub enum ParseFilterError { @@ -26,9 +28,8 @@ pub struct Filter<'a> { } impl<'a> Filter<'a> { - pub fn new_from_str(filter_str: &'a str) -> Result, ParseFilterError> { - use crate::issue::State; - let mut f = Filter { + pub fn new() -> Filter<'a> { + Self { include_states: std::collections::HashSet::::from([ State::InProgress, State::Blocked, @@ -38,51 +39,52 @@ impl<'a> Filter<'a> { include_assignees: std::collections::HashSet::<&'a str>::new(), include_tags: std::collections::HashSet::<&'a str>::new(), exclude_tags: 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); + pub fn parse(&mut self, filter_str: &'a str) -> Result<(), ParseFilterError> { + let tokens: Vec<&str> = filter_str.split("=").collect(); + if tokens.len() != 2 { + return Err(ParseFilterError::ParseError); + } + + match tokens[0] { + "state" => { + self.include_states.clear(); + for s in tokens[1].split(",") { + self.include_states + .insert(crate::issue::State::from_str(s)?); + } } - 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" => { + self.include_assignees.clear(); + for s in tokens[1].split(",") { + self.include_assignees.insert(s); + } + } + + "tag" => { + self.include_tags.clear(); + self.exclude_tags.clear(); + for s in tokens[1].split(",") { + if s.len() == 0 { + return Err(ParseFilterError::ParseError); + } + if s.chars().nth(0).unwrap() == '-' { + self.exclude_tags.insert(&s[1..]); + } else { + self.include_tags.insert(s); } } + } - "assignee" => { - f.include_assignees.clear(); - for s in tokens[1].split(",") { - f.include_assignees.insert(s); - } - } - - "tag" => { - f.include_tags.clear(); - f.exclude_tags.clear(); - for s in tokens[1].split(",") { - if s.len() == 0 { - return Err(ParseFilterError::ParseError); - } - if s.chars().nth(0).unwrap() == '-' { - f.exclude_tags.insert(&s[1..]); - } else { - f.include_tags.insert(s); - } - } - } - - _ => { - println!("unknown filter chunk '{}'", filter_chunk_str); - return Err(ParseFilterError::ParseError); - } + _ => { + println!("unknown filter string '{}'", filter_str); + return Err(ParseFilterError::ParseError); } } - Ok(f) + Ok(()) } }