From 733100fefb4f250b36d84716f72572e098aad711 Mon Sep 17 00:00:00 2001 From: sigil-03 Date: Tue, 15 Jul 2025 10:53:52 -0600 Subject: [PATCH 01/36] add the ID field back into the Issue struct --- src/issue.rs | 23 ++++++++++- src/issues.rs | 110 ++++++++++++++++++++++---------------------------- 2 files changed, 70 insertions(+), 63 deletions(-) diff --git a/src/issue.rs b/src/issue.rs index 1558578..0de413c 100644 --- a/src/issue.rs +++ b/src/issue.rs @@ -20,6 +20,7 @@ pub type IssueHandle = String; #[derive(Debug, PartialEq)] pub struct Issue { + pub id: String, pub author: String, pub timestamp: chrono::DateTime, pub tags: Vec, @@ -56,6 +57,8 @@ pub enum IssueError { TagNotFound(String), #[error("stdin/stdout is not a terminal")] StdioIsNotTerminal, + #[error("Failed to parse issue ID")] + IdError, } impl FromStr for State { @@ -145,10 +148,21 @@ impl Issue { return Err(IssueError::IssueParseError); } + // parse the issue ID from the directory name + let id = if let Some(parsed_id) = match dir.file_name() { + Some(name) => name.to_str(), + None => Err(IssueError::IdError)?, + } { + String::from(parsed_id) + } else { + Err(IssueError::IdError)? + }; + let author = crate::git::git_log_oldest_author(dir)?; let timestamp = crate::git::git_log_oldest_timestamp(dir)?; Ok(Self { + id, author, timestamp, tags, @@ -204,6 +218,7 @@ impl Issue { std::fs::create_dir(&issue_dir)?; let mut issue = Self { + id: String::from(&issue_id), author: String::from(""), timestamp: chrono::Local::now(), tags: Vec::::new(), @@ -444,6 +459,7 @@ mod tests { let issue_dir = std::path::Path::new("test/0000/3943fc5c173fdf41c0a22251593cd476d96e6c9f/"); let issue = Issue::new_from_dir(issue_dir).unwrap(); 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") .unwrap() @@ -451,12 +467,14 @@ mod tests { tags: Vec::::from([ String::from("tag1"), String::from("TAG2"), - String::from("i-am-also-a-tag") + String::from("i-am-also-a-tag"), ]), state: State::New, dependencies: None, assignee: None, - description: String::from("this is the title of my issue\n\nThis is the description of my issue.\nIt is multiple lines.\n* Arbitrary contents\n* But let's use markdown by convention\n"), + description: String::from( + "this is the title of my issue\n\nThis is the description of my issue.\nIt is multiple lines.\n* Arbitrary contents\n* But let's use markdown by convention\n", + ), comments: Vec::::new(), dir: std::path::PathBuf::from(issue_dir), }; @@ -468,6 +486,7 @@ mod tests { let issue_dir = std::path::Path::new("test/0000/7792b063eef6d33e7da5dc1856750c149ba678c6/"); let issue = Issue::new_from_dir(issue_dir).unwrap(); 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") .unwrap() diff --git a/src/issues.rs b/src/issues.rs index 5c314ac..42e964d 100644 --- a/src/issues.rs +++ b/src/issues.rs @@ -31,8 +31,8 @@ impl Issues { } } - pub fn add_issue(&mut self, uuid: String, issue: crate::issue::Issue) { - self.issues.insert(uuid, issue); + pub fn add_issue(&mut self, issue: crate::issue::Issue) { + self.issues.insert(issue.id.clone(), issue); } pub fn get_issue(&self, issue_id: &str) -> Option<&crate::issue::Issue> { @@ -56,14 +56,8 @@ impl Issues { for direntry in dir.read_dir()? { if let Ok(direntry) = direntry { if direntry.metadata()?.is_dir() { - let uuid = match direntry.file_name().into_string() { - Ok(uuid) => uuid, - Err(orig_string) => { - return Err(ReadIssuesError::FilenameError(orig_string)) - } - }; let issue = crate::issue::Issue::new_from_dir(direntry.path().as_path())?; - issues.add_issue(uuid, issue); + issues.add_issue(issue); } else if direntry.file_name() == "config.toml" { issues.parse_config(direntry.path().as_path())?; } else { @@ -93,29 +87,27 @@ mod tests { let uuid = String::from("7792b063eef6d33e7da5dc1856750c149ba678c6"); let mut dir = std::path::PathBuf::from(issues_dir); dir.push(&uuid); - expected.add_issue( - uuid, - crate::issue::Issue { - author: String::from("Sebastian Kuzminsky "), - timestamp: chrono::DateTime::parse_from_rfc3339("2025-07-03T12:14:26-06:00") - .unwrap() - .with_timezone(&chrono::Local), - tags: Vec::::new(), - state: crate::issue::State::InProgress, - dependencies: None, - assignee: Some(String::from("beep boop")), - description: String::from("minimal"), - comments: Vec::::new(), - dir, - }, - ); + 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") + .unwrap() + .with_timezone(&chrono::Local), + tags: Vec::::new(), + state: crate::issue::State::InProgress, + dependencies: None, + assignee: Some(String::from("beep boop")), + description: String::from("minimal"), + comments: Vec::::new(), + dir, + }); let uuid = String::from("3943fc5c173fdf41c0a22251593cd476d96e6c9f"); let mut dir = std::path::PathBuf::from(issues_dir); dir.push(&uuid); expected.add_issue( - uuid, crate::issue::Issue { + id: uuid, author: String::from("Sebastian Kuzminsky "), timestamp: chrono::DateTime::parse_from_rfc3339("2025-07-03T12:14:26-06:00") .unwrap() @@ -146,22 +138,20 @@ mod tests { let uuid = String::from("3fa5bfd93317ad25772680071d5ac3259cd2384f"); let mut dir = std::path::PathBuf::from(issues_dir); dir.push(&uuid); - expected.add_issue( - uuid, - crate::issue::Issue { - author: String::from("Sebastian Kuzminsky "), - timestamp: chrono::DateTime::parse_from_rfc3339("2025-07-03T11:59:44-06:00") - .unwrap() - .with_timezone(&chrono::Local), - tags: Vec::::new(), - state: crate::issue::State::Done, - dependencies: None, - assignee: None, - description: String::from("oh yeah we got titles"), - comments: Vec::::new(), - dir, - }, - ); + 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") + .unwrap() + .with_timezone(&chrono::Local), + tags: Vec::::new(), + state: crate::issue::State::Done, + dependencies: None, + assignee: None, + description: String::from("oh yeah we got titles"), + comments: Vec::::new(), + dir, + }); let uuid = String::from("dd79c8cfb8beeacd0460429944b4ecbe95a31561"); let mut dir = std::path::PathBuf::from(issues_dir); @@ -181,8 +171,8 @@ mod tests { } ); expected.add_issue( - uuid, crate::issue::Issue { + id: uuid, author: String::from("Sebastian Kuzminsky "), timestamp: chrono::DateTime::parse_from_rfc3339("2025-07-03T11:59:44-06:00") .unwrap() @@ -209,29 +199,27 @@ mod tests { let uuid = String::from("3fa5bfd93317ad25772680071d5ac3259cd2384f"); let mut dir = std::path::PathBuf::from(issues_dir); dir.push(&uuid); - expected.add_issue( - uuid, - crate::issue::Issue { - author: String::from("sigil-03 "), - timestamp: chrono::DateTime::parse_from_rfc3339("2025-07-05T13:55:49-06:00") - .unwrap() - .with_timezone(&chrono::Local), - tags: Vec::::new(), - state: crate::issue::State::Done, - dependencies: None, - assignee: None, - description: String::from("oh yeah we got titles\n"), - comments: Vec::::new(), - dir, - }, - ); + 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") + .unwrap() + .with_timezone(&chrono::Local), + tags: Vec::::new(), + state: crate::issue::State::Done, + dependencies: None, + assignee: None, + description: String::from("oh yeah we got titles\n"), + comments: Vec::::new(), + dir, + }); let uuid = String::from("dd79c8cfb8beeacd0460429944b4ecbe95a31561"); let mut dir = std::path::PathBuf::from(issues_dir); dir.push(&uuid); expected.add_issue( - uuid, crate::issue::Issue { + id: uuid, author: String::from("sigil-03 "), timestamp: chrono::DateTime::parse_from_rfc3339("2025-07-05T13:55:49-06:00") .unwrap() @@ -250,8 +238,8 @@ mod tests { let mut dir = std::path::PathBuf::from(issues_dir); dir.push(&uuid); expected.add_issue( - uuid, crate::issue::Issue { + id: uuid, author: String::from("sigil-03 "), timestamp: chrono::DateTime::parse_from_rfc3339("2025-07-05T13:55:49-06:00") .unwrap() From 5e5508a2ee3a0e380bdd2f4c1897826d439811f1 Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Tue, 15 Jul 2025 16:27:48 -0600 Subject: [PATCH 02/36] Issue: make a helper function to commit an Issue This improves code reuse and streamlines the code a bit. --- src/issue.rs | 73 ++++++++++++++++++++++------------------------------ 1 file changed, 31 insertions(+), 42 deletions(-) diff --git a/src/issue.rs b/src/issue.rs index 0de413c..23bb59d 100644 --- a/src/issue.rs +++ b/src/issue.rs @@ -243,8 +243,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 +252,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(()) } @@ -286,18 +279,12 @@ impl Issue { 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, + ))?; Ok(()) } @@ -319,18 +306,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 +425,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(()) } From 20c17f281b656ec135af0526783ee1acd2ce5ce7 Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Wed, 16 Jul 2025 21:29:42 -0600 Subject: [PATCH 03/36] `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(()) } } From 3df76b89df28c6eb8081732a314eaa7b5b9a8dfa Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Tue, 15 Jul 2025 15:10:34 -0600 Subject: [PATCH 04/36] rename Issue and Comment `timestamp` to `creation_time` This is to make room for a second timestamp that records when the issue was marked Done. --- src/bin/ent/main.rs | 6 +++--- src/comment.rs | 10 +++++----- src/issue.rs | 14 +++++++------- src/issues.rs | 16 ++++++++-------- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/bin/ent/main.rs b/src/bin/ent/main.rs index 7499a15..494b1ba 100644 --- a/src/bin/ent/main.rs +++ b/src/bin/ent/main.rs @@ -155,7 +155,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 { @@ -262,7 +262,7 @@ fn handle_command( Some(issue) => { println!("issue {}", issue_id); println!("author: {}", issue.author); - println!("timestamp: {}", issue.timestamp); + println!("creation_time: {}", issue.creation_time); println!("state: {:?}", issue.state); if let Some(dependencies) = &issue.dependencies { println!("dependencies: {:?}", dependencies); @@ -276,7 +276,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); } 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 23bb59d..72c1487 100644 --- a/src/issue.rs +++ b/src/issue.rs @@ -22,7 +22,7 @@ pub type IssueHandle = String; pub struct Issue { pub id: String, pub author: String, - pub timestamp: chrono::DateTime, + pub creation_time: chrono::DateTime, pub tags: Vec, pub state: State, pub dependencies: Option>, @@ -159,12 +159,12 @@ 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, tags, state: state, dependencies, @@ -185,7 +185,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 +220,7 @@ impl Issue { let mut issue = Self { id: String::from(&issue_id), author: String::from(""), - timestamp: chrono::Local::now(), + creation_time: chrono::Local::now(), tags: Vec::::new(), state: State::New, dependencies: None, @@ -450,7 +450,7 @@ 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), tags: Vec::::from([ @@ -477,7 +477,7 @@ 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), tags: Vec::::new(), diff --git a/src/issues.rs b/src/issues.rs index 42e964d..7f2e63e 100644 --- a/src/issues.rs +++ b/src/issues.rs @@ -90,7 +90,7 @@ 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), tags: Vec::::new(), @@ -109,7 +109,7 @@ 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), tags: Vec::::from([ @@ -141,7 +141,7 @@ 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), tags: Vec::::new(), @@ -165,7 +165,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,7 +174,7 @@ 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), tags: Vec::::new(), @@ -202,7 +202,7 @@ 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), tags: Vec::::new(), @@ -221,7 +221,7 @@ 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), tags: Vec::::new(), @@ -241,7 +241,7 @@ 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), tags: Vec::::new(), From 3b33ed41f5463309a4f9001fae9f6d278e61aabe Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Tue, 15 Jul 2025 15:37:23 -0600 Subject: [PATCH 05/36] Issue: add `done_time` field This records the DateTime that the issue moved to the Done state (if any). --- src/issue.rs | 13 +++++++++++++ src/issues.rs | 11 +++++++++++ .../done_time | 1 + 3 files changed, 25 insertions(+) create mode 100644 test/0001/3fa5bfd93317ad25772680071d5ac3259cd2384f/done_time diff --git a/src/issue.rs b/src/issue.rs index 72c1487..927eb7f 100644 --- a/src/issue.rs +++ b/src/issue.rs @@ -23,6 +23,7 @@ pub struct Issue { pub id: String, pub author: String, 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 @@ -165,6 +174,7 @@ impl Issue { id, author, creation_time, + done_time, tags, state: state, dependencies, @@ -221,6 +231,7 @@ impl Issue { id: String::from(&issue_id), author: String::from(""), creation_time: chrono::Local::now(), + done_time: None, tags: Vec::::new(), state: State::New, dependencies: None, @@ -453,6 +464,7 @@ mod tests { 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"), @@ -480,6 +492,7 @@ mod tests { 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 7f2e63e..709a79d 100644 --- a/src/issues.rs +++ b/src/issues.rs @@ -93,6 +93,7 @@ mod tests { 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, @@ -112,6 +113,7 @@ mod tests { 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"), @@ -144,6 +146,11 @@ mod tests { 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, @@ -177,6 +184,7 @@ mod tests { 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, @@ -205,6 +213,7 @@ mod tests { 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, @@ -224,6 +233,7 @@ mod tests { 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, @@ -244,6 +254,7 @@ mod tests { 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/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 From bc2b1bd3c15aa6c8a511904bb1effcdcafc4f961 Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Tue, 15 Jul 2025 16:28:14 -0600 Subject: [PATCH 06/36] add API and CLI to get & set done-time of an issue --- src/bin/ent/main.rs | 41 +++++++++++++++++++++++++++++++++++++++++ src/issue.rs | 24 +++++++++++++++++++++++- 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/bin/ent/main.rs b/src/bin/ent/main.rs index 494b1ba..ac55045 100644 --- a/src/bin/ent/main.rs +++ b/src/bin/ent/main.rs @@ -86,6 +86,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( @@ -263,6 +269,9 @@ fn handle_command( println!("issue {}", issue_id); println!("author: {}", issue.author); 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); @@ -448,6 +457,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(()) diff --git a/src/issue.rs b/src/issue.rs index 927eb7f..bc3d959 100644 --- a/src/issue.rs +++ b/src/issue.rs @@ -283,7 +283,8 @@ 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); @@ -296,6 +297,9 @@ impl Issue { old_state, new_state, ))?; + if new_state == State::Done { + self.set_done_time(chrono::Local::now())?; + } Ok(()) } @@ -307,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 { From a3077ca31321757772a6d6d1f283dca0c4cdb40e Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Wed, 16 Jul 2025 21:28:15 -0600 Subject: [PATCH 07/36] `ent list FILTER`: add filter "done-time=[START]..[END]" --- src/bin/ent/main.rs | 20 ++++++++++++++++++++ src/lib.rs | 27 +++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/src/bin/ent/main.rs b/src/bin/ent/main.rs index ac55045..a597fd9 100644 --- a/src/bin/ent/main.rs +++ b/src/bin/ent/main.rs @@ -41,6 +41,13 @@ enum Commands { /// 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. + /// + /// "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, }, @@ -138,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()) diff --git a/src/lib.rs b/src/lib.rs index dbb19e5..b6245b9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,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 @@ -25,6 +27,8 @@ 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> { @@ -39,6 +43,8 @@ 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, } } @@ -79,6 +85,27 @@ impl<'a> Filter<'a> { } } + "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); From 3e0ab7092e5afa4367c5349eb76a118b9ae3962e Mon Sep 17 00:00:00 2001 From: sigil-03 Date: Tue, 15 Jul 2025 10:56:09 -0600 Subject: [PATCH 08/36] update CLI to print the issue ID when a new issue is created --- src/bin/ent/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/bin/ent/main.rs b/src/bin/ent/main.rs index b211383..fff83e1 100644 --- a/src/bin/ent/main.rs +++ b/src/bin/ent/main.rs @@ -203,6 +203,7 @@ fn handle_command( } Ok(issue) => { println!("created new issue '{}'", issue.title()); + println!("ID: {}", issue.id); return Ok(()); } } From 8319a4f118d5735c33e377b71d7cb689d2cdffb8 Mon Sep 17 00:00:00 2001 From: sigil-03 Date: Fri, 18 Jul 2025 16:20:17 -0600 Subject: [PATCH 09/36] add dependency API / fix dependency representation / dependency management via CLI --- src/bin/ent/main.rs | 45 ++++++++++++++++++++++++++- src/issue.rs | 74 +++++++++++++++++++++++++++++++++++++++------ 2 files changed, 109 insertions(+), 10 deletions(-) diff --git a/src/bin/ent/main.rs b/src/bin/ent/main.rs index 1698954..4118ed3 100644 --- a/src/bin/ent/main.rs +++ b/src/bin/ent/main.rs @@ -99,6 +99,12 @@ enum Commands { issue_id: String, done_time: Option, }, + + /// get or add a dependency to the issue + Depend { + issue_id: String, + dependency_id: Option, + }, } fn handle_command( @@ -510,6 +516,43 @@ fn handle_command( }, }; } + + Commands::Depend { + issue_id, + dependency_id, + } => match dependency_id { + Some(dep_id) => { + let ent_db = entomologist::database::make_issues_database( + issues_database_source, + entomologist::database::IssuesDatabaseAccess::ReadWrite, + )?; + let mut issues = entomologist::issues::Issues::new_from_dir(&ent_db.dir)?; + if issues.issues.contains_key(dep_id) { + if let Some(issue) = issues.issues.get_mut(issue_id) { + issue.add_dependency(dep_id.clone())?; + } else { + Err(anyhow::anyhow!("issue {} not found", issue_id))?; + }; + } else { + Err(anyhow::anyhow!("dependency {} not found", dep_id))?; + }; + } + None => { + let ent_db = entomologist::database::read_issues_database(issues_database_source)?; + + let Some(issue) = ent_db.issues.get(issue_id) else { + Err(anyhow::anyhow!("issue {} not found", issue_id))? + }; + println!("DEPENDENCIES:"); + if let Some(list) = &issue.dependencies { + for dependency in list { + println!("{}", dependency); + } + } else { + println!("NONE"); + } + } + }, } Ok(()) @@ -531,7 +574,7 @@ fn main() -> anyhow::Result<()> { (Some(_), Some(_)) => { return Err(anyhow::anyhow!( "don't specify both `--issues-dir` and `--issues-branch`" - )) + )); } }; diff --git a/src/issue.rs b/src/issue.rs index bc3d959..363e2b7 100644 --- a/src/issue.rs +++ b/src/issue.rs @@ -62,6 +62,12 @@ pub enum IssueError { StdioIsNotTerminal, #[error("Failed to parse issue ID")] IdError, + #[error("Dependency not found")] + DepNotFound, + #[error("Dependency already exists")] + DepExists, + #[error("Self-dependency not allowed")] + DepSelf, } impl FromStr for State { @@ -128,15 +134,8 @@ impl Issue { 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 - .lines() - .map(|dep| IssueHandle::from(dep)) - .collect(); - if deps.len() > 0 { - dependencies = Some(deps); - } + } else if file_name == "dependencies" && direntry.metadata()?.is_dir() { + dependencies = Self::read_dependencies(&direntry.path())?; } else if file_name == "tags" { let contents = std::fs::read_to_string(direntry.path())?; tags = contents @@ -199,6 +198,23 @@ impl Issue { Ok(()) } + fn read_dependencies(dir: &std::path::Path) -> Result>, IssueError> { + let mut dependencies: Option> = None; + for direntry in dir.read_dir()? { + if let Ok(direntry) = direntry { + match &mut dependencies { + Some(deps) => { + deps.push(direntry.file_name().into_string().unwrap()); + } + None => { + dependencies = Some(vec![direntry.file_name().into_string().unwrap()]); + } + } + } + } + Ok(dependencies) + } + /// Add a new Comment to the Issue. Commits. pub fn add_comment( &mut self, @@ -392,6 +408,46 @@ impl Issue { } return false; } + + pub fn add_dependency(&mut self, dep: IssueHandle) -> Result<(), IssueError> { + if self.id == dep { + Err(IssueError::DepSelf)?; + } + match &mut self.dependencies { + Some(v) => v.push(dep.clone()), + None => self.dependencies = Some(vec![dep.clone()]), + } + let mut dir = std::path::PathBuf::from(&self.dir); + dir.push("dependencies"); + if !dir.exists() { + std::fs::create_dir(&dir)?; + } + + dir.push(dep.clone()); + + if !dir.exists() { + std::fs::File::create(&dir)?; + self.commit(&format!("add dep {} to issue {}", dep, self.id))?; + } else { + Err(IssueError::DepExists)?; + } + Ok(()) + } + + pub fn remove_dependency(&mut self, dep: IssueHandle) -> Result<(), IssueError> { + match &mut self.dependencies { + Some(v) => { + if let Some(i) = v.iter().position(|d| d == &dep) { + v.remove(i); + } else { + Err(IssueError::DepNotFound)?; + } + } + None => Err(IssueError::DepNotFound)?, + } + self.commit(&format!("remove dep {} from issue {}", dep, self.id))?; + Ok(()) + } } // This is the internal/private API of Issue. From 2ba13ebaeb27189fddfb44f897242cd577d9d33e Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Sat, 19 Jul 2025 09:53:36 -0600 Subject: [PATCH 10/36] Issue: get rid of all unwraps Make and return errors instead. --- src/issue.rs | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/src/issue.rs b/src/issue.rs index bc3d959..e129dff 100644 --- a/src/issue.rs +++ b/src/issue.rs @@ -153,9 +153,9 @@ impl Issue { } } - if description == None { + let Some(description) = description else { return Err(IssueError::IssueParseError); - } + }; // parse the issue ID from the directory name let id = if let Some(parsed_id) = match dir.file_name() { @@ -179,7 +179,7 @@ impl Issue { state: state, dependencies, assignee, - description: description.unwrap(), + description, comments, dir: std::path::PathBuf::from(dir), }) @@ -267,9 +267,9 @@ impl Issue { "edit description of issue {}", description_filename .parent() - .unwrap() + .ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))? .file_name() - .unwrap() + .ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))? .to_string_lossy(), ))?; Ok(()) @@ -293,7 +293,10 @@ impl Issue { write!(state_file, "{}", new_state)?; self.commit(&format!( "change state of issue {}, {} -> {}", - self.dir.file_name().unwrap().to_string_lossy(), + self.dir + .file_name() + .ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))? + .to_string_lossy(), old_state, new_state, ))?; @@ -323,7 +326,10 @@ impl Issue { self.done_time = Some(done_time.clone()); self.commit(&format!( "set done-time of issue {} to {}", - self.dir.file_name().unwrap().to_string_lossy(), + self.dir + .file_name() + .ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))? + .to_string_lossy(), done_time, ))?; Ok(()) @@ -341,7 +347,10 @@ impl Issue { write!(assignee_file, "{}", new_assignee)?; self.commit(&format!( "change assignee of issue {}, {} -> {}", - self.dir.file_name().unwrap().to_string_lossy(), + self.dir + .file_name() + .ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))? + .to_string_lossy(), old_assignee, new_assignee, ))?; @@ -358,7 +367,10 @@ impl Issue { self.tags.sort(); self.commit_tags(&format!( "issue {} add tag {}", - self.dir.file_name().unwrap().to_string_lossy(), + self.dir + .file_name() + .ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))? + .to_string_lossy(), tag ))?; Ok(()) @@ -373,7 +385,10 @@ impl Issue { self.tags.remove(index); self.commit_tags(&format!( "issue {} remove tag {}", - self.dir.file_name().unwrap().to_string_lossy(), + self.dir + .file_name() + .ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))? + .to_string_lossy(), tag ))?; Ok(()) @@ -432,8 +447,8 @@ impl Issue { .spawn()? .wait_with_output()?; if !result.status.success() { - println!("stdout: {}", std::str::from_utf8(&result.stdout).unwrap()); - println!("stderr: {}", std::str::from_utf8(&result.stderr).unwrap()); + println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); + println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); return Err(IssueError::EditorError); } if !description_filename.exists() || description_filename.metadata()?.len() == 0 { From 97a575316e4af91300b6c0414e5f3160578b3691 Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Sat, 19 Jul 2025 09:54:00 -0600 Subject: [PATCH 11/36] Issues: skip & warn about any Issue that fails to parse This lets us at least handle the other, valid issues, while informing the user about the ones we don't understand. --- src/issues.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/issues.rs b/src/issues.rs index 709a79d..fe5728c 100644 --- a/src/issues.rs +++ b/src/issues.rs @@ -56,8 +56,19 @@ impl Issues { for direntry in dir.read_dir()? { if let Ok(direntry) = direntry { if direntry.metadata()?.is_dir() { - let issue = crate::issue::Issue::new_from_dir(direntry.path().as_path())?; - issues.add_issue(issue); + match crate::issue::Issue::new_from_dir(direntry.path().as_path()) { + Err(e) => { + println!( + "failed to parse issue {}, skipping", + direntry.file_name().to_string_lossy() + ); + println!("ignoring error: {:?}", e); + continue; + } + Ok(issue) => { + issues.add_issue(issue); + } + } } else if direntry.file_name() == "config.toml" { issues.parse_config(direntry.path().as_path())?; } else { From c2174340711ad4628dea5b8899c93d48cb5c406f Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Sat, 19 Jul 2025 10:38:15 -0600 Subject: [PATCH 12/36] better error handling in comment and git This replaces a bunch of `unwrap()` calls with error returns. --- src/comment.rs | 31 ++++++++---- src/git.rs | 125 +++++++++++++++++++++++++++++-------------------- 2 files changed, 96 insertions(+), 60 deletions(-) diff --git a/src/comment.rs b/src/comment.rs index 216b34f..17324b3 100644 --- a/src/comment.rs +++ b/src/comment.rs @@ -48,19 +48,23 @@ impl Comment { } } } - if description == None { + let Some(description) = description else { return Err(CommentError::CommentParseError); - } + }; let author = crate::git::git_log_oldest_author(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()), + uuid: String::from( + dir.file_name() + .ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))? + .to_string_lossy(), + ), author, creation_time, - description: description.unwrap(), + description, dir: std::path::PathBuf::from(comment_dir), }) } @@ -109,7 +113,11 @@ impl Comment { &format!( "add comment {} on issue {}", comment.uuid, - issue.dir.file_name().unwrap().to_string_lossy(), + issue + .dir + .file_name() + .ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))? + .to_string_lossy(), ), )?; } @@ -130,10 +138,15 @@ impl Comment { crate::git::add(&description_filename)?; if crate::git::worktree_is_dirty(&self.dir.to_string_lossy())? { crate::git::commit( - &description_filename.parent().unwrap(), + &description_filename + .parent() + .ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))?, &format!( "edit comment {} on issue FIXME", // FIXME: name the issue that the comment is on - self.dir.file_name().unwrap().to_string_lossy() + self.dir + .file_name() + .ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))? + .to_string_lossy() ), )?; self.read_description()?; @@ -165,8 +178,8 @@ impl Comment { .spawn()? .wait_with_output()?; if !result.status.success() { - println!("stdout: {}", std::str::from_utf8(&result.stdout).unwrap()); - println!("stderr: {}", std::str::from_utf8(&result.stderr).unwrap()); + println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); + println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); return Err(CommentError::EditorError); } diff --git a/src/git.rs b/src/git.rs index fe0446a..4a50d57 100644 --- a/src/git.rs +++ b/src/git.rs @@ -48,8 +48,8 @@ impl Worktree { .args(["worktree", "add", &path.path().to_string_lossy(), branch]) .output()?; if !result.status.success() { - println!("stdout: {}", std::str::from_utf8(&result.stdout).unwrap()); - println!("stderr: {}", std::str::from_utf8(&result.stderr).unwrap()); + println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); + println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); return Err(GitError::Oops); } Ok(Self { path }) @@ -67,8 +67,8 @@ impl Worktree { ]) .output()?; if !result.status.success() { - println!("stdout: {}", std::str::from_utf8(&result.stdout).unwrap()); - println!("stderr: {}", std::str::from_utf8(&result.stderr).unwrap()); + println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); + println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); return Err(GitError::Oops); } Ok(Self { path }) @@ -87,8 +87,8 @@ pub fn checkout_branch_in_worktree( .args(["worktree", "add", &worktree_dir.to_string_lossy(), branch]) .output()?; if !result.status.success() { - println!("stdout: {}", std::str::from_utf8(&result.stdout).unwrap()); - println!("stderr: {}", std::str::from_utf8(&result.stderr).unwrap()); + println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); + println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); return Err(GitError::Oops); } Ok(()) @@ -99,8 +99,8 @@ pub fn git_worktree_prune() -> Result<(), GitError> { .args(["worktree", "prune"]) .output()?; if !result.status.success() { - println!("stdout: {}", std::str::from_utf8(&result.stdout).unwrap()); - println!("stderr: {}", std::str::from_utf8(&result.stderr).unwrap()); + println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); + println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); return Err(GitError::Oops); } Ok(()) @@ -111,8 +111,8 @@ pub fn git_remove_branch(branch: &str) -> Result<(), GitError> { .args(["branch", "-D", branch]) .output()?; if !result.status.success() { - println!("stdout: {}", std::str::from_utf8(&result.stdout).unwrap()); - println!("stderr: {}", std::str::from_utf8(&result.stderr).unwrap()); + println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); + println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); return Err(GitError::Oops); } Ok(()) @@ -139,11 +139,14 @@ pub fn worktree_is_dirty(dir: &str) -> Result { pub fn add(file: &std::path::Path) -> Result<(), GitError> { let result = std::process::Command::new("git") .args(["add", &file.to_string_lossy()]) - .current_dir(file.parent().unwrap()) + .current_dir( + file.parent() + .ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))?, + ) .output()?; if !result.status.success() { - println!("stdout: {}", std::str::from_utf8(&result.stdout).unwrap()); - println!("stderr: {}", std::str::from_utf8(&result.stderr).unwrap()); + println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); + println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); return Err(GitError::Oops); } return Ok(()); @@ -152,11 +155,14 @@ pub fn add(file: &std::path::Path) -> Result<(), GitError> { pub fn restore_file(file: &std::path::Path) -> Result<(), GitError> { let result = std::process::Command::new("git") .args(["restore", &file.to_string_lossy()]) - .current_dir(file.parent().unwrap()) + .current_dir( + file.parent() + .ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))?, + ) .output()?; if !result.status.success() { - println!("stdout: {}", std::str::from_utf8(&result.stdout).unwrap()); - println!("stderr: {}", std::str::from_utf8(&result.stderr).unwrap()); + println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); + println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); return Err(GitError::Oops); } return Ok(()); @@ -168,8 +174,8 @@ pub fn commit(dir: &std::path::Path, msg: &str) -> Result<(), GitError> { .current_dir(dir) .output()?; if !result.status.success() { - println!("stdout: {}", std::str::from_utf8(&result.stdout).unwrap()); - println!("stderr: {}", std::str::from_utf8(&result.stderr).unwrap()); + println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); + println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); return Err(GitError::Oops); } Ok(()) @@ -180,12 +186,18 @@ pub fn git_commit_file(file: &std::path::Path) -> Result<(), GitError> { git_dir.pop(); let result = std::process::Command::new("git") - .args(["add", &file.file_name().unwrap().to_string_lossy()]) + .args([ + "add", + &file + .file_name() + .ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))? + .to_string_lossy(), + ]) .current_dir(&git_dir) .output()?; if !result.status.success() { - println!("stdout: {}", std::str::from_utf8(&result.stdout).unwrap()); - println!("stderr: {}", std::str::from_utf8(&result.stderr).unwrap()); + println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); + println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); return Err(GitError::Oops); } @@ -195,15 +207,20 @@ pub fn git_commit_file(file: &std::path::Path) -> Result<(), GitError> { "-m", &format!( "update '{}' in issue {}", - file.file_name().unwrap().to_string_lossy(), - git_dir.file_name().unwrap().to_string_lossy() + file.file_name() + .ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))? + .to_string_lossy(), + git_dir + .file_name() + .ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))? + .to_string_lossy() ), ]) .current_dir(&git_dir) .output()?; if !result.status.success() { - println!("stdout: {}", std::str::from_utf8(&result.stdout).unwrap()); - println!("stderr: {}", std::str::from_utf8(&result.stderr).unwrap()); + println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); + println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); return Err(GitError::Oops); } @@ -216,8 +233,8 @@ pub fn git_fetch(dir: &std::path::Path, remote: &str) -> Result<(), GitError> { .current_dir(dir) .output()?; if !result.status.success() { - println!("stdout: {}", std::str::from_utf8(&result.stdout).unwrap()); - println!("stderr: {}", std::str::from_utf8(&result.stderr).unwrap()); + println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); + println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); return Err(GitError::Oops); } Ok(()) @@ -253,13 +270,13 @@ pub fn sync(dir: &std::path::Path, remote: &str, branch: &str) -> Result<(), Git "Sync failed! 'git log' error! Help, a human needs to fix the mess in {:?}", branch ); - println!("stdout: {}", std::str::from_utf8(&result.stdout).unwrap()); - println!("stderr: {}", std::str::from_utf8(&result.stderr).unwrap()); + println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); + println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); return Err(GitError::Oops); } if result.stdout.len() > 0 { println!("Changes fetched from remote {}:", remote); - println!("{}", std::str::from_utf8(&result.stdout).unwrap()); + println!("{}", &String::from_utf8_lossy(&result.stdout)); println!(""); } @@ -279,13 +296,13 @@ pub fn sync(dir: &std::path::Path, remote: &str, branch: &str) -> Result<(), Git "Sync failed! 'git log' error! Help, a human needs to fix the mess in {:?}", branch ); - println!("stdout: {}", std::str::from_utf8(&result.stdout).unwrap()); - println!("stderr: {}", std::str::from_utf8(&result.stderr).unwrap()); + println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); + println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); return Err(GitError::Oops); } if result.stdout.len() > 0 { println!("Changes to push to remote {}:", remote); - println!("{}", std::str::from_utf8(&result.stdout).unwrap()); + println!("{}", &String::from_utf8_lossy(&result.stdout)); println!(""); } @@ -299,8 +316,8 @@ pub fn sync(dir: &std::path::Path, remote: &str, branch: &str) -> Result<(), Git "Sync failed! Merge error! Help, a human needs to fix the mess in {:?}", branch ); - println!("stdout: {}", std::str::from_utf8(&result.stdout).unwrap()); - println!("stderr: {}", std::str::from_utf8(&result.stderr).unwrap()); + println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); + println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); return Err(GitError::Oops); } @@ -314,8 +331,8 @@ pub fn sync(dir: &std::path::Path, remote: &str, branch: &str) -> Result<(), Git "Sync failed! Push error! Help, a human needs to fix the mess in {:?}", branch ); - println!("stdout: {}", std::str::from_utf8(&result.stdout).unwrap()); - println!("stderr: {}", std::str::from_utf8(&result.stderr).unwrap()); + println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); + println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); return Err(GitError::Oops); } @@ -332,13 +349,16 @@ pub fn git_log_oldest_timestamp( "log", "--pretty=format:%at", "--", - &path.file_name().unwrap().to_string_lossy(), + &path + .file_name() + .ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))? + .to_string_lossy(), ]) .current_dir(&git_dir) .output()?; if !result.status.success() { - println!("stdout: {}", std::str::from_utf8(&result.stdout).unwrap()); - println!("stderr: {}", std::str::from_utf8(&result.stderr).unwrap()); + println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); + println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); return Err(GitError::Oops); } let timestamp_str = std::str::from_utf8(&result.stdout).unwrap(); @@ -358,13 +378,16 @@ pub fn git_log_oldest_author(path: &std::path::Path) -> Result "log", "--pretty=format:%an <%ae>", "--", - &path.file_name().unwrap().to_string_lossy(), + &path + .file_name() + .ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))? + .to_string_lossy(), ]) .current_dir(&git_dir) .output()?; if !result.status.success() { - println!("stdout: {}", std::str::from_utf8(&result.stdout).unwrap()); - println!("stderr: {}", std::str::from_utf8(&result.stderr).unwrap()); + println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); + println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); return Err(GitError::Oops); } let author_str = std::str::from_utf8(&result.stdout).unwrap(); @@ -383,8 +406,8 @@ pub fn create_orphan_branch(branch: &str) -> Result<(), GitError> { .args(["worktree", "prune"]) .output()?; if !result.status.success() { - println!("stdout: {}", std::str::from_utf8(&result.stdout).unwrap()); - println!("stderr: {}", std::str::from_utf8(&result.stderr).unwrap()); + println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); + println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); return Err(GitError::Oops); } @@ -400,8 +423,8 @@ fn create_orphan_branch_at_path( .args(["worktree", "add", "--orphan", "-b", branch, &worktree_dir]) .output()?; if !result.status.success() { - println!("stdout: {}", std::str::from_utf8(&result.stdout).unwrap()); - println!("stderr: {}", std::str::from_utf8(&result.stderr).unwrap()); + println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); + println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); return Err(GitError::Oops); } @@ -418,8 +441,8 @@ fn create_orphan_branch_at_path( .current_dir(worktree_path) .output()?; if !result.status.success() { - println!("stdout: {}", std::str::from_utf8(&result.stdout).unwrap()); - println!("stderr: {}", std::str::from_utf8(&result.stderr).unwrap()); + println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); + println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); return Err(GitError::Oops); } @@ -428,8 +451,8 @@ fn create_orphan_branch_at_path( .current_dir(worktree_path) .output()?; if !result.status.success() { - println!("stdout: {}", std::str::from_utf8(&result.stdout).unwrap()); - println!("stderr: {}", std::str::from_utf8(&result.stderr).unwrap()); + println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); + println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); return Err(GitError::Oops); } From e79fc4917d703b71a9f04431bf1b667079af0aef Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Sat, 19 Jul 2025 10:52:33 -0600 Subject: [PATCH 13/36] Issues::new_from_dir(): move error message to stderr --- src/issues.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/issues.rs b/src/issues.rs index fe5728c..a01f41c 100644 --- a/src/issues.rs +++ b/src/issues.rs @@ -58,11 +58,11 @@ impl Issues { if direntry.metadata()?.is_dir() { match crate::issue::Issue::new_from_dir(direntry.path().as_path()) { Err(e) => { - println!( + eprintln!( "failed to parse issue {}, skipping", direntry.file_name().to_string_lossy() ); - println!("ignoring error: {:?}", e); + eprintln!("ignoring error: {:?}", e); continue; } Ok(issue) => { From 8b41f1ebc67a33a3fd6a8cc6c8522bd2a82ece70 Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Sat, 19 Jul 2025 13:15:50 -0600 Subject: [PATCH 14/36] add a tools directory including a "done-last-week" script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This script lists issues that were marked `Done` between "midnight at the start of the second most recent Monday" and "midnight at the start of the most recent Monday". $ ./tools/done-last-week Done: 8c73c9fd5bc4f551ee5069035ae6e866 migrate the Todo list into entomologist 75cefad80aacbf23fc7b9c24a75aa236 🗨️ 4 # implement `ent comment ISSUE [DESCRIPTION]` (👉 seb) 7da3bd5b72de0a05936b094db5d24304 🗨️ 1 implement `ent edit ${COMMENT}` (👉 seb) 198a7d56a19f0579fbc04f2ee9cc234f fix ignoring unknown file in issues directory: "README.md" e089400e8a9e11fe9bf10d50b2f889d7 add `ent sync` to keep local `entomologist-data` branch in sync with remote a26da230276d317e85f9fcca41c19d2e `ent edit ${ISSUE}` with no change fails (👉 seb) 317ea8ccac1d414cde55771321bdec30 🗨️ 2 allow multiple read-only ent processes simultaneously (👉 seb) da435e5e298b28dc223f9dcfe62a9140 add user control over state transitions (👉 lex) fd81241f795333b64e7911cfb1b57c8f commit messages in the `entomologist-data` branch could be better (👉 seb) 093e87e8049b93bfa2d8fcd544cae75f add optional 'assignee' to issue (👉 seb) 793bda8b9726b0336d97e856895907f8 `ent list` should have a consistent sort order (👉 seb) af53c561b36e9b2709b939f81daee534 use git author info to attribute issues and comments to people (👉 seb) 9e69a30ad6965d7488514584c97ac63c teach `ent list FILTER` to filter by assignee (👉 seb) a5ac277614ea4d13f78031abb25ea7d6 `ent new` and `ent comment`: detect empty issue descriptions & comments (👉 seb) 7d2d236668872cf11f167ac0462f8751 🗨️ 1 add `ent tag ISSUE [[-]TAG]` (👉 seb) 54f0eb67b05aa10763c86869ce840f33 `ent sync` should report what changes got fetched & what changes will be pushed (👉 seb) 4e314a8590864fa76d22758e1785ae35 don't spawn an editor if stdin & stdout aren't a terminal (👉 seb) d3a705245bd69aa56524b80b5ae0bc26 🗨️ 1 move IssuesDatabase out of binary and into library (👉 sigil-03) --- tools/README.md | 4 ++++ tools/done-last-week | 11 +++++++++++ tools/set-done-time | 19 +++++++++++++++++++ 3 files changed, 34 insertions(+) create mode 100644 tools/README.md create mode 100755 tools/done-last-week create mode 100755 tools/set-done-time diff --git a/tools/README.md b/tools/README.md new file mode 100644 index 0000000..dd3cb2f --- /dev/null +++ b/tools/README.md @@ -0,0 +1,4 @@ +This directory contains small helper scripts and tools that are peripheral +or tangent to the main entomologist tool. + +We make no guarantees about functionality or correctness. diff --git a/tools/done-last-week b/tools/done-last-week new file mode 100755 index 0000000..da9ec94 --- /dev/null +++ b/tools/done-last-week @@ -0,0 +1,11 @@ +#!/bin/bash + +START=$(date --iso-8601=seconds --date='last monday - 1 week') +END=$(date --iso-8601=seconds --date='last monday') + +#echo START=${START} +#echo END=${END} + +ent list \ + state=done \ + done-time="${START}..${END}" diff --git a/tools/set-done-time b/tools/set-done-time new file mode 100755 index 0000000..6a29fd9 --- /dev/null +++ b/tools/set-done-time @@ -0,0 +1,19 @@ +#!/bin/bash +# +# This script finds all issues with state=Done which do not have a +# `done_time`. +# +# It sets each issue's `done_time` to the most recent time that the +# `state` was updated from the git log. +# + +set -e + +for ISSUE_ID in $(ent list state=done done-time=9999-01-01T00:00:00-06:00.. | grep ' ' | cut -f 1 -d ' '); do + echo ${ISSUE_ID} + UTIME=$(PAGER='' git log -n1 --pretty=format:%at%n entomologist-data -- ${ISSUE_ID}/state) + echo ${UTIME} + DATETIME=$(date --rfc-3339=seconds --date="@${UTIME}") + echo ${DATETIME} + ent done-time ${ISSUE_ID} "${DATETIME}" +done From 8af9c71ef6953471db44acdd56bd707bb85ff4b7 Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Sat, 19 Jul 2025 21:10:14 -0600 Subject: [PATCH 15/36] `ent done-time ISSUE TIME`: report parse error instead of panicking --- src/bin/ent/main.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/bin/ent/main.rs b/src/bin/ent/main.rs index 1698954..651e0fc 100644 --- a/src/bin/ent/main.rs +++ b/src/bin/ent/main.rs @@ -499,9 +499,13 @@ fn handle_command( 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); + let done_time = match chrono::DateTime::parse_from_rfc3339(done_time) { + Ok(done_time) => done_time.with_timezone(&chrono::Local), + Err(e) => { + eprintln!("failed to parse done-time from {}", done_time); + return Err(e.into()); + } + }; issue.set_done_time(done_time)?; } None => match &issue.done_time { From 0d9a893087b50e66a05def0d4f4f6d9f9054975e Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Sun, 20 Jul 2025 00:01:19 -0600 Subject: [PATCH 16/36] `ent show`: simplify logic This simplifies the code flow and gets rid of two levels of indentation. --- src/bin/ent/main.rs | 54 +++++++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/src/bin/ent/main.rs b/src/bin/ent/main.rs index 1698954..c9327e1 100644 --- a/src/bin/ent/main.rs +++ b/src/bin/ent/main.rs @@ -285,35 +285,31 @@ fn handle_command( Commands::Show { issue_id } => { let issues = entomologist::database::read_issues_database(issues_database_source)?; - match issues.get_issue(issue_id) { - Some(issue) => { - println!("issue {}", issue_id); - println!("author: {}", issue.author); - 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); - } - if let Some(assignee) = &issue.assignee { - println!("assignee: {}", assignee); - } - println!(""); - println!("{}", issue.description); - for comment in &issue.comments { - println!(""); - println!("comment: {}", comment.uuid); - println!("author: {}", comment.author); - println!("creation_time: {}", comment.creation_time); - println!(""); - println!("{}", comment.description); - } - } - None => { - return Err(anyhow::anyhow!("issue {} not found", issue_id)); - } + let Some(issue) = issues.get_issue(issue_id) else { + return Err(anyhow::anyhow!("issue {} not found", issue_id)); + }; + println!("issue {}", issue_id); + println!("author: {}", issue.author); + 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); + } + if let Some(assignee) = &issue.assignee { + println!("assignee: {}", assignee); + } + println!(""); + println!("{}", issue.description); + for comment in &issue.comments { + println!(""); + println!("comment: {}", comment.uuid); + println!("author: {}", comment.author); + println!("creation_time: {}", comment.creation_time); + println!(""); + println!("{}", comment.description); } } From c9dbec730cf18c5175a38c84b78443c204a0206f Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Sun, 20 Jul 2025 00:04:11 -0600 Subject: [PATCH 17/36] `ent show`: show tags, if any --- src/bin/ent/main.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/bin/ent/main.rs b/src/bin/ent/main.rs index c9327e1..13550db 100644 --- a/src/bin/ent/main.rs +++ b/src/bin/ent/main.rs @@ -290,6 +290,15 @@ fn handle_command( }; println!("issue {}", issue_id); println!("author: {}", issue.author); + if issue.tags.len() > 0 { + print!("tags: "); + let mut separator = ""; + for tag in &issue.tags { + print!("{}{}", separator, tag); + separator = ", "; + } + println!(""); + } println!("creation_time: {}", issue.creation_time); if let Some(done_time) = &issue.done_time { println!("done_time: {}", done_time); From 8a92bf2637e26536bb7fef7aa1920ed36b7ffc98 Mon Sep 17 00:00:00 2001 From: sigil-03 Date: Sun, 20 Jul 2025 11:55:29 -0600 Subject: [PATCH 18/36] fix test directory to match updated dependency representation --- test/0002/a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7/dependencies | 2 -- .../dependencies/3fa5bfd93317ad25772680071d5ac3259cd2384f | 0 .../dependencies/dd79c8cfb8beeacd0460429944b4ecbe95a31561 | 0 3 files changed, 2 deletions(-) delete mode 100644 test/0002/a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7/dependencies create mode 100644 test/0002/a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7/dependencies/3fa5bfd93317ad25772680071d5ac3259cd2384f create mode 100644 test/0002/a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7/dependencies/dd79c8cfb8beeacd0460429944b4ecbe95a31561 diff --git a/test/0002/a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7/dependencies b/test/0002/a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7/dependencies deleted file mode 100644 index 71e4ee3..0000000 --- a/test/0002/a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7/dependencies +++ /dev/null @@ -1,2 +0,0 @@ -3fa5bfd93317ad25772680071d5ac3259cd2384f -dd79c8cfb8beeacd0460429944b4ecbe95a31561 \ No newline at end of file diff --git a/test/0002/a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7/dependencies/3fa5bfd93317ad25772680071d5ac3259cd2384f b/test/0002/a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7/dependencies/3fa5bfd93317ad25772680071d5ac3259cd2384f new file mode 100644 index 0000000..e69de29 diff --git a/test/0002/a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7/dependencies/dd79c8cfb8beeacd0460429944b4ecbe95a31561 b/test/0002/a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7/dependencies/dd79c8cfb8beeacd0460429944b4ecbe95a31561 new file mode 100644 index 0000000..e69de29 From 9b8c077653861b9507e3b5dd75272879ceac318c Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Sun, 20 Jul 2025 11:58:47 -0600 Subject: [PATCH 19/36] remove Todo file, we have entomologist-data now --- Todo.md | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 Todo.md diff --git a/Todo.md b/Todo.md deleted file mode 100644 index 9594c71..0000000 --- a/Todo.md +++ /dev/null @@ -1,19 +0,0 @@ -# To do - -* migrate this todo list into entomologist - -* implement user control over state transitions - -* implement `ent comment ${ISSUE} [-m ${MESSAGE}]` - - each issue dir has a `comments` subdir - - each comment is identified by a sha1-style uid - - each comment is a file or directory under the `${ISSUE}/comments` - - comments are ordered by ctime? - -* implement `ent edit ${COMMENT}` - -* implement `ent attach ${ISSUE} ${FILE}` - - each issue has its own independent namespace for attached files - - issue description & comments can reference attached files via standard md links - -* write a manpage From f60dd18c0afd7132a459a18cccbab8f6a32ea2de Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Sun, 20 Jul 2025 22:22:41 -0600 Subject: [PATCH 20/36] sort dependencies alphabetically after reading them This is mostly to make the tests reliable. Without this the dependencies are inserted into the vector in directory order, which in my checkout of the repo did not match the alphabetical order of the dependencies in the test. --- src/issue.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/issue.rs b/src/issue.rs index 5022ace..aafb4e2 100644 --- a/src/issue.rs +++ b/src/issue.rs @@ -212,6 +212,9 @@ impl Issue { } } } + if let Some(deps) = &mut dependencies { + deps.sort(); + } Ok(dependencies) } From 452671d272e049263030cacc9ea1afba56beb926 Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Tue, 22 Jul 2025 10:28:29 -0600 Subject: [PATCH 21/36] add `time-ent` tool to measure runtime of different ent commands --- tools/time-ent | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100755 tools/time-ent diff --git a/tools/time-ent b/tools/time-ent new file mode 100755 index 0000000..366a02c --- /dev/null +++ b/tools/time-ent @@ -0,0 +1,43 @@ +#!/bin/bash +# +# * Create a temporary ent issue database branch based on a specific +# commit in `entomologist-data`. +# +# * Perform some ent operations on this temporary branch and measure +# the runtime. +# +# * Clean up by deleteting the temporary branch. + +set -e +#set -x + + +# This is a commit in the `entomologist-data` branch that we're somewhat +# arbitrarily using here to time different `ent` operations. +TEST_COMMIT=a33f1165d77571d770f1a1021afe4c07360247f0 + +# This is the branch that we create from the above commit and test our +# `ent` operations on. We'll delete this branch when we're done with +# the tests. +TEST_BRANCH=$(mktemp --dry-run entomologist-data-XXXXXXXX) + + +function time_ent() { + echo timing: ent "$@" + time -p ent -b "${TEST_BRANCH}" "$@" + echo +} + + +git branch "${TEST_BRANCH}" "${TEST_COMMIT}" + +time_ent tag 7e2a3a59fb6b77403ff1035255367607 +time_ent tag 7e2a3a59fb6b77403ff1035255367607 new-tag + +time_ent assign 7e2a3a59fb6b77403ff1035255367607 +time_ent assign 7e2a3a59fb6b77403ff1035255367607 new-user + +time_ent done-time 7e2a3a59fb6b77403ff1035255367607 +time_ent done-time 7e2a3a59fb6b77403ff1035255367607 2025-04-01T01:23:45-06:00 + +git branch -D "${TEST_BRANCH}" From cc1b3783468a9d29aad2f620105deed53a967a83 Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Tue, 22 Jul 2025 10:01:31 -0600 Subject: [PATCH 22/36] ent tag: speed up adding/removing tag --- src/bin/ent/main.rs | 75 ++++++++++++++++++++++----------------------- 1 file changed, 36 insertions(+), 39 deletions(-) diff --git a/src/bin/ent/main.rs b/src/bin/ent/main.rs index 549d16d..d3975c3 100644 --- a/src/bin/ent/main.rs +++ b/src/bin/ent/main.rs @@ -445,50 +445,47 @@ fn handle_command( } } - Commands::Tag { issue_id, tag } => { - 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 tag { - Some(tag) => { - // 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)); - }; - if tag.len() == 0 { - return Err(anyhow::anyhow!("invalid zero-length tag")); - } - if tag.chars().nth(0).unwrap() == '-' { - let tag = &tag[1..]; - issue.remove_tag(tag)?; - } else { - issue.add_tag(tag)?; - } + Commands::Tag { issue_id, tag } => match tag { + Some(tag) => { + // Add or remove tag. + if tag.len() == 0 { + return Err(anyhow::anyhow!("invalid zero-length tag")); } - None => { - // Just list the tags. - match &issue.tags.len() { - 0 => println!("no tags"), - _ => { - // Could use `format!(" {:?}", issue.tags)` - // here, but that results in `["tag1", "TAG2", - // "i-am-also-a-tag"]` and i don't want the - // double-quotes around each tag. - for tag in &issue.tags { - println!("{}", 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)); + }; + if tag.chars().nth(0).unwrap() == '-' { + let tag = &tag[1..]; + issue.remove_tag(tag)?; + } else { + issue.add_tag(tag)?; + } + } + None => { + // Just list the tags. + 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 &issue.tags.len() { + 0 => println!("no tags"), + _ => { + // Could use `format!(" {:?}", issue.tags)` + // here, but that results in `["tag1", "TAG2", + // "i-am-also-a-tag"]` and i don't want the + // double-quotes around each tag. + for tag in &issue.tags { + println!("{}", tag); } } } } - } + }, Commands::DoneTime { issue_id, From e2a7c81a132607b1430a32d206d3c6c59a2502a0 Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Tue, 22 Jul 2025 10:08:12 -0600 Subject: [PATCH 23/36] ent assign: speed up setting of assignee --- src/bin/ent/main.rs | 58 +++++++++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/src/bin/ent/main.rs b/src/bin/ent/main.rs index d3975c3..8a947e5 100644 --- a/src/bin/ent/main.rs +++ b/src/bin/ent/main.rs @@ -415,35 +415,37 @@ fn handle_command( Commands::Assign { issue_id, new_assignee, - } => { - let issues = entomologist::database::read_issues_database(issues_database_source)?; - let Some(original_issue) = issues.issues.get(issue_id) else { - return Err(anyhow::anyhow!("issue {} not found", issue_id)); - }; - let old_assignee: String = match &original_issue.assignee { - Some(assignee) => assignee.clone(), - None => String::from("None"), - }; - println!("issue: {}", issue_id); - match new_assignee { - Some(new_assignee) => { - 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)); - }; - println!("assignee: {} -> {}", old_assignee, new_assignee); - issue.set_assignee(new_assignee)?; - } - None => { - println!("assignee: {}", old_assignee); - } + } => match new_assignee { + Some(new_assignee) => { + 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 old_assignee: String = match &issue.assignee { + Some(assignee) => assignee.clone(), + None => String::from("None"), + }; + issue.set_assignee(new_assignee)?; + println!("issue: {}", issue_id); + println!("assignee: {} -> {}", old_assignee, new_assignee); } - } + None => { + let issues = entomologist::database::read_issues_database(issues_database_source)?; + let Some(original_issue) = issues.issues.get(issue_id) else { + return Err(anyhow::anyhow!("issue {} not found", issue_id)); + }; + let old_assignee: String = match &original_issue.assignee { + Some(assignee) => assignee.clone(), + None => String::from("None"), + }; + println!("issue: {}", issue_id); + println!("assignee: {}", old_assignee); + } + }, Commands::Tag { issue_id, tag } => match tag { Some(tag) => { From def729d43a761d07a1a3851cdb87ca663a167a2a Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Tue, 22 Jul 2025 10:12:09 -0600 Subject: [PATCH 24/36] ent done-time: speed up setting of done-time --- src/bin/ent/main.rs | 59 ++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/src/bin/ent/main.rs b/src/bin/ent/main.rs index 8a947e5..e071233 100644 --- a/src/bin/ent/main.rs +++ b/src/bin/ent/main.rs @@ -492,38 +492,37 @@ 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 = match chrono::DateTime::parse_from_rfc3339(done_time) { - Ok(done_time) => done_time.with_timezone(&chrono::Local), - Err(e) => { - eprintln!("failed to parse done-time from {}", done_time); - return Err(e.into()); - } - }; - issue.set_done_time(done_time)?; - } - None => match &issue.done_time { + } => 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 = match chrono::DateTime::parse_from_rfc3339(done_time) { + Ok(done_time) => done_time.with_timezone(&chrono::Local), + Err(e) => { + eprintln!("failed to parse done-time from {}", done_time); + return Err(e.into()); + } + }; + issue.set_done_time(done_time)?; + } + None => { + 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 &issue.done_time { Some(done_time) => println!("done_time: {}", done_time), None => println!("None"), - }, - }; - } + }; + } + }, Commands::Depend { issue_id, From c15736259c445311120bbb52e24175e745f34d16 Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Tue, 22 Jul 2025 13:04:54 -0600 Subject: [PATCH 25/36] add git::git_log_oldest_author_timestamp(), saves us one `git log` This cuts about 30% off the time to read the issues from entomologist-data. --- src/git.rs | 40 ++++++++++++++++++++++++++++++++++++++++ src/issue.rs | 3 +-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/git.rs b/src/git.rs index 4a50d57..3a03bac 100644 --- a/src/git.rs +++ b/src/git.rs @@ -395,6 +395,46 @@ pub fn git_log_oldest_author(path: &std::path::Path) -> Result Ok(String::from(author_last)) } +pub fn git_log_oldest_author_timestamp( + path: &std::path::Path, +) -> Result<(String, chrono::DateTime), GitError> { + let mut git_dir = std::path::PathBuf::from(path); + git_dir.pop(); + let result = std::process::Command::new("git") + .args([ + "log", + "--pretty=format:%at %an <%ae>", + "--", + &path + .file_name() + .ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))? + .to_string_lossy(), + ]) + .current_dir(&git_dir) + .output()?; + if !result.status.success() { + println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); + println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); + return Err(GitError::Oops); + } + + let raw_output_str = String::from_utf8_lossy(&result.stdout); + let Some(raw_output_last) = raw_output_str.split("\n").last() else { + return Err(GitError::Oops); + }; + let Some(index) = raw_output_last.find(' ') else { + return Err(GitError::Oops); + }; + let author_str = &raw_output_last[index + 1..]; + let timestamp_str = &raw_output_last[0..index]; + let timestamp_i64 = timestamp_str.parse::()?; + let timestamp = chrono::DateTime::from_timestamp(timestamp_i64, 0) + .unwrap() + .with_timezone(&chrono::Local); + + Ok((String::from(author_str), timestamp)) +} + pub fn create_orphan_branch(branch: &str) -> Result<(), GitError> { { let tmp_worktree = tempfile::tempdir().unwrap(); diff --git a/src/issue.rs b/src/issue.rs index aafb4e2..e3bf9d3 100644 --- a/src/issue.rs +++ b/src/issue.rs @@ -166,8 +166,7 @@ impl Issue { Err(IssueError::IdError)? }; - let author = crate::git::git_log_oldest_author(dir)?; - let creation_time = crate::git::git_log_oldest_timestamp(dir)?; + let (author, creation_time) = crate::git::git_log_oldest_author_timestamp(dir)?; Ok(Self { id, From eb7ac21ac85feae7b28861cb4ecf76b84b396d58 Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Tue, 22 Jul 2025 13:13:59 -0600 Subject: [PATCH 26/36] half as many `git log` calls when reading a comment --- src/comment.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/comment.rs b/src/comment.rs index 17324b3..e042f63 100644 --- a/src/comment.rs +++ b/src/comment.rs @@ -52,8 +52,8 @@ impl Comment { return Err(CommentError::CommentParseError); }; - let author = crate::git::git_log_oldest_author(comment_dir)?; - let creation_time = crate::git::git_log_oldest_timestamp(comment_dir)?; + let (author, creation_time) = crate::git::git_log_oldest_author_timestamp(comment_dir)?; + let dir = std::path::PathBuf::from(comment_dir); Ok(Self { From e1287514f65fcc8052a4627170dc2c8018a8a353 Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Wed, 23 Jul 2025 15:26:42 -0600 Subject: [PATCH 27/36] switch to pretty_assertions, makes it much easier to tell what blew up --- Cargo.toml | 3 +++ src/comment.rs | 1 + src/git.rs | 1 + src/issue.rs | 1 + src/issues.rs | 1 + 5 files changed, 7 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 864691a..4d2d2c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,9 @@ edition = "2024" default = [] log = ["dep:log", "dep:simple_logger"] +[dev-dependencies] +pretty_assertions = "1.4.1" + [dependencies] anyhow = "1.0.95" chrono = "0.4.41" diff --git a/src/comment.rs b/src/comment.rs index e042f63..9770d65 100644 --- a/src/comment.rs +++ b/src/comment.rs @@ -208,6 +208,7 @@ impl Comment { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn read_comment_0() { diff --git a/src/git.rs b/src/git.rs index 3a03bac..6e70fa8 100644 --- a/src/git.rs +++ b/src/git.rs @@ -502,6 +502,7 @@ fn create_orphan_branch_at_path( #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_worktree() { diff --git a/src/issue.rs b/src/issue.rs index e3bf9d3..4d82deb 100644 --- a/src/issue.rs +++ b/src/issue.rs @@ -548,6 +548,7 @@ impl Issue { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn read_issue_0() { diff --git a/src/issues.rs b/src/issues.rs index a01f41c..7c43e43 100644 --- a/src/issues.rs +++ b/src/issues.rs @@ -87,6 +87,7 @@ impl Issues { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn read_issues_0000() { From 7abcf2e4466d95464252c036743cda3a757726a9 Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Wed, 23 Jul 2025 18:45:20 -0600 Subject: [PATCH 28/36] sort issue tags This will be useful testing (and general consistency) when tags are files in a directory instead of lines in a file, and thus subject to random directory order. --- src/issue.rs | 3 ++- src/issues.rs | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/issue.rs b/src/issue.rs index 4d82deb..6f5889e 100644 --- a/src/issue.rs +++ b/src/issue.rs @@ -143,6 +143,7 @@ impl Issue { .filter(|s| s.len() > 0) .map(|tag| String::from(tag.trim())) .collect(); + tags.sort(); } else if file_name == "comments" && direntry.metadata()?.is_dir() { Self::read_comments(&mut comments, &direntry.path())?; } else { @@ -562,9 +563,9 @@ mod tests { .with_timezone(&chrono::Local), done_time: None, tags: Vec::::from([ - String::from("tag1"), String::from("TAG2"), String::from("i-am-also-a-tag"), + String::from("tag1"), ]), state: State::New, dependencies: None, diff --git a/src/issues.rs b/src/issues.rs index 7c43e43..fc182e7 100644 --- a/src/issues.rs +++ b/src/issues.rs @@ -127,9 +127,9 @@ mod tests { .with_timezone(&chrono::Local), done_time: None, tags: Vec::::from([ - String::from("tag1"), String::from("TAG2"), - String::from("i-am-also-a-tag") + String::from("i-am-also-a-tag"), + String::from("tag1"), ]), state: crate::issue::State::New, dependencies: None, From ef8a648cf8d6f60ac82c0017bec930459e3a76f3 Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Thu, 24 Jul 2025 08:36:25 -0600 Subject: [PATCH 29/36] test dir cleanup: rename test/0000/3943fc5c173fdf41c0a22251593cd476 Renaming everything also means they have new creation-times, since we're now git logging a different file/dir. --- src/issue.rs | 6 +++--- src/issues.rs | 4 ++-- .../description | 0 .../tags | 0 4 files changed, 5 insertions(+), 5 deletions(-) rename test/0000/{3943fc5c173fdf41c0a22251593cd476d96e6c9f => 3943fc5c173fdf41c0a22251593cd476}/description (100%) rename test/0000/{3943fc5c173fdf41c0a22251593cd476d96e6c9f => 3943fc5c173fdf41c0a22251593cd476}/tags (100%) diff --git a/src/issue.rs b/src/issue.rs index 6f5889e..3e13a7d 100644 --- a/src/issue.rs +++ b/src/issue.rs @@ -553,12 +553,12 @@ mod tests { #[test] fn read_issue_0() { - let issue_dir = std::path::Path::new("test/0000/3943fc5c173fdf41c0a22251593cd476d96e6c9f/"); + let issue_dir = std::path::Path::new("test/0000/3943fc5c173fdf41c0a22251593cd476/"); let issue = Issue::new_from_dir(issue_dir).unwrap(); let expected = Issue { - id: String::from("3943fc5c173fdf41c0a22251593cd476d96e6c9f"), + id: String::from("3943fc5c173fdf41c0a22251593cd476"), author: String::from("Sebastian Kuzminsky "), - creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-03T12:14:26-06:00") + creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-24T08:36:25-06:00") .unwrap() .with_timezone(&chrono::Local), done_time: None, diff --git a/src/issues.rs b/src/issues.rs index fc182e7..11cb233 100644 --- a/src/issues.rs +++ b/src/issues.rs @@ -115,14 +115,14 @@ mod tests { dir, }); - let uuid = String::from("3943fc5c173fdf41c0a22251593cd476d96e6c9f"); + let uuid = String::from("3943fc5c173fdf41c0a22251593cd476"); let mut dir = std::path::PathBuf::from(issues_dir); dir.push(&uuid); expected.add_issue( crate::issue::Issue { id: uuid, author: String::from("Sebastian Kuzminsky "), - creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-03T12:14:26-06:00") + creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-24T08:36:25-06:00") .unwrap() .with_timezone(&chrono::Local), done_time: None, diff --git a/test/0000/3943fc5c173fdf41c0a22251593cd476d96e6c9f/description b/test/0000/3943fc5c173fdf41c0a22251593cd476/description similarity index 100% rename from test/0000/3943fc5c173fdf41c0a22251593cd476d96e6c9f/description rename to test/0000/3943fc5c173fdf41c0a22251593cd476/description diff --git a/test/0000/3943fc5c173fdf41c0a22251593cd476d96e6c9f/tags b/test/0000/3943fc5c173fdf41c0a22251593cd476/tags similarity index 100% rename from test/0000/3943fc5c173fdf41c0a22251593cd476d96e6c9f/tags rename to test/0000/3943fc5c173fdf41c0a22251593cd476/tags From 4683760942d33f99c50e321c3a901c0fc619da96 Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Thu, 24 Jul 2025 08:37:07 -0600 Subject: [PATCH 30/36] test dir cleanup: rename test/0000/7792b063eef6d33e7da5dc1856750c14 Renaming everything also means they have new creation-times, since we're now git logging a different file/dir. --- src/issue.rs | 6 +++--- src/issues.rs | 4 ++-- .../assignee | 0 .../description | 0 .../state | 0 5 files changed, 5 insertions(+), 5 deletions(-) rename test/0000/{7792b063eef6d33e7da5dc1856750c149ba678c6 => 7792b063eef6d33e7da5dc1856750c14}/assignee (100%) rename test/0000/{7792b063eef6d33e7da5dc1856750c149ba678c6 => 7792b063eef6d33e7da5dc1856750c14}/description (100%) rename test/0000/{7792b063eef6d33e7da5dc1856750c149ba678c6 => 7792b063eef6d33e7da5dc1856750c14}/state (100%) diff --git a/src/issue.rs b/src/issue.rs index 3e13a7d..06f959f 100644 --- a/src/issue.rs +++ b/src/issue.rs @@ -581,12 +581,12 @@ mod tests { #[test] fn read_issue_1() { - let issue_dir = std::path::Path::new("test/0000/7792b063eef6d33e7da5dc1856750c149ba678c6/"); + let issue_dir = std::path::Path::new("test/0000/7792b063eef6d33e7da5dc1856750c14/"); let issue = Issue::new_from_dir(issue_dir).unwrap(); let expected = Issue { - id: String::from("7792b063eef6d33e7da5dc1856750c149ba678c6"), + id: String::from("7792b063eef6d33e7da5dc1856750c14"), author: String::from("Sebastian Kuzminsky "), - creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-03T12:14:26-06:00") + creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-24T08:37:07-06:00") .unwrap() .with_timezone(&chrono::Local), done_time: None, diff --git a/src/issues.rs b/src/issues.rs index 11cb233..db32737 100644 --- a/src/issues.rs +++ b/src/issues.rs @@ -96,13 +96,13 @@ mod tests { let mut expected = Issues::new(); - let uuid = String::from("7792b063eef6d33e7da5dc1856750c149ba678c6"); + let uuid = String::from("7792b063eef6d33e7da5dc1856750c14"); let mut dir = std::path::PathBuf::from(issues_dir); dir.push(&uuid); expected.add_issue(crate::issue::Issue { id: uuid, author: String::from("Sebastian Kuzminsky "), - creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-03T12:14:26-06:00") + creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-24T08:37:07-06:00") .unwrap() .with_timezone(&chrono::Local), done_time: None, diff --git a/test/0000/7792b063eef6d33e7da5dc1856750c149ba678c6/assignee b/test/0000/7792b063eef6d33e7da5dc1856750c14/assignee similarity index 100% rename from test/0000/7792b063eef6d33e7da5dc1856750c149ba678c6/assignee rename to test/0000/7792b063eef6d33e7da5dc1856750c14/assignee diff --git a/test/0000/7792b063eef6d33e7da5dc1856750c149ba678c6/description b/test/0000/7792b063eef6d33e7da5dc1856750c14/description similarity index 100% rename from test/0000/7792b063eef6d33e7da5dc1856750c149ba678c6/description rename to test/0000/7792b063eef6d33e7da5dc1856750c14/description diff --git a/test/0000/7792b063eef6d33e7da5dc1856750c149ba678c6/state b/test/0000/7792b063eef6d33e7da5dc1856750c14/state similarity index 100% rename from test/0000/7792b063eef6d33e7da5dc1856750c149ba678c6/state rename to test/0000/7792b063eef6d33e7da5dc1856750c14/state From 694d127638842de94ef646819be71e1e1bb86255 Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Thu, 24 Jul 2025 08:37:46 -0600 Subject: [PATCH 31/36] test dir cleanup: rename test/0001/3fa5bfd93317ad25772680071d5ac325 Renaming everything also means they have new creation-times, since we're now git logging a different file/dir. --- src/issues.rs | 4 ++-- .../description | 0 .../done_time | 0 .../state | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename test/0001/{3fa5bfd93317ad25772680071d5ac3259cd2384f => 3fa5bfd93317ad25772680071d5ac325}/description (100%) rename test/0001/{3fa5bfd93317ad25772680071d5ac3259cd2384f => 3fa5bfd93317ad25772680071d5ac325}/done_time (100%) rename test/0001/{3fa5bfd93317ad25772680071d5ac3259cd2384f => 3fa5bfd93317ad25772680071d5ac325}/state (100%) diff --git a/src/issues.rs b/src/issues.rs index db32737..bd8d687 100644 --- a/src/issues.rs +++ b/src/issues.rs @@ -149,13 +149,13 @@ mod tests { let mut expected = Issues::new(); - let uuid = String::from("3fa5bfd93317ad25772680071d5ac3259cd2384f"); + let uuid = String::from("3fa5bfd93317ad25772680071d5ac325"); let mut dir = std::path::PathBuf::from(issues_dir); dir.push(&uuid); expected.add_issue(crate::issue::Issue { id: uuid, author: String::from("Sebastian Kuzminsky "), - creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-03T11:59:44-06:00") + creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-24T08:37:46-06:00") .unwrap() .with_timezone(&chrono::Local), done_time: Some( diff --git a/test/0001/3fa5bfd93317ad25772680071d5ac3259cd2384f/description b/test/0001/3fa5bfd93317ad25772680071d5ac325/description similarity index 100% rename from test/0001/3fa5bfd93317ad25772680071d5ac3259cd2384f/description rename to test/0001/3fa5bfd93317ad25772680071d5ac325/description diff --git a/test/0001/3fa5bfd93317ad25772680071d5ac3259cd2384f/done_time b/test/0001/3fa5bfd93317ad25772680071d5ac325/done_time similarity index 100% rename from test/0001/3fa5bfd93317ad25772680071d5ac3259cd2384f/done_time rename to test/0001/3fa5bfd93317ad25772680071d5ac325/done_time diff --git a/test/0001/3fa5bfd93317ad25772680071d5ac3259cd2384f/state b/test/0001/3fa5bfd93317ad25772680071d5ac325/state similarity index 100% rename from test/0001/3fa5bfd93317ad25772680071d5ac3259cd2384f/state rename to test/0001/3fa5bfd93317ad25772680071d5ac325/state From 05c7c6f4416f23d8d71b54e80ca2a0683bad2b2c Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Thu, 24 Jul 2025 10:08:24 -0600 Subject: [PATCH 32/36] test dir cleanup: rename test/0001/dd79c8cfb8beeacd0460429944b4ecbe Renaming everything also means they have new creation-times, since we're now git logging a different file/dir. fixup test/0001/dd79c8cfb8beeacd0460429944b4ecbe, no comment yet --- src/issues.rs | 15 +++------------ .../description | 0 .../state | 0 3 files changed, 3 insertions(+), 12 deletions(-) rename test/0001/{dd79c8cfb8beeacd0460429944b4ecbe95a31561 => dd79c8cfb8beeacd0460429944b4ecbe}/description (100%) rename test/0001/{dd79c8cfb8beeacd0460429944b4ecbe95a31561 => dd79c8cfb8beeacd0460429944b4ecbe}/state (100%) diff --git a/src/issues.rs b/src/issues.rs index bd8d687..e8b759f 100644 --- a/src/issues.rs +++ b/src/issues.rs @@ -172,28 +172,19 @@ mod tests { dir, }); - let uuid = String::from("dd79c8cfb8beeacd0460429944b4ecbe95a31561"); + let uuid = String::from("dd79c8cfb8beeacd0460429944b4ecbe"); let mut dir = std::path::PathBuf::from(issues_dir); dir.push(&uuid); let mut comment_dir = dir.clone(); let comment_uuid = String::from("9055dac36045fe36545bed7ae7b49347"); comment_dir.push("comments"); comment_dir.push(&comment_uuid); - let mut expected_comments = Vec::::new(); - expected_comments.push( - crate::comment::Comment { - uuid: comment_uuid, - author: String::from("Sebastian Kuzminsky "), - 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), - } - ); + let expected_comments = Vec::::new(); expected.add_issue( crate::issue::Issue { id: uuid, author: String::from("Sebastian Kuzminsky "), - creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-03T11:59:44-06:00") + creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-24T10:08:24-06:00") .unwrap() .with_timezone(&chrono::Local), done_time: None, diff --git a/test/0001/dd79c8cfb8beeacd0460429944b4ecbe95a31561/description b/test/0001/dd79c8cfb8beeacd0460429944b4ecbe/description similarity index 100% rename from test/0001/dd79c8cfb8beeacd0460429944b4ecbe95a31561/description rename to test/0001/dd79c8cfb8beeacd0460429944b4ecbe/description diff --git a/test/0001/dd79c8cfb8beeacd0460429944b4ecbe95a31561/state b/test/0001/dd79c8cfb8beeacd0460429944b4ecbe/state similarity index 100% rename from test/0001/dd79c8cfb8beeacd0460429944b4ecbe95a31561/state rename to test/0001/dd79c8cfb8beeacd0460429944b4ecbe/state From 598f4e5df838618584ca81e1edceb9d6791b228e Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Thu, 24 Jul 2025 10:08:38 -0600 Subject: [PATCH 33/36] test dir cleanup: rename test/0001/dd79c8cfb8beeacd0460429944b4ecbe comment Renaming everything also means they have new creation-times, since we're now git logging a different file/dir. --- src/comment.rs | 9 +++++---- src/issues.rs | 11 ++++++++++- .../9055dac36045fe36545bed7ae7b49347/description | 3 +++ .../9055dac36045fe36545bed7ae7b49347/description | 3 --- 4 files changed, 18 insertions(+), 8 deletions(-) create mode 100644 test/0001/dd79c8cfb8beeacd0460429944b4ecbe/comments/9055dac36045fe36545bed7ae7b49347/description delete mode 100644 test/0001/dd79c8cfb8beeacd0460429944b4ecbe95a31561/comments/9055dac36045fe36545bed7ae7b49347/description diff --git a/src/comment.rs b/src/comment.rs index 9770d65..1fa2e36 100644 --- a/src/comment.rs +++ b/src/comment.rs @@ -212,16 +212,17 @@ mod tests { #[test] fn read_comment_0() { - let comment_dir = - std::path::Path::new("test/0001/dd79c8cfb8beeacd0460429944b4ecbe95a31561/comments/9055dac36045fe36545bed7ae7b49347"); + let comment_dir = std::path::Path::new( + "test/0001/dd79c8cfb8beeacd0460429944b4ecbe/comments/9055dac36045fe36545bed7ae7b49347", + ); let comment = Comment::new_from_dir(comment_dir).unwrap(); let expected = Comment { uuid: String::from("9055dac36045fe36545bed7ae7b49347"), author: String::from("Sebastian Kuzminsky "), - creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-07T15:26:26-06:00") + creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-24T10:08:38-06:00") .unwrap() .with_timezone(&chrono::Local), - description: String::from("This is a comment on issue dd79c8cfb8beeacd0460429944b4ecbe95a31561\n\nIt has multiple lines\n"), + description: String::from("This is a comment on issue dd79c8cfb8beeacd0460429944b4ecbe\n\nIt has multiple lines\n"), dir: std::path::PathBuf::from(comment_dir), }; assert_eq!(comment, expected); diff --git a/src/issues.rs b/src/issues.rs index e8b759f..8dca0cd 100644 --- a/src/issues.rs +++ b/src/issues.rs @@ -179,7 +179,16 @@ mod tests { let comment_uuid = String::from("9055dac36045fe36545bed7ae7b49347"); comment_dir.push("comments"); comment_dir.push(&comment_uuid); - let expected_comments = Vec::::new(); + let mut expected_comments = Vec::::new(); + expected_comments.push( + crate::comment::Comment { + uuid: comment_uuid, + author: String::from("Sebastian Kuzminsky "), + creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-24T10:08:38-06:00").unwrap().with_timezone(&chrono::Local), + description: String::from("This is a comment on issue dd79c8cfb8beeacd0460429944b4ecbe\n\nIt has multiple lines\n"), + dir: std::path::PathBuf::from(comment_dir), + } + ); expected.add_issue( crate::issue::Issue { id: uuid, diff --git a/test/0001/dd79c8cfb8beeacd0460429944b4ecbe/comments/9055dac36045fe36545bed7ae7b49347/description b/test/0001/dd79c8cfb8beeacd0460429944b4ecbe/comments/9055dac36045fe36545bed7ae7b49347/description new file mode 100644 index 0000000..daa3d62 --- /dev/null +++ b/test/0001/dd79c8cfb8beeacd0460429944b4ecbe/comments/9055dac36045fe36545bed7ae7b49347/description @@ -0,0 +1,3 @@ +This is a comment on issue dd79c8cfb8beeacd0460429944b4ecbe + +It has multiple lines diff --git a/test/0001/dd79c8cfb8beeacd0460429944b4ecbe95a31561/comments/9055dac36045fe36545bed7ae7b49347/description b/test/0001/dd79c8cfb8beeacd0460429944b4ecbe95a31561/comments/9055dac36045fe36545bed7ae7b49347/description deleted file mode 100644 index f9de678..0000000 --- a/test/0001/dd79c8cfb8beeacd0460429944b4ecbe95a31561/comments/9055dac36045fe36545bed7ae7b49347/description +++ /dev/null @@ -1,3 +0,0 @@ -This is a comment on issue dd79c8cfb8beeacd0460429944b4ecbe95a31561 - -It has multiple lines From b3f5aaeb76652989c82cb9d2d655e8fec23cc81d Mon Sep 17 00:00:00 2001 From: sigil-03 Date: Thu, 24 Jul 2025 08:38:40 -0600 Subject: [PATCH 34/36] test dir cleanup: rename test/0002/3fa5bfd93317ad25772680071d5ac325 Renaming everything also means they have new creation-times, since we're now git logging a different file/dir. --- src/issues.rs | 4 ++-- .../description | 0 .../state | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename test/0002/{3fa5bfd93317ad25772680071d5ac3259cd2384f => 3fa5bfd93317ad25772680071d5ac325}/description (100%) rename test/0002/{3fa5bfd93317ad25772680071d5ac3259cd2384f => 3fa5bfd93317ad25772680071d5ac325}/state (100%) diff --git a/src/issues.rs b/src/issues.rs index 8dca0cd..e991880 100644 --- a/src/issues.rs +++ b/src/issues.rs @@ -216,13 +216,13 @@ mod tests { let mut expected = Issues::new(); - let uuid = String::from("3fa5bfd93317ad25772680071d5ac3259cd2384f"); + let uuid = String::from("3fa5bfd93317ad25772680071d5ac325"); let mut dir = std::path::PathBuf::from(issues_dir); dir.push(&uuid); expected.add_issue(crate::issue::Issue { id: uuid, author: String::from("sigil-03 "), - creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-05T13:55:49-06:00") + creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-24T08:38:40-06:00") .unwrap() .with_timezone(&chrono::Local), done_time: None, diff --git a/test/0002/3fa5bfd93317ad25772680071d5ac3259cd2384f/description b/test/0002/3fa5bfd93317ad25772680071d5ac325/description similarity index 100% rename from test/0002/3fa5bfd93317ad25772680071d5ac3259cd2384f/description rename to test/0002/3fa5bfd93317ad25772680071d5ac325/description diff --git a/test/0002/3fa5bfd93317ad25772680071d5ac3259cd2384f/state b/test/0002/3fa5bfd93317ad25772680071d5ac325/state similarity index 100% rename from test/0002/3fa5bfd93317ad25772680071d5ac3259cd2384f/state rename to test/0002/3fa5bfd93317ad25772680071d5ac325/state From b3903a9ed2e25214239ae796878c2ba9adc559bd Mon Sep 17 00:00:00 2001 From: sigil-03 Date: Thu, 24 Jul 2025 08:39:02 -0600 Subject: [PATCH 35/36] test dir cleanup: rename test/0002/a85f81fc5f14cb5d4851dd445dc9744c Renaming everything also means they have new creation-times, since we're now git logging a different file/dir. --- src/issues.rs | 8 ++++---- .../dependencies/3fa5bfd93317ad25772680071d5ac325} | 0 .../dependencies/dd79c8cfb8beeacd0460429944b4ecbe} | 0 .../description | 0 .../state | 0 5 files changed, 4 insertions(+), 4 deletions(-) rename test/0002/{a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7/dependencies/3fa5bfd93317ad25772680071d5ac3259cd2384f => a85f81fc5f14cb5d4851dd445dc9744c/dependencies/3fa5bfd93317ad25772680071d5ac325} (100%) rename test/0002/{a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7/dependencies/dd79c8cfb8beeacd0460429944b4ecbe95a31561 => a85f81fc5f14cb5d4851dd445dc9744c/dependencies/dd79c8cfb8beeacd0460429944b4ecbe} (100%) rename test/0002/{a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7 => a85f81fc5f14cb5d4851dd445dc9744c}/description (100%) rename test/0002/{a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7 => a85f81fc5f14cb5d4851dd445dc9744c}/state (100%) diff --git a/src/issues.rs b/src/issues.rs index e991880..f54fe87 100644 --- a/src/issues.rs +++ b/src/issues.rs @@ -256,22 +256,22 @@ mod tests { }, ); - let uuid = String::from("a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7"); + let uuid = String::from("a85f81fc5f14cb5d4851dd445dc9744c"); let mut dir = std::path::PathBuf::from(issues_dir); dir.push(&uuid); expected.add_issue( crate::issue::Issue { id: uuid, author: String::from("sigil-03 "), - creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-05T13:55:49-06:00") + creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-24T08:39:02-06:00") .unwrap() .with_timezone(&chrono::Local), done_time: None, tags: Vec::::new(), state: crate::issue::State::WontDo, dependencies: Some(vec![ - crate::issue::IssueHandle::from("3fa5bfd93317ad25772680071d5ac3259cd2384f"), - crate::issue::IssueHandle::from("dd79c8cfb8beeacd0460429944b4ecbe95a31561"), + crate::issue::IssueHandle::from("3fa5bfd93317ad25772680071d5ac325"), + crate::issue::IssueHandle::from("dd79c8cfb8beeacd0460429944b4ecbe"), ]), assignee: None, description: String::from("issue with dependencies\n\na test has begun\nfor dependencies we seek\nintertwining life"), diff --git a/test/0002/a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7/dependencies/3fa5bfd93317ad25772680071d5ac3259cd2384f b/test/0002/a85f81fc5f14cb5d4851dd445dc9744c/dependencies/3fa5bfd93317ad25772680071d5ac325 similarity index 100% rename from test/0002/a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7/dependencies/3fa5bfd93317ad25772680071d5ac3259cd2384f rename to test/0002/a85f81fc5f14cb5d4851dd445dc9744c/dependencies/3fa5bfd93317ad25772680071d5ac325 diff --git a/test/0002/a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7/dependencies/dd79c8cfb8beeacd0460429944b4ecbe95a31561 b/test/0002/a85f81fc5f14cb5d4851dd445dc9744c/dependencies/dd79c8cfb8beeacd0460429944b4ecbe similarity index 100% rename from test/0002/a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7/dependencies/dd79c8cfb8beeacd0460429944b4ecbe95a31561 rename to test/0002/a85f81fc5f14cb5d4851dd445dc9744c/dependencies/dd79c8cfb8beeacd0460429944b4ecbe diff --git a/test/0002/a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7/description b/test/0002/a85f81fc5f14cb5d4851dd445dc9744c/description similarity index 100% rename from test/0002/a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7/description rename to test/0002/a85f81fc5f14cb5d4851dd445dc9744c/description diff --git a/test/0002/a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7/state b/test/0002/a85f81fc5f14cb5d4851dd445dc9744c/state similarity index 100% rename from test/0002/a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7/state rename to test/0002/a85f81fc5f14cb5d4851dd445dc9744c/state From fad23ba233adc90a34175281552c5eadace9f661 Mon Sep 17 00:00:00 2001 From: sigil-03 Date: Thu, 24 Jul 2025 08:39:20 -0600 Subject: [PATCH 36/36] test dir cleanup: rename test/0002/dd79c8cfb8beeacd0460429944b4ecbe Renaming everything also means they have new creation-times, since we're now git logging a different file/dir. --- src/issues.rs | 4 ++-- .../description | 0 .../state | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename test/0002/{dd79c8cfb8beeacd0460429944b4ecbe95a31561 => dd79c8cfb8beeacd0460429944b4ecbe}/description (100%) rename test/0002/{dd79c8cfb8beeacd0460429944b4ecbe95a31561 => dd79c8cfb8beeacd0460429944b4ecbe}/state (100%) diff --git a/src/issues.rs b/src/issues.rs index f54fe87..d3c57c0 100644 --- a/src/issues.rs +++ b/src/issues.rs @@ -235,14 +235,14 @@ mod tests { dir, }); - let uuid = String::from("dd79c8cfb8beeacd0460429944b4ecbe95a31561"); + let uuid = String::from("dd79c8cfb8beeacd0460429944b4ecbe"); let mut dir = std::path::PathBuf::from(issues_dir); dir.push(&uuid); expected.add_issue( crate::issue::Issue { id: uuid, author: String::from("sigil-03 "), - creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-05T13:55:49-06:00") + creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-24T08:39:20-06:00") .unwrap() .with_timezone(&chrono::Local), done_time: None, diff --git a/test/0002/dd79c8cfb8beeacd0460429944b4ecbe95a31561/description b/test/0002/dd79c8cfb8beeacd0460429944b4ecbe/description similarity index 100% rename from test/0002/dd79c8cfb8beeacd0460429944b4ecbe95a31561/description rename to test/0002/dd79c8cfb8beeacd0460429944b4ecbe/description diff --git a/test/0002/dd79c8cfb8beeacd0460429944b4ecbe95a31561/state b/test/0002/dd79c8cfb8beeacd0460429944b4ecbe/state similarity index 100% rename from test/0002/dd79c8cfb8beeacd0460429944b4ecbe95a31561/state rename to test/0002/dd79c8cfb8beeacd0460429944b4ecbe/state