diff --git a/src/bin/ent/main.rs b/src/bin/ent/main.rs index 9c31c60..1c9ddaa 100644 --- a/src/bin/ent/main.rs +++ b/src/bin/ent/main.rs @@ -39,6 +39,12 @@ enum Commands { issue_id: String, new_state: Option, }, + + /// Create a new comment on an issue. + Comment { + issue_id: String, + description: Option, + }, } fn handle_command(args: &Args, issues_dir: &std::path::Path) -> anyhow::Result<()> { @@ -90,6 +96,11 @@ fn handle_command(args: &Args, issues_dir: &std::path::Path) -> anyhow::Result<( } println!(""); println!("{}", issue.description); + for (uuid, comment) in issue.comments.iter() { + println!(""); + println!("comment: {}", uuid); + println!("{}", comment.description); + } } None => { return Err(anyhow::anyhow!("issue {} not found", issue_id)); @@ -123,6 +134,26 @@ fn handle_command(args: &Args, issues_dir: &std::path::Path) -> anyhow::Result<( } } } + + Commands::Comment { + issue_id, + description, + } => { + let mut issues = + entomologist::issues::Issues::new_from_dir(std::path::Path::new(issues_dir))?; + let Some(issue) = issues.get_mut_issue(issue_id) else { + return Err(anyhow::anyhow!("issue {} not found", issue_id)); + }; + let mut comment = issue.new_comment()?; + match description { + Some(description) => { + comment.set_description(description)?; + } + None => { + comment.edit_description()?; + } + } + } } Ok(()) diff --git a/src/comment.rs b/src/comment.rs new file mode 100644 index 0000000..ab2ced9 --- /dev/null +++ b/src/comment.rs @@ -0,0 +1,104 @@ +use std::io::Write; + +#[derive(Debug, PartialEq)] +pub struct Comment { + pub description: String, + + /// This is the directory that the comment lives in. Only used + /// internally by the entomologist library. + pub dir: std::path::PathBuf, +} + +#[derive(Debug, thiserror::Error)] +pub enum CommentError { + #[error(transparent)] + StdIoError(#[from] std::io::Error), + #[error("Failed to parse comment")] + CommentParseError, + #[error("Failed to run git")] + GitError(#[from] crate::git::GitError), + #[error("Failed to run editor")] + EditorError, +} + +impl Comment { + pub fn new_from_dir(comment_dir: &std::path::Path) -> Result { + let mut description: Option = None; + + for direntry in comment_dir.read_dir()? { + if let Ok(direntry) = direntry { + let file_name = direntry.file_name(); + if file_name == "description" { + description = Some(std::fs::read_to_string(direntry.path())?); + } else { + #[cfg(feature = "log")] + debug!( + "ignoring unknown file in comment directory: {:?}", + file_name + ); + } + } + } + + if description == None { + return Err(CommentError::CommentParseError); + } + + Ok(Self { + description: description.unwrap(), + dir: std::path::PathBuf::from(comment_dir), + }) + } + + pub fn set_description(&mut self, description: &str) -> Result<(), CommentError> { + self.description = String::from(description); + let mut description_filename = std::path::PathBuf::from(&self.dir); + description_filename.push("description"); + let mut description_file = std::fs::File::create(&description_filename)?; + write!(description_file, "{}", description)?; + crate::git::git_commit_file(&description_filename)?; + Ok(()) + } + + pub fn read_description(&mut self) -> Result<(), CommentError> { + let mut description_filename = std::path::PathBuf::from(&self.dir); + description_filename.push("description"); + self.description = std::fs::read_to_string(description_filename)?; + Ok(()) + } + + pub fn edit_description(&mut self) -> Result<(), CommentError> { + let mut description_filename = std::path::PathBuf::from(&self.dir); + description_filename.push("description"); + let result = std::process::Command::new("vi") + .arg(&description_filename.as_mut_os_str()) + .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()); + return Err(CommentError::EditorError); + } + crate::git::git_commit_file(&description_filename)?; + self.read_description()?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn read_comment_0() { + let comment_dir = + std::path::Path::new("test/0001/dd79c8cfb8beeacd0460429944b4ecbe95a31561/comments/9055dac36045fe36545bed7ae7b49347"); + let comment = Comment::new_from_dir(comment_dir).unwrap(); + let expected = Comment { + description: String::from("This is a comment on issue dd79c8cfb8beeacd0460429944b4ecbe95a31561\n\nIt has multiple lines\n"), + + dir: std::path::PathBuf::from(comment_dir), + }; + assert_eq!(comment, expected); + } +} diff --git a/src/issue.rs b/src/issue.rs index f931be3..c762c82 100644 --- a/src/issue.rs +++ b/src/issue.rs @@ -23,6 +23,7 @@ pub struct Issue { pub description: String, pub state: State, pub dependencies: Option>, + pub comments: std::collections::HashMap, /// This is the directory that the issue lives in. Only used /// internally by the entomologist library. @@ -33,6 +34,8 @@ pub struct Issue { pub enum IssueError { #[error(transparent)] StdIoError(#[from] std::io::Error), + #[error(transparent)] + CommentError(#[from] crate::comment::CommentError), #[error("Failed to parse issue")] IssueParseError, #[error("Failed to run git")] @@ -83,6 +86,7 @@ impl Issue { let mut description: Option = None; let mut state = State::New; // default state, if not specified in the issue let mut dependencies: Option> = None; + let mut comments = std::collections::HashMap::::new(); for direntry in dir.read_dir()? { if let Ok(direntry) = direntry { @@ -101,6 +105,8 @@ impl Issue { if deps.len() > 0 { dependencies = Some(deps); } + } else if file_name == "comments" && direntry.metadata()?.is_dir() { + Self::read_comments(&mut comments, &direntry.path())?; } else { #[cfg(feature = "log")] debug!("ignoring unknown file in issue directory: {:?}", file_name); @@ -116,10 +122,43 @@ impl Issue { description: description.unwrap(), state: state, dependencies, + comments, dir: std::path::PathBuf::from(dir), }) } + fn read_comments( + comments: &mut std::collections::HashMap, + dir: &std::path::Path, + ) -> Result<(), IssueError> { + for direntry in dir.read_dir()? { + if let Ok(direntry) = direntry { + let uuid = direntry.file_name(); + let comment = crate::comment::Comment::new_from_dir(&direntry.path())?; + comments.insert(String::from(uuid.to_string_lossy()), comment); + } + } + Ok(()) + } + + pub fn new_comment(&mut self) -> Result { + let mut dir = std::path::PathBuf::from(&self.dir); + dir.push("comments"); + if !dir.exists() { + println!("creating {}", dir.to_string_lossy()); + std::fs::create_dir(&dir)?; + } + + let rnd: u128 = rand::random(); + dir.push(&format!("{:032x}", rnd)); + std::fs::create_dir(&dir)?; + + Ok(crate::comment::Comment { + description: String::from(""), // FIXME + dir, + }) + } + pub fn new(dir: &std::path::Path) -> Result { let mut issue_dir = std::path::PathBuf::from(dir); let rnd: u128 = rand::random(); @@ -129,6 +168,7 @@ impl Issue { description: String::from(""), // FIXME: kind of bogus to use the empty string as None state: State::New, dependencies: None, + comments: std::collections::HashMap::::new(), dir: issue_dir, }) } @@ -204,6 +244,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"), state: State::New, dependencies: None, + comments: std::collections::HashMap::::new(), dir: std::path::PathBuf::from(issue_dir), }; assert_eq!(issue, expected); @@ -217,6 +258,7 @@ mod tests { description: String::from("minimal"), state: State::InProgress, dependencies: None, + comments: std::collections::HashMap::::new(), dir: std::path::PathBuf::from(issue_dir), }; assert_eq!(issue, expected); diff --git a/src/issues.rs b/src/issues.rs index 2e40930..28df2e9 100644 --- a/src/issues.rs +++ b/src/issues.rs @@ -99,6 +99,7 @@ mod tests { description: String::from("minimal"), state: crate::issue::State::InProgress, dependencies: None, + comments: std::collections::HashMap::::new(), dir, }, ); @@ -112,6 +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"), state: crate::issue::State::New, dependencies: None, + comments: std::collections::HashMap::::new(), dir, } ); @@ -134,6 +136,7 @@ mod tests { description: String::from("oh yeah we got titles"), state: crate::issue::State::Done, dependencies: None, + comments: std::collections::HashMap::::new(), dir, }, ); @@ -141,12 +144,26 @@ mod tests { let uuid = String::from("dd79c8cfb8beeacd0460429944b4ecbe95a31561"); 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 = + std::collections::HashMap::::new(); + expected_comments.insert( + String::from(&comment_uuid), + crate::comment::Comment { + description: String::from("This is a comment on issue dd79c8cfb8beeacd0460429944b4ecbe95a31561\n\nIt has multiple lines\n"), + dir: std::path::PathBuf::from(comment_dir), + } + ); expected.add_issue( uuid, crate::issue::Issue { 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, dependencies: None, + comments: expected_comments, dir, }, ); @@ -169,6 +186,7 @@ mod tests { description: String::from("oh yeah we got titles\n"), state: crate::issue::State::Done, dependencies: None, + comments: std::collections::HashMap::::new(), dir, }, ); @@ -182,6 +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"), state: crate::issue::State::WontDo, dependencies: None, + comments: std::collections::HashMap::::new(), dir, }, ); @@ -198,6 +217,7 @@ mod tests { crate::issue::IssueHandle::from("3fa5bfd93317ad25772680071d5ac3259cd2384f"), crate::issue::IssueHandle::from("dd79c8cfb8beeacd0460429944b4ecbe95a31561"), ]), + comments: std::collections::HashMap::::new(), dir, }, ); diff --git a/src/lib.rs b/src/lib.rs index cca1c90..e129eff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +pub mod comment; pub mod git; pub mod issue; pub mod issues; diff --git a/test/0001/dd79c8cfb8beeacd0460429944b4ecbe95a31561/comments/9055dac36045fe36545bed7ae7b49347/description b/test/0001/dd79c8cfb8beeacd0460429944b4ecbe95a31561/comments/9055dac36045fe36545bed7ae7b49347/description new file mode 100644 index 0000000..f9de678 --- /dev/null +++ b/test/0001/dd79c8cfb8beeacd0460429944b4ecbe95a31561/comments/9055dac36045fe36545bed7ae7b49347/description @@ -0,0 +1,3 @@ +This is a comment on issue dd79c8cfb8beeacd0460429944b4ecbe95a31561 + +It has multiple lines