diff --git a/src/bin/ent/main.rs b/src/bin/ent/main.rs index b211383..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. @@ -84,6 +93,12 @@ enum Commands { #[arg(allow_hyphen_values = true)] tag: Option, }, + + /// Get or set the `done_time` of the Issue. + DoneTime { + issue_id: String, + done_time: Option, + }, } fn handle_command( @@ -93,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>, @@ -123,6 +145,19 @@ fn handle_command( } } + if let Some(issue_done_time) = issue.done_time { + if let Some(start_done_time) = filter.start_done_time { + if start_done_time > issue_done_time { + continue; + } + } + if let Some(end_done_time) = filter.end_done_time { + if end_done_time < issue_done_time { + continue; + } + } + } + // This issue passed all the filters, include it in list. uuids_by_state .entry(issue.state.clone()) @@ -146,7 +181,7 @@ fn handle_command( these_uuids.sort_by(|a_id, b_id| { let a = issues.issues.get(*a_id).unwrap(); let b = issues.issues.get(*b_id).unwrap(); - a.timestamp.cmp(&b.timestamp) + a.creation_time.cmp(&b.creation_time) }); println!("{:?}:", state); for uuid in these_uuids { @@ -191,8 +226,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 +246,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() { @@ -249,7 +288,10 @@ fn handle_command( Some(issue) => { println!("issue {}", issue_id); println!("author: {}", issue.author); - println!("timestamp: {}", issue.timestamp); + println!("creation_time: {}", issue.creation_time); + if let Some(done_time) = &issue.done_time { + println!("done_time: {}", done_time); + } println!("state: {:?}", issue.state); if let Some(dependencies) = &issue.dependencies { println!("dependencies: {:?}", dependencies); @@ -263,7 +305,7 @@ fn handle_command( println!(""); println!("comment: {}", comment.uuid); println!("author: {}", comment.author); - println!("timestamp: {}", comment.timestamp); + println!("creation_time: {}", comment.creation_time); println!(""); println!("{}", comment.description); } @@ -279,8 +321,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 +356,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 +384,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 { @@ -427,6 +477,38 @@ fn handle_command( } } } + + Commands::DoneTime { + issue_id, + done_time, + } => { + let issues = entomologist::database::read_issues_database(issues_database_source)?; + let Some(issue) = issues.issues.get(issue_id) else { + return Err(anyhow::anyhow!("issue {} not found", issue_id)); + }; + match done_time { + Some(done_time) => { + // Add or remove tag. + 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)); + }; + let done_time = chrono::DateTime::parse_from_rfc3339(done_time) + .unwrap() + .with_timezone(&chrono::Local); + issue.set_done_time(done_time)?; + } + None => match &issue.done_time { + Some(done_time) => println!("done_time: {}", done_time), + None => println!("None"), + }, + }; + } } Ok(()) @@ -440,7 +522,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/comment.rs b/src/comment.rs index c8e26c9..216b34f 100644 --- a/src/comment.rs +++ b/src/comment.rs @@ -4,7 +4,7 @@ use std::io::{IsTerminal, Write}; pub struct Comment { pub uuid: String, pub author: String, - pub timestamp: chrono::DateTime, + pub creation_time: chrono::DateTime, pub description: String, /// This is the directory that the comment lives in. Only used @@ -53,13 +53,13 @@ impl Comment { } let author = crate::git::git_log_oldest_author(comment_dir)?; - let timestamp = crate::git::git_log_oldest_timestamp(comment_dir)?; + let creation_time = crate::git::git_log_oldest_timestamp(comment_dir)?; let dir = std::path::PathBuf::from(comment_dir); Ok(Self { uuid: String::from(dir.file_name().unwrap().to_string_lossy()), author, - timestamp, + creation_time, description: description.unwrap(), dir: std::path::PathBuf::from(comment_dir), }) @@ -84,7 +84,7 @@ impl Comment { let mut comment = crate::comment::Comment { uuid, author: String::from(""), // this will be updated from git when we re-read this comment - timestamp: chrono::Local::now(), + creation_time: chrono::Local::now(), description: String::from(""), // this will be set immediately below dir: dir.clone(), }; @@ -204,7 +204,7 @@ mod tests { let expected = Comment { uuid: String::from("9055dac36045fe36545bed7ae7b49347"), author: String::from("Sebastian Kuzminsky "), - timestamp: chrono::DateTime::parse_from_rfc3339("2025-07-07T15:26:26-06:00") + creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-07T15:26:26-06:00") .unwrap() .with_timezone(&chrono::Local), description: String::from("This is a comment on issue dd79c8cfb8beeacd0460429944b4ecbe95a31561\n\nIt has multiple lines\n"), diff --git a/src/issue.rs b/src/issue.rs index 0de413c..bc3d959 100644 --- a/src/issue.rs +++ b/src/issue.rs @@ -22,7 +22,8 @@ pub type IssueHandle = String; pub struct Issue { pub id: String, pub author: String, - pub timestamp: chrono::DateTime, + pub creation_time: chrono::DateTime, + pub done_time: Option>, pub tags: Vec, pub state: State, pub dependencies: Option>, @@ -43,6 +44,8 @@ pub enum IssueError { EnvVarError(#[from] std::env::VarError), #[error(transparent)] CommentError(#[from] crate::comment::CommentError), + #[error(transparent)] + ChronoParseError(#[from] chrono::format::ParseError), #[error("Failed to parse issue")] IssueParseError, #[error("Failed to parse state")] @@ -106,6 +109,7 @@ impl Issue { let mut comments = Vec::::new(); let mut assignee: Option = None; let mut tags = Vec::::new(); + let mut done_time: Option> = None; for direntry in dir.read_dir()? { if let Ok(direntry) = direntry { @@ -119,6 +123,11 @@ impl Issue { assignee = Some(String::from( std::fs::read_to_string(direntry.path())?.trim(), )); + } else if file_name == "done_time" { + let raw_done_time = chrono::DateTime::<_>::parse_from_rfc3339( + std::fs::read_to_string(direntry.path())?.trim(), + )?; + done_time = Some(raw_done_time.into()); } else if file_name == "dependencies" { let dep_strings = std::fs::read_to_string(direntry.path())?; let deps: Vec = dep_strings @@ -159,12 +168,13 @@ impl Issue { }; let author = crate::git::git_log_oldest_author(dir)?; - let timestamp = crate::git::git_log_oldest_timestamp(dir)?; + let creation_time = crate::git::git_log_oldest_timestamp(dir)?; Ok(Self { id, author, - timestamp, + creation_time, + done_time, tags, state: state, dependencies, @@ -185,7 +195,7 @@ impl Issue { comments.push(comment); } } - comments.sort_by(|a, b| a.timestamp.cmp(&b.timestamp)); + comments.sort_by(|a, b| a.creation_time.cmp(&b.creation_time)); Ok(()) } @@ -220,7 +230,8 @@ impl Issue { let mut issue = Self { id: String::from(&issue_id), author: String::from(""), - timestamp: chrono::Local::now(), + creation_time: chrono::Local::now(), + done_time: None, tags: Vec::::new(), state: State::New, dependencies: None, @@ -243,8 +254,7 @@ impl Issue { None => issue.edit_description_file()?, }; - crate::git::add(&issue_dir)?; - crate::git::commit(&issue_dir, &format!("create new issue {}", issue_id))?; + issue.commit(&format!("create new issue {}", issue_id))?; Ok(issue) } @@ -253,21 +263,15 @@ impl Issue { pub fn edit_description(&mut self) -> Result<(), IssueError> { self.edit_description_file()?; let description_filename = self.description_filename(); - crate::git::add(&description_filename)?; - if crate::git::worktree_is_dirty(&self.dir.to_string_lossy())? { - crate::git::commit( - &description_filename.parent().unwrap(), - &format!( - "edit description of issue {}", - description_filename - .parent() - .unwrap() - .file_name() - .unwrap() - .to_string_lossy() - ), - )?; - } + self.commit(&format!( + "edit description of issue {}", + description_filename + .parent() + .unwrap() + .file_name() + .unwrap() + .to_string_lossy(), + ))?; Ok(()) } @@ -279,24 +283,22 @@ impl Issue { } } - /// Change the State of the Issue. + /// Change the State of the Issue. If the new state is `Done`, + /// set the Issue `done_time`. Commits. pub fn set_state(&mut self, new_state: State) -> Result<(), IssueError> { let old_state = self.state.clone(); let mut state_filename = std::path::PathBuf::from(&self.dir); state_filename.push("state"); let mut state_file = std::fs::File::create(&state_filename)?; write!(state_file, "{}", new_state)?; - crate::git::add(&state_filename)?; - if crate::git::worktree_is_dirty(&self.dir.to_string_lossy())? { - crate::git::commit( - &self.dir, - &format!( - "change state of issue {}, {} -> {}", - self.dir.file_name().unwrap().to_string_lossy(), - old_state, - new_state, - ), - )?; + self.commit(&format!( + "change state of issue {}, {} -> {}", + self.dir.file_name().unwrap().to_string_lossy(), + old_state, + new_state, + ))?; + if new_state == State::Done { + self.set_done_time(chrono::Local::now())?; } Ok(()) } @@ -309,6 +311,24 @@ impl Issue { Ok(()) } + /// Set the `done_time` of the Issue. Commits. + pub fn set_done_time( + &mut self, + done_time: chrono::DateTime, + ) -> Result<(), IssueError> { + let mut done_time_filename = std::path::PathBuf::from(&self.dir); + done_time_filename.push("done_time"); + let mut done_time_file = std::fs::File::create(&done_time_filename)?; + write!(done_time_file, "{}", done_time.to_rfc3339())?; + self.done_time = Some(done_time.clone()); + self.commit(&format!( + "set done-time of issue {} to {}", + self.dir.file_name().unwrap().to_string_lossy(), + done_time, + ))?; + Ok(()) + } + /// Set the Assignee of an Issue. pub fn set_assignee(&mut self, new_assignee: &str) -> Result<(), IssueError> { let old_assignee = match &self.assignee { @@ -319,18 +339,12 @@ impl Issue { assignee_filename.push("assignee"); let mut assignee_file = std::fs::File::create(&assignee_filename)?; write!(assignee_file, "{}", new_assignee)?; - crate::git::add(&assignee_filename)?; - if crate::git::worktree_is_dirty(&self.dir.to_string_lossy())? { - crate::git::commit( - &self.dir, - &format!( - "change assignee of issue {}, {} -> {}", - self.dir.file_name().unwrap().to_string_lossy(), - old_assignee, - new_assignee, - ), - )?; - } + self.commit(&format!( + "change assignee of issue {}, {} -> {}", + self.dir.file_name().unwrap().to_string_lossy(), + old_assignee, + new_assignee, + ))?; Ok(()) } @@ -444,7 +458,15 @@ impl Issue { for tag in &self.tags { writeln!(tags_file, "{}", tag)?; } - crate::git::add(&tags_filename)?; + self.commit(commit_message)?; + Ok(()) + } + + fn commit(&self, commit_message: &str) -> Result<(), IssueError> { + crate::git::add(&self.dir)?; + if !crate::git::worktree_is_dirty(&self.dir.to_string_lossy())? { + return Ok(()); + } crate::git::commit(&self.dir, commit_message)?; Ok(()) } @@ -461,9 +483,10 @@ mod tests { let expected = Issue { id: String::from("3943fc5c173fdf41c0a22251593cd476d96e6c9f"), author: String::from("Sebastian Kuzminsky "), - timestamp: chrono::DateTime::parse_from_rfc3339("2025-07-03T12:14:26-06:00") + creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-03T12:14:26-06:00") .unwrap() .with_timezone(&chrono::Local), + done_time: None, tags: Vec::::from([ String::from("tag1"), String::from("TAG2"), @@ -488,9 +511,10 @@ mod tests { let expected = Issue { id: String::from("7792b063eef6d33e7da5dc1856750c149ba678c6"), author: String::from("Sebastian Kuzminsky "), - timestamp: chrono::DateTime::parse_from_rfc3339("2025-07-03T12:14:26-06:00") + creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-03T12:14:26-06:00") .unwrap() .with_timezone(&chrono::Local), + done_time: None, tags: Vec::::new(), state: State::InProgress, dependencies: None, diff --git a/src/issues.rs b/src/issues.rs index 42e964d..709a79d 100644 --- a/src/issues.rs +++ b/src/issues.rs @@ -90,9 +90,10 @@ mod tests { expected.add_issue(crate::issue::Issue { id: uuid, author: String::from("Sebastian Kuzminsky "), - timestamp: chrono::DateTime::parse_from_rfc3339("2025-07-03T12:14:26-06:00") + creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-03T12:14:26-06:00") .unwrap() .with_timezone(&chrono::Local), + done_time: None, tags: Vec::::new(), state: crate::issue::State::InProgress, dependencies: None, @@ -109,9 +110,10 @@ mod tests { crate::issue::Issue { id: uuid, author: String::from("Sebastian Kuzminsky "), - timestamp: chrono::DateTime::parse_from_rfc3339("2025-07-03T12:14:26-06:00") + creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-03T12:14:26-06:00") .unwrap() .with_timezone(&chrono::Local), + done_time: None, tags: Vec::::from([ String::from("tag1"), String::from("TAG2"), @@ -141,9 +143,14 @@ mod tests { expected.add_issue(crate::issue::Issue { id: uuid, author: String::from("Sebastian Kuzminsky "), - timestamp: chrono::DateTime::parse_from_rfc3339("2025-07-03T11:59:44-06:00") + creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-03T11:59:44-06:00") .unwrap() .with_timezone(&chrono::Local), + done_time: Some( + chrono::DateTime::parse_from_rfc3339("2025-07-15T15:15:15-06:00") + .unwrap() + .with_timezone(&chrono::Local), + ), tags: Vec::::new(), state: crate::issue::State::Done, dependencies: None, @@ -165,7 +172,7 @@ mod tests { crate::comment::Comment { uuid: comment_uuid, author: String::from("Sebastian Kuzminsky "), - timestamp: chrono::DateTime::parse_from_rfc3339("2025-07-07T15:26:26-06:00").unwrap().with_timezone(&chrono::Local), + creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-07T15:26:26-06:00").unwrap().with_timezone(&chrono::Local), description: String::from("This is a comment on issue dd79c8cfb8beeacd0460429944b4ecbe95a31561\n\nIt has multiple lines\n"), dir: std::path::PathBuf::from(comment_dir), } @@ -174,9 +181,10 @@ mod tests { crate::issue::Issue { id: uuid, author: String::from("Sebastian Kuzminsky "), - timestamp: chrono::DateTime::parse_from_rfc3339("2025-07-03T11:59:44-06:00") + creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-03T11:59:44-06:00") .unwrap() .with_timezone(&chrono::Local), + done_time: None, tags: Vec::::new(), state: crate::issue::State::WontDo, dependencies: None, @@ -202,9 +210,10 @@ mod tests { expected.add_issue(crate::issue::Issue { id: uuid, author: String::from("sigil-03 "), - timestamp: chrono::DateTime::parse_from_rfc3339("2025-07-05T13:55:49-06:00") + creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-05T13:55:49-06:00") .unwrap() .with_timezone(&chrono::Local), + done_time: None, tags: Vec::::new(), state: crate::issue::State::Done, dependencies: None, @@ -221,9 +230,10 @@ mod tests { crate::issue::Issue { id: uuid, author: String::from("sigil-03 "), - timestamp: chrono::DateTime::parse_from_rfc3339("2025-07-05T13:55:49-06:00") + creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-05T13:55:49-06:00") .unwrap() .with_timezone(&chrono::Local), + done_time: None, tags: Vec::::new(), state: crate::issue::State::WontDo, dependencies: None, @@ -241,9 +251,10 @@ mod tests { crate::issue::Issue { id: uuid, author: String::from("sigil-03 "), - timestamp: chrono::DateTime::parse_from_rfc3339("2025-07-05T13:55:49-06:00") + creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-05T13:55:49-06:00") .unwrap() .with_timezone(&chrono::Local), + done_time: None, tags: Vec::::new(), state: crate::issue::State::WontDo, dependencies: Some(vec![ diff --git a/src/lib.rs b/src/lib.rs index 17104ea..b6245b9 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 { @@ -12,6 +14,8 @@ pub enum ParseFilterError { ParseError, #[error(transparent)] IssueParseError(#[from] crate::issue::IssueError), + #[error(transparent)] + ChronoParseError(#[from] chrono::format::ParseError), } // FIXME: It's easy to imagine a full dsl for filtering issues, for now @@ -23,12 +27,13 @@ pub struct Filter<'a> { pub include_assignees: std::collections::HashSet<&'a str>, pub include_tags: std::collections::HashSet<&'a str>, pub exclude_tags: std::collections::HashSet<&'a str>, + pub start_done_time: Option>, + pub end_done_time: Option>, } 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 +43,75 @@ 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(), - }; + 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); + } + } + + "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); + "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(()) } } diff --git a/test/0001/3fa5bfd93317ad25772680071d5ac3259cd2384f/done_time b/test/0001/3fa5bfd93317ad25772680071d5ac3259cd2384f/done_time new file mode 100644 index 0000000..d455c4d --- /dev/null +++ b/test/0001/3fa5bfd93317ad25772680071d5ac3259cd2384f/done_time @@ -0,0 +1 @@ +2025-07-15T15:15:15-06:00