diff --git a/src/bin/ent/main.rs b/src/bin/ent/main.rs index af01182..a597fd9 100644 --- a/src/bin/ent/main.rs +++ b/src/bin/ent/main.rs @@ -24,22 +24,31 @@ 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. + /// "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. /// - #[arg(default_value_t = String::from("state=New,Backlog,Blocked,InProgress"))] - filter: String, + /// "done-time": Time range of issue completion, in the form + /// "[START]..[END]". Includes issues that were marked Done + /// between START and END. START and END are both in RFC 3339 + /// format, e.g. "YYYY-MM-DDTHH:MM:SS[+-]HH:MM". If START + /// is omitted, defaults to the beginning of time. If END is + /// omitted, defaults to the end of time. + filter: Vec, }, /// Create a new issue. @@ -85,7 +94,7 @@ enum Commands { tag: Option, }, - // Set the `done_time` of the Issue. + /// Get or set the `done_time` of the Issue. DoneTime { issue_id: String, done_time: Option, @@ -99,7 +108,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>, diff --git a/src/lib.rs b/src/lib.rs index 3ec1ba8..b6245b9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,8 @@ pub mod git; pub mod issue; pub mod issues; +use crate::issue::State; + #[derive(Debug, thiserror::Error)] pub enum ParseFilterError { #[error("Failed to parse filter")] @@ -30,9 +32,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, @@ -44,70 +45,73 @@ impl<'a> Filter<'a> { exclude_tags: std::collections::HashSet::<&'a str>::new(), start_done_time: None, end_done_time: None, - }; + } + } - 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); } + } - "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); - } - } - } - - "done-time" => { - let times: Vec<&str> = tokens[1].split("..").collect(); - if times.len() > 2 { + "tag" => { + self.include_tags.clear(); + self.exclude_tags.clear(); + for s in tokens[1].split(",") { + if s.len() == 0 { return Err(ParseFilterError::ParseError); } - if times[0].len() != 0 { - f.start_done_time = Some( - chrono::DateTime::parse_from_rfc3339(times[0])? - .with_timezone(&chrono::Local), - ); - } - if times[1].len() != 0 { - f.end_done_time = Some( - chrono::DateTime::parse_from_rfc3339(times[1])? - .with_timezone(&chrono::Local), - ); + if s.chars().nth(0).unwrap() == '-' { + self.exclude_tags.insert(&s[1..]); + } else { + self.include_tags.insert(s); } } + } - _ => { - println!("unknown filter chunk '{}'", filter_chunk_str); + "done-time" => { + self.start_done_time = None; + self.end_done_time = None; + let times: Vec<&str> = tokens[1].split("..").collect(); + if times.len() > 2 { return Err(ParseFilterError::ParseError); } + if times[0].len() != 0 { + self.start_done_time = Some( + chrono::DateTime::parse_from_rfc3339(times[0])? + .with_timezone(&chrono::Local), + ); + } + if times[1].len() != 0 { + self.end_done_time = Some( + chrono::DateTime::parse_from_rfc3339(times[1])? + .with_timezone(&chrono::Local), + ); + } + } + + _ => { + println!("unknown filter string '{}'", filter_str); + return Err(ParseFilterError::ParseError); } } - Ok(f) + Ok(()) } }