From 1f4456fcaf4723810de70421bb52368cb2f079d4 Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Sat, 5 Jul 2025 23:34:24 -0600 Subject: [PATCH 1/6] teach Issue to know what dir it lives in The Issue struct is a cache of files on disk. There is never an Issue without a directory to live in. This commit adds a field to Issue to track what that directory is, so that we can update those filew when we change the Issue, and commit the changes to git. --- src/issue.rs | 7 +++++++ src/issues.rs | 49 ++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/src/issue.rs b/src/issue.rs index 939af47..0f3dfd2 100644 --- a/src/issue.rs +++ b/src/issue.rs @@ -18,6 +18,10 @@ pub struct Issue { pub description: String, pub state: State, pub dependencies: Option>, + + /// This is the directory that the issue lives in. Only used + /// internally by the entomologist library. + pub dir: std::path::PathBuf, } #[derive(Debug, thiserror::Error)] @@ -87,6 +91,7 @@ impl Issue { description: description.unwrap(), state: state, dependencies, + dir: std::path::PathBuf::from(dir), }) } @@ -110,6 +115,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, + dir: std::path::PathBuf::from(issue_dir), }; assert_eq!(issue, expected); } @@ -122,6 +128,7 @@ mod tests { description: String::from("minimal"), state: State::InProgress, dependencies: None, + dir: std::path::PathBuf::from(issue_dir), }; assert_eq!(issue, expected); } diff --git a/src/issues.rs b/src/issues.rs index 9422ded..0bfda84 100644 --- a/src/issues.rs +++ b/src/issues.rs @@ -77,20 +77,30 @@ mod tests { let issues = Issues::new_from_dir(issues_dir).unwrap(); let mut expected = Issues::new(); + + let uuid = String::from("7792b063eef6d33e7da5dc1856750c149ba678c6"); + let mut dir = std::path::PathBuf::from(issues_dir); + dir.push(&uuid); expected.add_issue( - String::from("7792b063eef6d33e7da5dc1856750c149ba678c6"), + uuid, crate::issue::Issue { description: String::from("minimal"), state: crate::issue::State::InProgress, dependencies: None, + dir, }, ); + + let uuid = String::from("3943fc5c173fdf41c0a22251593cd476d96e6c9f"); + let mut dir = std::path::PathBuf::from(issues_dir); + dir.push(&uuid); expected.add_issue( - String::from("3943fc5c173fdf41c0a22251593cd476d96e6c9f"), + uuid, crate::issue::Issue { 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, + dir, } ); assert_eq!(issues, expected); @@ -102,20 +112,30 @@ mod tests { let issues = Issues::new_from_dir(issues_dir).unwrap(); let mut expected = Issues::new(); + + let uuid = String::from("3fa5bfd93317ad25772680071d5ac3259cd2384f"); + let mut dir = std::path::PathBuf::from(issues_dir); + dir.push(&uuid); expected.add_issue( - String::from("3fa5bfd93317ad25772680071d5ac3259cd2384f"), + uuid, crate::issue::Issue { description: String::from("oh yeah we got titles"), state: crate::issue::State::Done, dependencies: None, + dir, }, ); + + let uuid = String::from("dd79c8cfb8beeacd0460429944b4ecbe95a31561"); + let mut dir = std::path::PathBuf::from(issues_dir); + dir.push(&uuid); expected.add_issue( - String::from("dd79c8cfb8beeacd0460429944b4ecbe95a31561"), + 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, + dir, }, ); assert_eq!(issues, expected); @@ -127,24 +147,38 @@ mod tests { let issues = Issues::new_from_dir(issues_dir).unwrap(); let mut expected = Issues::new(); + + let uuid = String::from("3fa5bfd93317ad25772680071d5ac3259cd2384f"); + let mut dir = std::path::PathBuf::from(issues_dir); + dir.push(&uuid); expected.add_issue( - String::from("3fa5bfd93317ad25772680071d5ac3259cd2384f"), + uuid, crate::issue::Issue { description: String::from("oh yeah we got titles\n"), state: crate::issue::State::Done, dependencies: None, + dir, }, ); + + let uuid = String::from("dd79c8cfb8beeacd0460429944b4ecbe95a31561"); + let mut dir = std::path::PathBuf::from(issues_dir); + dir.push(&uuid); expected.add_issue( - String::from("dd79c8cfb8beeacd0460429944b4ecbe95a31561"), + 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, + dir, }, ); + + let uuid = String::from("a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7"); + let mut dir = std::path::PathBuf::from(issues_dir); + dir.push(&uuid); expected.add_issue( - String::from("a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7"), + uuid, crate::issue::Issue { description: String::from("issue with dependencies\n\na test has begun\nfor dependencies we seek\nintertwining life"), state: crate::issue::State::WontDo, @@ -152,6 +186,7 @@ mod tests { crate::issue::IssueHandle::from("3fa5bfd93317ad25772680071d5ac3259cd2384f"), crate::issue::IssueHandle::from("dd79c8cfb8beeacd0460429944b4ecbe95a31561"), ]), + dir, }, ); assert_eq!(issues, expected); From 5b1c7a52b913450bc455385b9f905eb3ec1b2493 Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Sat, 5 Jul 2025 23:51:54 -0600 Subject: [PATCH 2/6] git: add git_commit_file() --- src/git.rs | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/git.rs b/src/git.rs index 2b03b5a..bb17763 100644 --- a/src/git.rs +++ b/src/git.rs @@ -89,6 +89,41 @@ pub fn git_branch_exists(branch: &str) -> Result { return Ok(result.status.success()); } +pub fn git_commit_file(file: &std::path::Path) -> Result<(), GitError> { + let mut git_dir = std::path::PathBuf::from(file); + git_dir.pop(); + + let result = std::process::Command::new("git") + .args(["add", &file.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 result = std::process::Command::new("git") + .args([ + "commit", + "-m", + &format!( + "update '{}' in issue {}", + file.file_name().unwrap().to_string_lossy(), + git_dir.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); + } + + Ok(()) +} + pub fn create_orphan_branch(branch: &str) -> Result<(), GitError> { { let tmp_worktree = tempfile::tempdir().unwrap(); From 5e482edb5c5192efc447817f13172a08667f0fbe Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Sun, 6 Jul 2025 00:11:23 -0600 Subject: [PATCH 3/6] rename ReadIssueError to just IssueError Error handling is pretty broken in this project :-( --- src/issue.rs | 10 +++++----- src/issues.rs | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/issue.rs b/src/issue.rs index 0f3dfd2..753454b 100644 --- a/src/issue.rs +++ b/src/issue.rs @@ -25,7 +25,7 @@ pub struct Issue { } #[derive(Debug, thiserror::Error)] -pub enum ReadIssueError { +pub enum IssueError { #[error(transparent)] StdIoError(#[from] std::io::Error), #[error("Failed to parse issue")] @@ -33,7 +33,7 @@ pub enum ReadIssueError { } impl FromStr for State { - type Err = ReadIssueError; + type Err = IssueError; fn from_str(s: &str) -> Result { let s = s.to_lowercase(); if s == "new" { @@ -49,13 +49,13 @@ impl FromStr for State { } else if s == "wontdo" { Ok(State::WontDo) } else { - Err(ReadIssueError::IssueParseError) + Err(IssueError::IssueParseError) } } } impl Issue { - pub fn new_from_dir(dir: &std::path::Path) -> Result { + pub fn new_from_dir(dir: &std::path::Path) -> Result { let mut description: Option = None; let mut state = State::New; // default state, if not specified in the issue let mut dependencies: Option> = None; @@ -84,7 +84,7 @@ impl Issue { } if description == None { - return Err(ReadIssueError::IssueParseError); + return Err(IssueError::IssueParseError); } Ok(Self { diff --git a/src/issues.rs b/src/issues.rs index 0bfda84..bb548e4 100644 --- a/src/issues.rs +++ b/src/issues.rs @@ -12,8 +12,8 @@ pub struct Issues { pub enum ReadIssuesError { #[error(transparent)] StdIoError(#[from] std::io::Error), - #[error("Failed to parse issue")] - IssueParseError(#[from] crate::issue::ReadIssueError), + #[error(transparent)] + IssueError(#[from] crate::issue::IssueError), #[error("cannot handle filename")] FilenameError(std::ffi::OsString), #[error(transparent)] From 09373cda56ecc1b00478a28a55a1cb7e8a76d341 Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Sun, 6 Jul 2025 00:12:22 -0600 Subject: [PATCH 4/6] add `ent new` --- src/bin/ent/main.rs | 21 ++++++++++-------- src/issue.rs | 52 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 9 deletions(-) diff --git a/src/bin/ent/main.rs b/src/bin/ent/main.rs index 65e36dc..e0dca3c 100644 --- a/src/bin/ent/main.rs +++ b/src/bin/ent/main.rs @@ -22,10 +22,7 @@ enum Commands { List, /// Create a new issue. - New { - title: Option, - description: Option, - }, + New { description: Option }, } fn handle_command(args: &Args, issues_dir: &std::path::Path) -> anyhow::Result<()> { @@ -37,11 +34,17 @@ fn handle_command(args: &Args, issues_dir: &std::path::Path) -> anyhow::Result<( println!("{} {} ({:?})", uuid, issue.title(), issue.state); } } - Commands::New { title, description } => { - println!( - "should make a new issue, title={:?}, description={:?}", - title, description - ); + Commands::New { + description: Some(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()); } } diff --git a/src/issue.rs b/src/issue.rs index 753454b..b91d3c6 100644 --- a/src/issue.rs +++ b/src/issue.rs @@ -1,3 +1,4 @@ +use std::io::Write; use std::str::FromStr; #[derive(Debug, PartialEq, serde::Deserialize)] @@ -30,6 +31,10 @@ pub enum IssueError { StdIoError(#[from] std::io::Error), #[error("Failed to parse issue")] IssueParseError, + #[error("Failed to run git")] + GitError(#[from] crate::git::GitError), + #[error("Failed to run editor")] + EditorError, } impl FromStr for State { @@ -95,6 +100,53 @@ impl Issue { }) } + pub fn new(dir: &std::path::Path) -> Result { + let mut issue_dir = std::path::PathBuf::from(dir); + let rnd: u128 = rand::random(); + issue_dir.push(&format!("{:0x}", rnd)); + std::fs::create_dir(&issue_dir)?; + Ok(Self { + description: String::from(""), // FIXME: kind of bogus to use the empty string as None + state: State::New, + dependencies: None, + dir: issue_dir, + }) + } + + pub fn set_description(&mut self, description: &str) -> Result<(), IssueError> { + 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(()) + } + + pub fn edit_description(&mut self) -> Result<(), IssueError> { + 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(IssueError::EditorError); + } + crate::git::git_commit_file(&description_filename)?; + self.read_description()?; + Ok(()) + } + pub fn title<'a>(&'a self) -> &'a str { match self.description.find("\n") { Some(index) => &self.description.as_str()[..index], From ba0862f5a6d8f59395b5fab2a573118ab8ed4c4e Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Sun, 6 Jul 2025 00:18:36 -0600 Subject: [PATCH 5/6] add `ent edit` --- src/bin/ent/main.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/bin/ent/main.rs b/src/bin/ent/main.rs index e0dca3c..3d6f80b 100644 --- a/src/bin/ent/main.rs +++ b/src/bin/ent/main.rs @@ -23,6 +23,9 @@ enum Commands { /// Create a new issue. New { description: Option }, + + /// Edit the description of an issue. + Edit { issue_id: String }, } fn handle_command(args: &Args, issues_dir: &std::path::Path) -> anyhow::Result<()> { @@ -46,6 +49,18 @@ fn handle_command(args: &Args, issues_dir: &std::path::Path) -> anyhow::Result<( issue.edit_description()?; println!("created new issue '{}'", issue.title()); } + Commands::Edit { issue_id } => { + let mut issues = + entomologist::issues::Issues::new_from_dir(std::path::Path::new(issues_dir))?; + match issues.issues.get_mut(issue_id) { + Some(issue) => { + issue.edit_description()?; + } + None => { + println!("issue {} not found", issue_id); + } + } + } } Ok(()) From 3f2d3b1520a49b50a75b11df0891d771530f69fd Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Sun, 6 Jul 2025 00:21:25 -0600 Subject: [PATCH 6/6] add `ent show` --- src/bin/ent/main.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/bin/ent/main.rs b/src/bin/ent/main.rs index 3d6f80b..3561f4c 100644 --- a/src/bin/ent/main.rs +++ b/src/bin/ent/main.rs @@ -26,6 +26,9 @@ enum Commands { /// Edit the description of an issue. Edit { issue_id: String }, + + /// Show the full description of an issue. + Show { issue_id: String }, } fn handle_command(args: &Args, issues_dir: &std::path::Path) -> anyhow::Result<()> { @@ -61,6 +64,21 @@ fn handle_command(args: &Args, issues_dir: &std::path::Path) -> anyhow::Result<( } } } + Commands::Show { issue_id } => { + let issues = + entomologist::issues::Issues::new_from_dir(std::path::Path::new(issues_dir))?; + match issues.issues.get(issue_id) { + Some(issue) => { + println!("issue {}", issue_id); + println!("state {:?}", issue.state); + println!(""); + println!("{}", issue.description); + } + None => { + println!("issue {} not found", issue_id); + } + } + } } Ok(())