diff --git a/src/bin/ent/main.rs b/src/bin/ent/main.rs index d2b5445..8493cf4 100644 --- a/src/bin/ent/main.rs +++ b/src/bin/ent/main.rs @@ -216,7 +216,12 @@ fn handle_command( Commands::New { description } => { let issues_database = make_issues_database(issues_database_source, IssuesDatabaseAccess::ReadWrite)?; - match entomologist::issue::Issue::new(&issues_database.dir, description) { + let mut issue = entomologist::issue::Issue::new(&issues_database.dir)?; + let r = match description { + Some(description) => issue.set_description(description), + None => issue.edit_description(), + }; + match r { Err(entomologist::issue::IssueError::EmptyDescription) => { println!("no new issue created"); return Ok(()); @@ -224,7 +229,7 @@ fn handle_command( Err(e) => { return Err(e.into()); } - Ok(issue) => { + Ok(()) => { println!("created new issue '{}'", issue.title()); return Ok(()); } @@ -327,21 +332,21 @@ fn handle_command( let Some(issue) = issues.get_mut_issue(issue_id) else { return Err(anyhow::anyhow!("issue {} not found", issue_id)); }; - match issue.add_comment(description) { - Err(entomologist::issue::IssueError::CommentError( - entomologist::comment::CommentError::EmptyDescription, - )) => { + let mut comment = issue.new_comment()?; + let r = match description { + Some(description) => comment.set_description(description), + None => comment.edit_description(), + }; + match r { + Err(entomologist::comment::CommentError::EmptyDescription) => { println!("aborted new comment"); return Ok(()); } Err(e) => { return Err(e.into()); } - Ok(comment) => { - println!( - "created new comment {} on issue {}", - &comment.uuid, &issue_id - ); + Ok(()) => { + println!("created new comment {}", &comment.uuid); } } } @@ -363,32 +368,31 @@ fn handle_command( issue_id, new_assignee, } => { - let issues = read_issues_database(issues_database_source)?; - let Some(original_issue) = issues.issues.get(issue_id) else { + let issues_database = + make_issues_database(issues_database_source, IssuesDatabaseAccess::ReadWrite)?; + let mut issues = entomologist::issues::Issues::new_from_dir(&issues_database.dir)?; + let Some(issue) = issues.issues.get_mut(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 = make_issues_database( - issues_database_source, - 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)); - }; + match (&issue.assignee, new_assignee) { + (Some(old_assignee), Some(new_assignee)) => { + println!("issue: {}", issue_id); println!("assignee: {} -> {}", old_assignee, new_assignee); issue.set_assignee(new_assignee)?; } - None => { + (Some(old_assignee), None) => { + println!("issue: {}", issue_id); println!("assignee: {}", old_assignee); } + (None, Some(new_assignee)) => { + println!("issue: {}", issue_id); + println!("assignee: None -> {}", new_assignee); + issue.set_assignee(new_assignee)?; + } + (None, None) => { + println!("issue: {}", issue_id); + println!("assignee: None"); + } } } } diff --git a/src/comment.rs b/src/comment.rs index e9c3134..c7afe44 100644 --- a/src/comment.rs +++ b/src/comment.rs @@ -16,8 +16,6 @@ pub struct Comment { pub enum CommentError { #[error(transparent)] StdIoError(#[from] std::io::Error), - #[error(transparent)] - EnvVarError(#[from] std::env::VarError), #[error("Failed to parse comment")] CommentParseError, #[error("Failed to run git")] @@ -63,56 +61,17 @@ impl Comment { }) } - /// Create a new Comment on the specified Issue. Commits. - pub fn new( - issue: &crate::issue::Issue, - description: &Option, - ) -> Result { - let mut dir = std::path::PathBuf::from(&issue.dir); - dir.push("comments"); - if !dir.exists() { - std::fs::create_dir(&dir)?; + pub fn set_description(&mut self, description: &str) -> Result<(), CommentError> { + if description.len() == 0 { + return Err(CommentError::EmptyDescription); } - - 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 { - return Err(CommentError::EmptyDescription); - } - comment.description = String::from(description); - let description_filename = comment.description_filename(); - let mut description_file = std::fs::File::create(&description_filename)?; - write!(description_file, "{}", description)?; - } - 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) + 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> { @@ -123,39 +82,11 @@ impl Comment { } pub fn edit_description(&mut self) -> Result<(), CommentError> { - 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 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 mut description_filename = std::path::PathBuf::from(&self.dir); + description_filename.push("description"); let exists = description_filename.exists(); - - 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()) + let result = std::process::Command::new("vi") + .arg(&description_filename.as_mut_os_str()) .spawn()? .wait_with_output()?; if !result.status.success() { @@ -163,8 +94,9 @@ impl Comment { println!("stderr: {}", std::str::from_utf8(&result.stderr).unwrap()); return Err(CommentError::EditorError); } - - if !description_filename.exists() || description_filename.metadata()?.len() == 0 { + if description_filename.exists() && description_filename.metadata()?.len() > 0 { + crate::git::add(&description_filename)?; + } else { // User saved an empty file, which means they changed their // mind and no longer want to edit the description. if exists { @@ -172,20 +104,25 @@ impl Comment { } return Err(CommentError::EmptyDescription); } - self.read_description()?; + 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()?; + } 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)] mod tests { use super::*; diff --git a/src/git.rs b/src/git.rs index 63cb004..0ea0ee6 100644 --- a/src/git.rs +++ b/src/git.rs @@ -233,62 +233,6 @@ pub fn sync(dir: &std::path::Path, remote: &str, branch: &str) -> Result<(), Git git_fetch(dir, remote)?; - // FIXME: Possible things to add: - // * `git log -p` shows diff - // * `git log --numstat` shows machine-readable diffstat - - // Show what we just fetched from the remote. - let result = std::process::Command::new("git") - .args([ - "log", - "--no-merges", - "--pretty=format:%an: %s", - &format!("{}/{}", remote, branch), - &format!("^{}", branch), - ]) - .current_dir(dir) - .output()?; - if !result.status.success() { - println!( - "Sync failed! 'git log' error! Help, a human needs to fix the mess in {:?}", - dir - ); - println!("stdout: {}", std::str::from_utf8(&result.stdout).unwrap()); - println!("stderr: {}", std::str::from_utf8(&result.stderr).unwrap()); - return Err(GitError::Oops); - } - if result.stdout.len() > 0 { - println!("Changes fetched from remote {}:", remote); - println!("{}", std::str::from_utf8(&result.stdout).unwrap()); - println!(""); - } - - // Show what we are about to push to the remote. - let result = std::process::Command::new("git") - .args([ - "log", - "--no-merges", - "--pretty=format:%an: %s", - &format!("{}", branch), - &format!("^{}/{}", remote, branch), - ]) - .current_dir(dir) - .output()?; - if !result.status.success() { - println!( - "Sync failed! 'git log' error! Help, a human needs to fix the mess in {:?}", - dir - ); - println!("stdout: {}", std::str::from_utf8(&result.stdout).unwrap()); - println!("stderr: {}", std::str::from_utf8(&result.stderr).unwrap()); - 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!(""); - } - // Merge remote branch into local. let result = std::process::Command::new("git") .args(["merge", &format!("{}/{}", remote, branch)]) diff --git a/src/issue.rs b/src/issue.rs index 15d97f0..3ea6afe 100644 --- a/src/issue.rs +++ b/src/issue.rs @@ -38,8 +38,6 @@ pub enum IssueError { #[error(transparent)] StdIoError(#[from] std::io::Error), #[error(transparent)] - EnvVarError(#[from] std::env::VarError), - #[error(transparent)] CommentError(#[from] crate::comment::CommentError), #[error("Failed to parse issue")] IssueParseError, @@ -89,7 +87,6 @@ impl fmt::Display for State { } } -// This is the public API of Issue. impl Issue { pub fn new_from_dir(dir: &std::path::Path) -> Result { let mut description: Option = None; @@ -161,35 +158,33 @@ impl Issue { Ok(()) } - /// Add a new Comment to the Issue. Commits. - pub fn add_comment( - &mut self, - description: &Option, - ) -> Result { - let comment = crate::comment::Comment::new(self, description)?; - Ok(comment) + pub fn new_comment(&mut self) -> Result { + let mut dir = std::path::PathBuf::from(&self.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)?; + + Ok(crate::comment::Comment { + uuid, + author: String::from("Sebastian Kuzminsky "), + timestamp: chrono::Local::now(), + description: String::from(""), // FIXME + dir, + }) } - /// Create a new Issue in an Issues database specified by a directory. - /// The new Issue will live in a new subdirectory, named by a unique - /// Issue identifier. - /// - /// If a description string is supplied, the new Issue's description - /// will be initialized from it with no user interaction. - /// - /// If no description is supplied, the user will be prompted to - /// input one into an editor. - /// - /// On success, the new Issue with its valid description is committed - /// to the Issues database. - pub fn new(dir: &std::path::Path, description: &Option) -> Result { + pub fn new(dir: &std::path::Path) -> Result { let mut issue_dir = std::path::PathBuf::from(dir); let rnd: u128 = rand::random(); - let issue_id = format!("{:032x}", rnd); - issue_dir.push(&issue_id); + issue_dir.push(&format!("{:032x}", rnd)); std::fs::create_dir(&issue_dir)?; - - let mut issue = Self { + Ok(Self { author: String::from(""), timestamp: chrono::Local::now(), state: State::New, @@ -197,38 +192,58 @@ impl Issue { assignee: None, description: String::from(""), // FIXME: kind of bogus to use the empty string as None comments: Vec::::new(), - dir: issue_dir.clone(), - }; - - match description { - Some(description) => { - if description.len() == 0 { - return Err(IssueError::EmptyDescription); - } - issue.description = String::from(description); - let description_filename = issue.description_filename(); - let mut description_file = std::fs::File::create(&description_filename)?; - write!(description_file, "{}", description)?; - } - None => issue.edit_description_file()?, - }; - - crate::git::add(&issue_dir)?; - crate::git::commit(&issue_dir, &format!("create new issue {}", issue_id))?; - - Ok(issue) + dir: issue_dir, + }) + } + + pub fn set_description(&mut self, description: &str) -> Result<(), IssueError> { + if description.len() == 0 { + return Err(IssueError::EmptyDescription); + } + 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<(), IssueError> { + let mut description_filename = std::path::PathBuf::from(&self.dir); + description_filename.push("description"); + self.description = std::fs::read_to_string(description_filename)?; + Ok(()) } - /// Interactively edit the description of an existing Issue. pub fn edit_description(&mut self) -> Result<(), IssueError> { - self.edit_description_file()?; - let description_filename = self.description_filename(); - crate::git::add(&description_filename)?; + let mut description_filename = std::path::PathBuf::from(&self.dir); + description_filename.push("description"); + let exists = description_filename.exists(); + 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(IssueError::EditorError); + } + if description_filename.exists() && description_filename.metadata()?.len() > 0 { + crate::git::add(&description_filename)?; + } else { + // User saved an empty file, which means they changed their + // mind and no longer want to edit the description. + if exists { + crate::git::restore_file(&description_filename)?; + } + return Err(IssueError::EmptyDescription); + } if crate::git::worktree_is_dirty(&self.dir.to_string_lossy())? { crate::git::commit( &description_filename.parent().unwrap(), &format!( - "edit description of issue {}", + "new description for issue {}", description_filename .parent() .unwrap() @@ -237,11 +252,11 @@ impl Issue { .to_string_lossy() ), )?; + self.read_description()?; } Ok(()) } - /// Return the Issue title (first line of the description). pub fn title<'a>(&'a self) -> &'a str { match self.description.find("\n") { Some(index) => &self.description.as_str()[..index], @@ -249,23 +264,12 @@ impl Issue { } } - /// Change the State of the Issue. pub fn set_state(&mut self, new_state: State) -> Result<(), IssueError> { let mut state_filename = std::path::PathBuf::from(&self.dir); state_filename.push("state"); let mut state_file = std::fs::File::create(&state_filename)?; write!(state_file, "{}", new_state)?; - crate::git::add(&state_filename)?; - if crate::git::worktree_is_dirty(&self.dir.to_string_lossy())? { - crate::git::commit( - &self.dir, - &format!( - "change state of issue {} to {}", - self.dir.file_name().unwrap().to_string_lossy(), - new_state, - ), - )?; - } + crate::git::git_commit_file(&state_filename)?; Ok(()) } @@ -277,82 +281,12 @@ impl Issue { Ok(()) } - /// Set the Assignee of an Issue. pub fn set_assignee(&mut self, new_assignee: &str) -> Result<(), IssueError> { - let old_assignee = match &self.assignee { - Some(assignee) => assignee.clone(), - None => String::from("None"), - }; let mut assignee_filename = std::path::PathBuf::from(&self.dir); 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, - ), - )?; - } - Ok(()) - } -} - -// This is the internal/private API of Issue. -impl Issue { - fn description_filename(&self) -> std::path::PathBuf { - let mut description_filename = std::path::PathBuf::from(&self.dir); - description_filename.push("description"); - description_filename - } - - /// Read the Issue's description file into the internal Issue representation. - fn read_description(&mut self) -> Result<(), IssueError> { - let description_filename = self.description_filename(); - self.description = std::fs::read_to_string(description_filename)?; - Ok(()) - } - - /// Opens the Issue's `description` file in an editor. Validates the - /// editor's exit code. Updates the Issue's internal description - /// from what the user saved in the file. - /// - /// Used by Issue::new() when no description is supplied, and also - /// used by `ent edit ISSUE`. - fn edit_description_file(&mut self) -> Result<(), IssueError> { - let description_filename = self.description_filename(); - let exists = description_filename.exists(); - 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()? - .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(IssueError::EditorError); - } - if !description_filename.exists() || description_filename.metadata()?.len() == 0 { - // User saved an empty file, or exited without saving while - // editing a new description file. Both means they changed - // their mind and no longer want to edit the description. - if exists { - // File existed before the user emptied it, so restore - // the original. - crate::git::restore_file(&description_filename)?; - } - return Err(IssueError::EmptyDescription); - } - self.read_description()?; + crate::git::git_commit_file(&assignee_filename)?; Ok(()) } }