From ca353352f8dced335506ffb4bd839213b64afefb Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Wed, 9 Jul 2025 22:03:49 -0600 Subject: [PATCH 01/10] git::Worktree::drop() now force-drops the worktree This avoids leaving prunable worktrees around if we dirtied the worktree and didn't commit. This can happen in the following situation: 1. User runs `ent new`. 2. ent creates a new directory for the issue. 3. ent opens an editor to let the user type in the description of the new issue. The editor saves to `ISSUE/description`. 4. User changes their mind and no longer wants to make a new issue, so they save an empty buffer and exit the editor. 5. ent sees that the file is empty, and returns an error from Issue::edit_description(). 6. ent propagates the error up through program exit, and eventually the git::Worktree struct is dropped. Since the worktree is dirty (it has the new issue dir with an empty description file in it), `git worktree remove` fails. But `git worktree remove --force` works! --- src/git.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/git.rs b/src/git.rs index 6018392..d83389e 100644 --- a/src/git.rs +++ b/src/git.rs @@ -20,9 +20,24 @@ pub struct Worktree { impl Drop for Worktree { fn drop(&mut self) { - let _result = std::process::Command::new("git") - .args(["worktree", "remove", &self.path.path().to_string_lossy()]) + let result = std::process::Command::new("git") + .args([ + "worktree", + "remove", + "--force", + &self.path.path().to_string_lossy(), + ]) .output(); + match result { + Err(e) => { + println!("failed to run git: {:#?}", e); + } + Ok(result) => { + if !result.status.success() { + println!("failed to remove git worktree: {:#?}", result); + } + } + } } } -- 2.47.3 From 1509c42734e360d32c235f8e54dda5835f52e82b Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Wed, 9 Jul 2025 22:20:37 -0600 Subject: [PATCH 02/10] add git::worktree_is_dirty() This returns Ok(true) if the worktree has any modified files (staged or unstaged), or any added (staged) files. Ok(false) if not. Ignores untracked files. --- src/git.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/git.rs b/src/git.rs index d83389e..54b5735 100644 --- a/src/git.rs +++ b/src/git.rs @@ -106,6 +106,17 @@ pub fn git_branch_exists(branch: &str) -> Result { return Ok(result.status.success()); } +pub fn worktree_is_dirty(dir: &str) -> Result { + // `git status --porcelain` prints a terse list of files added or + // modified (both staged and not), and new untracked files. So if + // says *anything at all* it means the worktree is dirty. + let result = std::process::Command::new("git") + .args(["status", "--porcelain", "--untracked-files=no"]) + .current_dir(dir) + .output()?; + return Ok(result.stdout.len() > 0); +} + pub fn git_commit_file(file: &std::path::Path) -> Result<(), GitError> { let mut git_dir = std::path::PathBuf::from(file); git_dir.pop(); -- 2.47.3 From ac72251e0e5c9fb12116eb1f71379f02d1eb22e2 Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Wed, 9 Jul 2025 22:23:02 -0600 Subject: [PATCH 03/10] add git::restore_file() This restores a file from the index to the worktree. --- src/git.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/git.rs b/src/git.rs index 54b5735..17a00bc 100644 --- a/src/git.rs +++ b/src/git.rs @@ -117,6 +117,19 @@ pub fn worktree_is_dirty(dir: &str) -> Result { return Ok(result.stdout.len() > 0); } +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()) + .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); + } + return Ok(()); +} + pub fn git_commit_file(file: &std::path::Path) -> Result<(), GitError> { let mut git_dir = std::path::PathBuf::from(file); git_dir.pop(); -- 2.47.3 From 16de030b8e64b2b3705995d8ad39e8bb13cd99fe Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Wed, 9 Jul 2025 21:16:28 -0600 Subject: [PATCH 04/10] add git::add_file() --- src/git.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/git.rs b/src/git.rs index 17a00bc..087b13c 100644 --- a/src/git.rs +++ b/src/git.rs @@ -117,6 +117,19 @@ pub fn worktree_is_dirty(dir: &str) -> Result { return Ok(result.stdout.len() > 0); } +pub fn add_file(file: &std::path::Path) -> Result<(), GitError> { + let result = std::process::Command::new("git") + .args(["add", &file.to_string_lossy()]) + .current_dir(file.parent().unwrap()) + .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); + } + return Ok(()); +} + pub fn restore_file(file: &std::path::Path) -> Result<(), GitError> { let result = std::process::Command::new("git") .args(["restore", &file.to_string_lossy()]) -- 2.47.3 From bfdf6178f4dca2d492a9c514f8f7953d89bc1e1b Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Wed, 9 Jul 2025 21:16:37 -0600 Subject: [PATCH 05/10] add git::commit() --- src/git.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/git.rs b/src/git.rs index 087b13c..3374542 100644 --- a/src/git.rs +++ b/src/git.rs @@ -143,6 +143,19 @@ pub fn restore_file(file: &std::path::Path) -> Result<(), GitError> { return Ok(()); } +pub fn commit(dir: &std::path::Path, msg: &str) -> Result<(), GitError> { + let result = std::process::Command::new("git") + .args(["commit", "-m", msg]) + .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()); + return Err(GitError::Oops); + } + Ok(()) +} + pub fn git_commit_file(file: &std::path::Path) -> Result<(), GitError> { let mut git_dir = std::path::PathBuf::from(file); git_dir.pop(); -- 2.47.3 From 211bf92dde1a07dbf15d89285a51c8d3d533d83c Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Wed, 9 Jul 2025 22:21:31 -0600 Subject: [PATCH 06/10] Issue: handle empty description from user This fixes issue a26da230276d317e85f9fcca41c19d2e. --- src/issue.rs | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/issue.rs b/src/issue.rs index 68cb8f9..7201871 100644 --- a/src/issue.rs +++ b/src/issue.rs @@ -47,6 +47,8 @@ pub enum IssueError { GitError(#[from] crate::git::GitError), #[error("Failed to run editor")] EditorError, + #[error("supplied description is empty")] + EmptyDescription, } impl FromStr for State { @@ -195,6 +197,9 @@ impl Issue { } 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"); @@ -214,6 +219,7 @@ impl Issue { pub fn edit_description(&mut self) -> Result<(), IssueError> { 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()? @@ -223,8 +229,31 @@ impl Issue { println!("stderr: {}", std::str::from_utf8(&result.stderr).unwrap()); return Err(IssueError::EditorError); } - crate::git::git_commit_file(&description_filename)?; - self.read_description()?; + if description_filename.exists() && description_filename.metadata()?.len() > 0 { + crate::git::add_file(&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!( + "new description for issue {}", + description_filename + .parent() + .unwrap() + .file_name() + .unwrap() + .to_string_lossy() + ), + )?; + self.read_description()?; + } Ok(()) } -- 2.47.3 From fc658009f5fd51a8987cae057d2690b2278b833c Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Wed, 9 Jul 2025 22:34:55 -0600 Subject: [PATCH 07/10] Comment: handle empty description --- src/comment.rs | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/comment.rs b/src/comment.rs index d6ed66e..e6c95fd 100644 --- a/src/comment.rs +++ b/src/comment.rs @@ -22,6 +22,8 @@ pub enum CommentError { GitError(#[from] crate::git::GitError), #[error("Failed to run editor")] EditorError, + #[error("supplied description is empty")] + EmptyDescription, } impl Comment { @@ -60,6 +62,9 @@ impl Comment { } pub fn set_description(&mut self, description: &str) -> Result<(), CommentError> { + if description.len() == 0 { + return Err(CommentError::EmptyDescription); + } self.description = String::from(description); let mut description_filename = std::path::PathBuf::from(&self.dir); description_filename.push("description"); @@ -79,6 +84,7 @@ impl Comment { pub fn edit_description(&mut self) -> Result<(), CommentError> { 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()? @@ -88,8 +94,31 @@ impl Comment { println!("stderr: {}", std::str::from_utf8(&result.stderr).unwrap()); return Err(CommentError::EditorError); } - crate::git::git_commit_file(&description_filename)?; - self.read_description()?; + if description_filename.exists() && description_filename.metadata()?.len() > 0 { + crate::git::add_file(&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(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()?; + } Ok(()) } } -- 2.47.3 From a199fbc7f71dc0565a6c811735981ec8fc73d829 Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Wed, 9 Jul 2025 22:09:50 -0600 Subject: [PATCH 08/10] handle aborts in `ent edit ISSUE` The user saving an empty description file is a normal user-initiated abort, not an error. --- src/bin/ent/main.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/bin/ent/main.rs b/src/bin/ent/main.rs index 6fb30c9..fa18b2a 100644 --- a/src/bin/ent/main.rs +++ b/src/bin/ent/main.rs @@ -150,9 +150,16 @@ fn handle_command(args: &Args, issues_dir: &std::path::Path) -> anyhow::Result<( let mut issues = entomologist::issues::Issues::new_from_dir(std::path::Path::new(issues_dir))?; match issues.get_mut_issue(issue_id) { - Some(issue) => { - issue.edit_description()?; - } + Some(issue) => match issue.edit_description() { + Err(entomologist::issue::IssueError::EmptyDescription) => { + println!("aborted issue edit"); + return Ok(()); + } + Err(e) => { + return Err(e.into()); + } + Ok(()) => (), + }, None => { return Err(anyhow::anyhow!("issue {} not found", issue_id)); } -- 2.47.3 From acf539c683a6545bd01f866bed0c82fd0fa33efb Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Wed, 9 Jul 2025 22:11:27 -0600 Subject: [PATCH 09/10] handle user abort in `ent comment` The user saving an empty description is a normal user-initiated abort, not an error. --- src/bin/ent/main.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/bin/ent/main.rs b/src/bin/ent/main.rs index fa18b2a..893534b 100644 --- a/src/bin/ent/main.rs +++ b/src/bin/ent/main.rs @@ -234,12 +234,20 @@ fn handle_command(args: &Args, issues_dir: &std::path::Path) -> anyhow::Result<( return Err(anyhow::anyhow!("issue {} not found", issue_id)); }; let mut comment = issue.new_comment()?; - match description { - Some(description) => { - comment.set_description(description)?; + 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(()); } - None => { - comment.edit_description()?; + Err(e) => { + return Err(e.into()); + } + Ok(()) => { + println!("created new comment {}", &comment.uuid); } } } -- 2.47.3 From e09e4b9cb72608c4eb7f83f7bae433d1c0180b3c Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Wed, 9 Jul 2025 22:12:58 -0600 Subject: [PATCH 10/10] simplify `ent new` --- src/bin/ent/main.rs | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/bin/ent/main.rs b/src/bin/ent/main.rs index 893534b..8bc0817 100644 --- a/src/bin/ent/main.rs +++ b/src/bin/ent/main.rs @@ -132,18 +132,24 @@ fn handle_command(args: &Args, issues_dir: &std::path::Path) -> anyhow::Result<( } } - Commands::New { - description: Some(description), - } => { + Commands::New { description } => { let mut issue = entomologist::issue::Issue::new(issues_dir)?; - issue.set_description(description)?; - println!("created new issue '{}'", issue.title()); - } - - Commands::New { description: None } => { - let mut issue = entomologist::issue::Issue::new(issues_dir)?; - issue.edit_description()?; - println!("created new issue '{}'", issue.title()); + 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(()); + } + Err(e) => { + return Err(e.into()); + } + Ok(()) => { + println!("created new issue '{}'", issue.title()); + } + } } Commands::Edit { issue_id } => { -- 2.47.3