give Comment a timestamp, display in chronological order #11

Merged
seb merged 2 commits from sort-comments-chronologically into main 2025-07-08 20:58:43 -06:00
6 changed files with 67 additions and 23 deletions

View file

@ -9,6 +9,7 @@ log = ["dep:log", "dep:simple_logger"]
[dependencies] [dependencies]
anyhow = "1.0.95" anyhow = "1.0.95"
chrono = "0.4.41"
clap = { version = "4.5.26", features = ["derive"] } clap = { version = "4.5.26", features = ["derive"] }
log = { version = "0.4.27", optional = true } log = { version = "0.4.27", optional = true }
rand = "0.9.1" rand = "0.9.1"

View file

@ -112,9 +112,10 @@ fn handle_command(args: &Args, issues_dir: &std::path::Path) -> anyhow::Result<(
} }
println!(""); println!("");
println!("{}", issue.description); println!("{}", issue.description);
for (uuid, comment) in issue.comments.iter() { for comment in &issue.comments {
println!(""); println!("");
println!("comment: {}", uuid); println!("comment: {}", comment.uuid);
println!("timestamp: {}", comment.timestamp);
println!("{}", comment.description); println!("{}", comment.description);
} }
} }

View file

@ -2,6 +2,8 @@ use std::io::Write;
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub struct Comment { pub struct Comment {
pub uuid: String,
pub timestamp: chrono::DateTime<chrono::Local>,
pub description: String, pub description: String,
/// This is the directory that the comment lives in. Only used /// This is the directory that the comment lives in. Only used
@ -39,12 +41,16 @@ impl Comment {
} }
} }
} }
if description == None { if description == None {
return Err(CommentError::CommentParseError); return Err(CommentError::CommentParseError);
} }
let timestamp = crate::git::git_log_oldest_timestamp(comment_dir)?;
let dir = std::path::PathBuf::from(comment_dir);
Ok(Self { Ok(Self {
uuid: String::from(dir.file_name().unwrap().to_string_lossy()),
timestamp,
description: description.unwrap(), description: description.unwrap(),
dir: std::path::PathBuf::from(comment_dir), dir: std::path::PathBuf::from(comment_dir),
}) })
@ -95,8 +101,11 @@ mod tests {
std::path::Path::new("test/0001/dd79c8cfb8beeacd0460429944b4ecbe95a31561/comments/9055dac36045fe36545bed7ae7b49347"); std::path::Path::new("test/0001/dd79c8cfb8beeacd0460429944b4ecbe95a31561/comments/9055dac36045fe36545bed7ae7b49347");
let comment = Comment::new_from_dir(comment_dir).unwrap(); let comment = Comment::new_from_dir(comment_dir).unwrap();
let expected = Comment { let expected = Comment {
uuid: String::from("9055dac36045fe36545bed7ae7b49347"),
timestamp: 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"), description: String::from("This is a comment on issue dd79c8cfb8beeacd0460429944b4ecbe95a31561\n\nIt has multiple lines\n"),
dir: std::path::PathBuf::from(comment_dir), dir: std::path::PathBuf::from(comment_dir),
}; };
assert_eq!(comment, expected); assert_eq!(comment, expected);

View file

@ -4,6 +4,8 @@ use std::io::Write;
pub enum GitError { pub enum GitError {
#[error(transparent)] #[error(transparent)]
StdIoError(#[from] std::io::Error), StdIoError(#[from] std::io::Error),
#[error(transparent)]
ParseIntError(#[from] std::num::ParseIntError),
#[error("Oops, something went wrong")] #[error("Oops, something went wrong")]
Oops, Oops,
} }
@ -180,6 +182,34 @@ pub fn sync(dir: &std::path::Path, remote: &str, branch: &str) -> Result<(), Git
Ok(()) Ok(())
} }
pub fn git_log_oldest_timestamp(
path: &std::path::Path,
) -> Result<chrono::DateTime<chrono::Local>, 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",
"--",
&path.file_name().unwrap().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());
return Err(GitError::Oops);
}
let timestamp_str = std::str::from_utf8(&result.stdout).unwrap();
let timestamp_last = timestamp_str.split("\n").last().unwrap();
let timestamp_i64 = timestamp_last.parse::<i64>()?;
let timestamp = chrono::DateTime::from_timestamp(timestamp_i64, 0)
.unwrap()
.with_timezone(&chrono::Local);
Ok(timestamp)
}
pub fn create_orphan_branch(branch: &str) -> Result<(), GitError> { pub fn create_orphan_branch(branch: &str) -> Result<(), GitError> {
{ {
let tmp_worktree = tempfile::tempdir().unwrap(); let tmp_worktree = tempfile::tempdir().unwrap();

View file

@ -23,7 +23,7 @@ pub struct Issue {
pub description: String, pub description: String,
pub state: State, pub state: State,
pub dependencies: Option<Vec<IssueHandle>>, pub dependencies: Option<Vec<IssueHandle>>,
pub comments: std::collections::HashMap<String, crate::comment::Comment>, pub comments: Vec<crate::comment::Comment>,
/// This is the directory that the issue lives in. Only used /// This is the directory that the issue lives in. Only used
/// internally by the entomologist library. /// internally by the entomologist library.
@ -87,7 +87,7 @@ impl Issue {
let mut description: Option<String> = None; let mut description: Option<String> = None;
let mut state = State::New; // default state, if not specified in the issue let mut state = State::New; // default state, if not specified in the issue
let mut dependencies: Option<Vec<String>> = None; let mut dependencies: Option<Vec<String>> = None;
let mut comments = std::collections::HashMap::<String, crate::comment::Comment>::new(); let mut comments = Vec::<crate::comment::Comment>::new();
for direntry in dir.read_dir()? { for direntry in dir.read_dir()? {
if let Ok(direntry) = direntry { if let Ok(direntry) = direntry {
@ -129,16 +129,16 @@ impl Issue {
} }
fn read_comments( fn read_comments(
comments: &mut std::collections::HashMap<String, crate::comment::Comment>, comments: &mut Vec<crate::comment::Comment>,
dir: &std::path::Path, dir: &std::path::Path,
) -> Result<(), IssueError> { ) -> Result<(), IssueError> {
for direntry in dir.read_dir()? { for direntry in dir.read_dir()? {
if let Ok(direntry) = direntry { if let Ok(direntry) = direntry {
let uuid = direntry.file_name();
let comment = crate::comment::Comment::new_from_dir(&direntry.path())?; let comment = crate::comment::Comment::new_from_dir(&direntry.path())?;
comments.insert(String::from(uuid.to_string_lossy()), comment); comments.push(comment);
} }
} }
comments.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
Ok(()) Ok(())
} }
@ -150,10 +150,13 @@ impl Issue {
} }
let rnd: u128 = rand::random(); let rnd: u128 = rand::random();
dir.push(&format!("{:032x}", rnd)); let uuid = format!("{:032x}", rnd);
dir.push(&uuid);
std::fs::create_dir(&dir)?; std::fs::create_dir(&dir)?;
Ok(crate::comment::Comment { Ok(crate::comment::Comment {
uuid,
timestamp: chrono::Local::now(),
description: String::from(""), // FIXME description: String::from(""), // FIXME
dir, dir,
}) })
@ -168,7 +171,7 @@ impl Issue {
description: String::from(""), // FIXME: kind of bogus to use the empty string as None description: String::from(""), // FIXME: kind of bogus to use the empty string as None
state: State::New, state: State::New,
dependencies: None, dependencies: None,
comments: std::collections::HashMap::<String, crate::comment::Comment>::new(), comments: Vec::<crate::comment::Comment>::new(),
dir: issue_dir, dir: issue_dir,
}) })
} }
@ -244,7 +247,7 @@ mod tests {
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"),
state: State::New, state: State::New,
dependencies: None, dependencies: None,
comments: std::collections::HashMap::<String, crate::comment::Comment>::new(), comments: Vec::<crate::comment::Comment>::new(),
dir: std::path::PathBuf::from(issue_dir), dir: std::path::PathBuf::from(issue_dir),
}; };
assert_eq!(issue, expected); assert_eq!(issue, expected);
@ -258,7 +261,7 @@ mod tests {
description: String::from("minimal"), description: String::from("minimal"),
state: State::InProgress, state: State::InProgress,
dependencies: None, dependencies: None,
comments: std::collections::HashMap::<String, crate::comment::Comment>::new(), comments: Vec::<crate::comment::Comment>::new(),
dir: std::path::PathBuf::from(issue_dir), dir: std::path::PathBuf::from(issue_dir),
}; };
assert_eq!(issue, expected); assert_eq!(issue, expected);

View file

@ -99,7 +99,7 @@ mod tests {
description: String::from("minimal"), description: String::from("minimal"),
state: crate::issue::State::InProgress, state: crate::issue::State::InProgress,
dependencies: None, dependencies: None,
comments: std::collections::HashMap::<String, crate::comment::Comment>::new(), comments: Vec::<crate::comment::Comment>::new(),
dir, dir,
}, },
); );
@ -113,7 +113,7 @@ mod tests {
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"),
state: crate::issue::State::New, state: crate::issue::State::New,
dependencies: None, dependencies: None,
comments: std::collections::HashMap::<String, crate::comment::Comment>::new(), comments: Vec::<crate::comment::Comment>::new(),
dir, dir,
} }
); );
@ -136,7 +136,7 @@ mod tests {
description: String::from("oh yeah we got titles"), description: String::from("oh yeah we got titles"),
state: crate::issue::State::Done, state: crate::issue::State::Done,
dependencies: None, dependencies: None,
comments: std::collections::HashMap::<String, crate::comment::Comment>::new(), comments: Vec::<crate::comment::Comment>::new(),
dir, dir,
}, },
); );
@ -148,12 +148,12 @@ mod tests {
let comment_uuid = String::from("9055dac36045fe36545bed7ae7b49347"); let comment_uuid = String::from("9055dac36045fe36545bed7ae7b49347");
comment_dir.push("comments"); comment_dir.push("comments");
comment_dir.push(&comment_uuid); comment_dir.push(&comment_uuid);
let mut expected_comments = let mut expected_comments = Vec::<crate::comment::Comment>::new();
std::collections::HashMap::<String, crate::comment::Comment>::new(); expected_comments.push(
expected_comments.insert(
String::from(&comment_uuid),
crate::comment::Comment { crate::comment::Comment {
uuid: comment_uuid,
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 dd79c8cfb8beeacd0460429944b4ecbe95a31561\n\nIt has multiple lines\n"),
timestamp: chrono::DateTime::parse_from_rfc3339("2025-07-07T15:26:26-06:00").unwrap().with_timezone(&chrono::Local),
dir: std::path::PathBuf::from(comment_dir), dir: std::path::PathBuf::from(comment_dir),
} }
); );
@ -186,7 +186,7 @@ mod tests {
description: String::from("oh yeah we got titles\n"), description: String::from("oh yeah we got titles\n"),
state: crate::issue::State::Done, state: crate::issue::State::Done,
dependencies: None, dependencies: None,
comments: std::collections::HashMap::<String, crate::comment::Comment>::new(), comments: Vec::<crate::comment::Comment>::new(),
dir, dir,
}, },
); );
@ -200,7 +200,7 @@ mod tests {
description: String::from("issues out the wazoo\n\nLots of words\nthat don't say much\nbecause this is just\na test\n"), description: String::from("issues out the wazoo\n\nLots of words\nthat don't say much\nbecause this is just\na test\n"),
state: crate::issue::State::WontDo, state: crate::issue::State::WontDo,
dependencies: None, dependencies: None,
comments: std::collections::HashMap::<String, crate::comment::Comment>::new(), comments: Vec::<crate::comment::Comment>::new(),
dir, dir,
}, },
); );
@ -217,7 +217,7 @@ mod tests {
crate::issue::IssueHandle::from("3fa5bfd93317ad25772680071d5ac3259cd2384f"), crate::issue::IssueHandle::from("3fa5bfd93317ad25772680071d5ac3259cd2384f"),
crate::issue::IssueHandle::from("dd79c8cfb8beeacd0460429944b4ecbe95a31561"), crate::issue::IssueHandle::from("dd79c8cfb8beeacd0460429944b4ecbe95a31561"),
]), ]),
comments: std::collections::HashMap::<String, crate::comment::Comment>::new(), comments: Vec::<crate::comment::Comment>::new(),
dir, dir,
}, },
); );