refactor ent comment

Instead of a two-step process handled by the application
(`Issue::new_comment()` and `Comment::set_description()` or
`Comment::edit_description()`), make a simpler-to-use single-step
`Issue::add_comment()`.

Move the implementation details from Issue to Comment.

Better log message when adding a comment.
This commit is contained in:
Sebastian Kuzminsky 2025-07-11 11:48:39 -06:00
parent 1c8d994fd9
commit d642004ee0
3 changed files with 111 additions and 60 deletions

View file

@ -327,21 +327,21 @@ fn handle_command(
let Some(issue) = issues.get_mut_issue(issue_id) else { let Some(issue) = issues.get_mut_issue(issue_id) else {
return Err(anyhow::anyhow!("issue {} not found", issue_id)); return Err(anyhow::anyhow!("issue {} not found", issue_id));
}; };
let mut comment = issue.new_comment()?; match issue.add_comment(description) {
let r = match description { Err(entomologist::issue::IssueError::CommentError(
Some(description) => comment.set_description(description), entomologist::comment::CommentError::EmptyDescription,
None => comment.edit_description(), )) => {
};
match r {
Err(entomologist::comment::CommentError::EmptyDescription) => {
println!("aborted new comment"); println!("aborted new comment");
return Ok(()); return Ok(());
} }
Err(e) => { Err(e) => {
return Err(e.into()); return Err(e.into());
} }
Ok(()) => { Ok(comment) => {
println!("created new comment {}", &comment.uuid); println!(
"created new comment {} on issue {}",
&comment.uuid, &issue_id
);
} }
} }
} }

View file

@ -16,6 +16,8 @@ pub struct Comment {
pub enum CommentError { pub enum CommentError {
#[error(transparent)] #[error(transparent)]
StdIoError(#[from] std::io::Error), StdIoError(#[from] std::io::Error),
#[error(transparent)]
EnvVarError(#[from] std::env::VarError),
#[error("Failed to parse comment")] #[error("Failed to parse comment")]
CommentParseError, CommentParseError,
#[error("Failed to run git")] #[error("Failed to run git")]
@ -61,17 +63,56 @@ impl Comment {
}) })
} }
pub fn set_description(&mut self, description: &str) -> Result<(), CommentError> { /// Create a new Comment on the specified Issue. Commits.
pub fn new(
issue: &crate::issue::Issue,
description: &Option<String>,
) -> Result<crate::comment::Comment, CommentError> {
let mut dir = std::path::PathBuf::from(&issue.dir);
dir.push("comments");
if !dir.exists() {
std::fs::create_dir(&dir)?;
}
let rnd: u128 = rand::random();
let uuid = format!("{:032x}", rnd);
dir.push(&uuid);
std::fs::create_dir(&dir)?;
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(),
description: String::from(""), // this will be set immediately below
dir: dir.clone(),
};
match description {
Some(description) => {
if description.len() == 0 { if description.len() == 0 {
return Err(CommentError::EmptyDescription); return Err(CommentError::EmptyDescription);
} }
self.description = String::from(description); comment.description = String::from(description);
let mut description_filename = std::path::PathBuf::from(&self.dir); let description_filename = comment.description_filename();
description_filename.push("description");
let mut description_file = std::fs::File::create(&description_filename)?; let mut description_file = std::fs::File::create(&description_filename)?;
write!(description_file, "{}", description)?; write!(description_file, "{}", description)?;
crate::git::git_commit_file(&description_filename)?; }
Ok(()) None => comment.edit_description_file()?,
};
crate::git::add(&dir)?;
if crate::git::worktree_is_dirty(&dir.to_string_lossy())? {
crate::git::commit(
&dir,
&format!(
"add comment {} on issue {}",
comment.uuid,
issue.dir.file_name().unwrap().to_string_lossy(),
),
)?;
}
Ok(comment)
} }
pub fn read_description(&mut self) -> Result<(), CommentError> { pub fn read_description(&mut self) -> Result<(), CommentError> {
@ -82,11 +123,39 @@ impl Comment {
} }
pub fn edit_description(&mut self) -> Result<(), CommentError> { pub fn edit_description(&mut self) -> Result<(), CommentError> {
let mut description_filename = std::path::PathBuf::from(&self.dir); self.edit_description_file()?;
description_filename.push("description"); 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 comment {} on issue FIXME", // FIXME: name the issue that the comment is on
self.dir.file_name().unwrap().to_string_lossy()
),
)?;
self.read_description()?;
}
Ok(())
}
/// Opens the Comment's `description` file in an editor. Validates
/// the editor's exit code. Updates the Comment's internal
/// description from what the user saved in the file.
///
/// Used by Issue::add_comment() when no description is supplied,
/// and (FIXME: in the future) used by `ent edit COMMENT`.
pub fn edit_description_file(&mut self) -> Result<(), CommentError> {
let description_filename = self.description_filename();
let exists = description_filename.exists(); let exists = description_filename.exists();
let result = std::process::Command::new("vi")
.arg(&description_filename.as_mut_os_str()) let editor = match std::env::var("EDITOR") {
Ok(editor) => editor,
Err(std::env::VarError::NotPresent) => String::from("vi"),
Err(e) => return Err(e.into()),
};
let result = std::process::Command::new(editor)
.arg(&description_filename.as_os_str())
.spawn()? .spawn()?
.wait_with_output()?; .wait_with_output()?;
if !result.status.success() { if !result.status.success() {
@ -94,9 +163,8 @@ impl Comment {
println!("stderr: {}", std::str::from_utf8(&result.stderr).unwrap()); println!("stderr: {}", std::str::from_utf8(&result.stderr).unwrap());
return Err(CommentError::EditorError); return Err(CommentError::EditorError);
} }
if description_filename.exists() && description_filename.metadata()?.len() > 0 {
crate::git::add(&description_filename)?; if !description_filename.exists() || description_filename.metadata()?.len() == 0 {
} else {
// User saved an empty file, which means they changed their // User saved an empty file, which means they changed their
// mind and no longer want to edit the description. // mind and no longer want to edit the description.
if exists { if exists {
@ -104,25 +172,20 @@ impl Comment {
} }
return Err(CommentError::EmptyDescription); return Err(CommentError::EmptyDescription);
} }
if crate::git::worktree_is_dirty(&self.dir.to_string_lossy())? {
crate::git::commit(
&description_filename.parent().unwrap(),
&format!(
"new description for comment {}",
description_filename
.parent()
.unwrap()
.file_name()
.unwrap()
.to_string_lossy()
),
)?;
self.read_description()?; self.read_description()?;
}
Ok(()) Ok(())
} }
} }
// This is the private, internal API.
impl Comment {
fn description_filename(&self) -> std::path::PathBuf {
let mut description_filename = std::path::PathBuf::from(&self.dir);
description_filename.push("description");
description_filename
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View file

@ -161,25 +161,13 @@ impl Issue {
Ok(()) Ok(())
} }
pub fn new_comment(&mut self) -> Result<crate::comment::Comment, IssueError> { /// Add a new Comment to the Issue. Commits.
let mut dir = std::path::PathBuf::from(&self.dir); pub fn add_comment(
dir.push("comments"); &mut self,
if !dir.exists() { description: &Option<String>,
std::fs::create_dir(&dir)?; ) -> Result<crate::comment::Comment, IssueError> {
} let comment = crate::comment::Comment::new(self, description)?;
Ok(comment)
let rnd: u128 = rand::random();
let uuid = format!("{:032x}", rnd);
dir.push(&uuid);
std::fs::create_dir(&dir)?;
Ok(crate::comment::Comment {
uuid,
author: String::from("Sebastian Kuzminsky <seb@highlab.com>"),
timestamp: chrono::Local::now(),
description: String::from(""), // FIXME
dir,
})
} }
/// Create a new Issue in an Issues database specified by a directory. /// Create a new Issue in an Issues database specified by a directory.