From 6eacb405d90eb58a2ed33ade7298afc0b3ce1243 Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Thu, 3 Jul 2025 12:14:26 -0600 Subject: [PATCH 01/13] WIP start adding Issue --- Cargo.toml | 1 + src/issue.rs | 109 ++++++++++++++++++ src/lib.rs | 2 +- .../description | 4 + .../title | 1 + .../state | 1 + .../title | 1 + 7 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 src/issue.rs create mode 100644 test/0000/3943fc5c173fdf41c0a22251593cd476d96e6c9f/description create mode 100644 test/0000/3943fc5c173fdf41c0a22251593cd476d96e6c9f/title create mode 100644 test/0000/7792b063eef6d33e7da5dc1856750c149ba678c6/state create mode 100644 test/0000/7792b063eef6d33e7da5dc1856750c149ba678c6/title diff --git a/Cargo.toml b/Cargo.toml index ba3bd7b..905a9b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,3 +4,4 @@ version = "0.1.0" edition = "2024" [dependencies] +thiserror = "2.0.11" diff --git a/src/issue.rs b/src/issue.rs new file mode 100644 index 0000000..6dde005 --- /dev/null +++ b/src/issue.rs @@ -0,0 +1,109 @@ +use std::str::FromStr; + +#[derive(Debug, PartialEq, serde::Deserialize)] +/// These are the states an issue can be in. +pub enum State { + New, + Backlog, + InProgress, + Done, + WontDo, +} + +#[derive(Debug, PartialEq)] +pub struct Issue { + pub title: String, + pub description: Option, + pub state: State, +} + +#[derive(Debug, thiserror::Error)] +pub enum ReadIssueError { + #[error(transparent)] + StdIoError(#[from] std::io::Error), + #[error("Failed to parse issue")] + IssueParseError, +} + +impl FromStr for State { + type Err = ReadIssueError; + fn from_str(s: &str) -> Result { + let s = s.to_lowercase(); + if s == "new" { + Ok(State::New) + } else if s == "backlog" { + Ok(State::Backlog) + } else if s == "inprogress" { + Ok(State::InProgress) + } else if s == "done" { + Ok(State::Done) + } else if s == "wontdo" { + Ok(State::WontDo) + } else { + Err(ReadIssueError::IssueParseError) + } + } +} + +impl Issue { + pub fn new_from_dir(dir: &std::path::Path) -> Result { + let mut title: Option = None; + let mut description: Option = None; + let mut state = State::New; // default state, if not specified in the issue + + for direntry in dir.read_dir()? { + if let Ok(direntry) = direntry { + let file_name = direntry.file_name(); + if file_name == "title" { + title = Some(std::fs::read_to_string(direntry.path())?.trim().into()); + } else if file_name == "description" { + description = Some(std::fs::read_to_string(direntry.path())?); + } else if file_name == "state" { + let state_string = std::fs::read_to_string(direntry.path())?; + state = State::from_str(state_string.trim())?; + } else { + println!("ignoring unknown file in issue directory: {:?}", file_name); + } + } + } + + if title == None { + return Err(ReadIssueError::IssueParseError); + } + + Ok(Self { + title: title.unwrap(), + description: description, + state: state, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn read_issue_0() { + let issue_dir = std::path::Path::new("test/0000/3943fc5c173fdf41c0a22251593cd476d96e6c9f/"); + let issue = Issue::new_from_dir(issue_dir).unwrap(); + let expected = Issue { + title: String::from("this is the title of my issue"), + description: Some(String::from("This 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, + }; + assert_eq!(issue, expected); + } + + #[test] + fn read_issue_1() { + let issue_dir = std::path::Path::new("test/0000/7792b063eef6d33e7da5dc1856750c149ba678c6/"); + let issue = Issue::new_from_dir(issue_dir).unwrap(); + let expected = Issue { + title: String::from("minimal"), + description: None, + state: State::InProgress, + }; + assert_eq!(issue, expected); + } +} diff --git a/src/lib.rs b/src/lib.rs index 8b13789..d93d369 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1 +1 @@ - +pub mod issue; diff --git a/test/0000/3943fc5c173fdf41c0a22251593cd476d96e6c9f/description b/test/0000/3943fc5c173fdf41c0a22251593cd476d96e6c9f/description new file mode 100644 index 0000000..3db0fcf --- /dev/null +++ b/test/0000/3943fc5c173fdf41c0a22251593cd476d96e6c9f/description @@ -0,0 +1,4 @@ +This is the description of my issue. +It is multiple lines. +* Arbitrary contents +* But let's use markdown by convention diff --git a/test/0000/3943fc5c173fdf41c0a22251593cd476d96e6c9f/title b/test/0000/3943fc5c173fdf41c0a22251593cd476d96e6c9f/title new file mode 100644 index 0000000..c9c2379 --- /dev/null +++ b/test/0000/3943fc5c173fdf41c0a22251593cd476d96e6c9f/title @@ -0,0 +1 @@ +this is the title of my issue diff --git a/test/0000/7792b063eef6d33e7da5dc1856750c149ba678c6/state b/test/0000/7792b063eef6d33e7da5dc1856750c149ba678c6/state new file mode 100644 index 0000000..0737713 --- /dev/null +++ b/test/0000/7792b063eef6d33e7da5dc1856750c149ba678c6/state @@ -0,0 +1 @@ +inprogress diff --git a/test/0000/7792b063eef6d33e7da5dc1856750c149ba678c6/title b/test/0000/7792b063eef6d33e7da5dc1856750c149ba678c6/title new file mode 100644 index 0000000..dd1a932 --- /dev/null +++ b/test/0000/7792b063eef6d33e7da5dc1856750c149ba678c6/title @@ -0,0 +1 @@ +minimal From 561f85fbb059f4fbcf7b82a7ad2b9597886ae99c Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Thu, 3 Jul 2025 11:59:44 -0600 Subject: [PATCH 02/13] WIP start adding Issues struct to hold everything --- Cargo.toml | 2 + src/issues.rs | 125 ++++++++++++++++++ src/lib.rs | 1 + .../state | 1 + .../title | 1 + test/0001/config.toml | 1 + .../description | 4 + .../state | 1 + .../title | 1 + 9 files changed, 137 insertions(+) create mode 100644 src/issues.rs create mode 100644 test/0001/3fa5bfd93317ad25772680071d5ac3259cd2384f/state create mode 100644 test/0001/3fa5bfd93317ad25772680071d5ac3259cd2384f/title create mode 100644 test/0001/config.toml create mode 100644 test/0001/dd79c8cfb8beeacd0460429944b4ecbe95a31561/description create mode 100644 test/0001/dd79c8cfb8beeacd0460429944b4ecbe95a31561/state create mode 100644 test/0001/dd79c8cfb8beeacd0460429944b4ecbe95a31561/title diff --git a/Cargo.toml b/Cargo.toml index 905a9b5..2230bb4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,4 +4,6 @@ version = "0.1.0" edition = "2024" [dependencies] +serde = { version = "1.0.217", features = ["derive"] } thiserror = "2.0.11" +toml = "0.8.19" diff --git a/src/issues.rs b/src/issues.rs new file mode 100644 index 0000000..b5c1c87 --- /dev/null +++ b/src/issues.rs @@ -0,0 +1,125 @@ +// Just a placeholder for now, get rid of this if we don't need it. +#[derive(Debug, PartialEq, serde::Deserialize)] +pub struct Config {} + +#[derive(Debug, PartialEq)] +pub struct Issues { + pub issues: std::collections::HashMap, + pub config: Config, +} + +#[derive(Debug, thiserror::Error)] +pub enum ReadIssuesError { + #[error(transparent)] + StdIoError(#[from] std::io::Error), + #[error("Failed to parse issue")] + IssueParseError(#[from] crate::issue::ReadIssueError), + #[error("cannot handle filename")] + FilenameError(std::ffi::OsString), + #[error(transparent)] + TomlDeserializeError(#[from] toml::de::Error), +} + +impl Issues { + pub fn new() -> Self { + Self { + issues: std::collections::HashMap::new(), + config: Config {}, + } + } + + pub fn add_issue(&mut self, uuid: String, issue: crate::issue::Issue) { + self.issues.insert(uuid, issue); + } + + fn parse_config(&mut self, config_path: &std::path::Path) -> Result<(), ReadIssuesError> { + let config_contents = std::fs::read_to_string(config_path)?; + let config: Config = toml::from_str(&config_contents)?; + self.config = config; + Ok(()) + } + + pub fn new_from_dir(dir: &std::path::Path) -> Result { + let mut issues = Self::new(); + + for direntry in dir.read_dir()? { + if let Ok(direntry) = direntry { + if direntry.metadata()?.is_dir() { + let uuid = match direntry.file_name().into_string() { + Ok(uuid) => uuid, + Err(orig_string) => { + return Err(ReadIssuesError::FilenameError(orig_string)) + } + }; + let issue = crate::issue::Issue::new_from_dir(direntry.path().as_path())?; + issues.add_issue(uuid, issue); + } else if direntry.file_name() == "config.toml" { + issues.parse_config(direntry.path().as_path())?; + } else { + println!( + "ignoring unknown file in issues directory: {:?}", + direntry.file_name() + ); + } + } + } + return Ok(issues); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn read_issues_0000() { + let issues_dir = std::path::Path::new("test/0000/"); + let issues = Issues::new_from_dir(issues_dir).unwrap(); + + let mut expected = Issues::new(); + expected.add_issue( + String::from("7792b063eef6d33e7da5dc1856750c149ba678c6"), + crate::issue::Issue { + title: String::from("minimal"), + description: None, + state: crate::issue::State::InProgress, + }, + ); + expected.add_issue( + String::from("3943fc5c173fdf41c0a22251593cd476d96e6c9f"), + crate::issue::Issue { + title: String::from("this is the title of my issue"), + description: Some(String::from("This 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, + } + ); + assert_eq!(issues, expected); + } + + #[test] + fn read_issues_0001() { + let issues_dir = std::path::Path::new("test/0001/"); + let issues = Issues::new_from_dir(issues_dir).unwrap(); + + let mut expected = Issues::new(); + expected.add_issue( + String::from("3fa5bfd93317ad25772680071d5ac3259cd2384f"), + crate::issue::Issue { + title: String::from("oh yeah we got titles"), + description: None, + state: crate::issue::State::Done, + }, + ); + expected.add_issue( + String::from("dd79c8cfb8beeacd0460429944b4ecbe95a31561"), + crate::issue::Issue { + title: String::from("issues out the wazoo"), + description: Some(String::from( + "Lots of words\nthat don't say much\nbecause this is just\na test\n", + )), + state: crate::issue::State::WontDo, + }, + ); + assert_eq!(issues, expected); + } +} diff --git a/src/lib.rs b/src/lib.rs index d93d369..713acd1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1 +1,2 @@ pub mod issue; +pub mod issues; diff --git a/test/0001/3fa5bfd93317ad25772680071d5ac3259cd2384f/state b/test/0001/3fa5bfd93317ad25772680071d5ac3259cd2384f/state new file mode 100644 index 0000000..19f86f4 --- /dev/null +++ b/test/0001/3fa5bfd93317ad25772680071d5ac3259cd2384f/state @@ -0,0 +1 @@ +done diff --git a/test/0001/3fa5bfd93317ad25772680071d5ac3259cd2384f/title b/test/0001/3fa5bfd93317ad25772680071d5ac3259cd2384f/title new file mode 100644 index 0000000..18a1926 --- /dev/null +++ b/test/0001/3fa5bfd93317ad25772680071d5ac3259cd2384f/title @@ -0,0 +1 @@ +oh yeah we got titles diff --git a/test/0001/config.toml b/test/0001/config.toml new file mode 100644 index 0000000..dfc15f2 --- /dev/null +++ b/test/0001/config.toml @@ -0,0 +1 @@ +states = [ "open", "closed" ] diff --git a/test/0001/dd79c8cfb8beeacd0460429944b4ecbe95a31561/description b/test/0001/dd79c8cfb8beeacd0460429944b4ecbe95a31561/description new file mode 100644 index 0000000..010156b --- /dev/null +++ b/test/0001/dd79c8cfb8beeacd0460429944b4ecbe95a31561/description @@ -0,0 +1,4 @@ +Lots of words +that don't say much +because this is just +a test diff --git a/test/0001/dd79c8cfb8beeacd0460429944b4ecbe95a31561/state b/test/0001/dd79c8cfb8beeacd0460429944b4ecbe95a31561/state new file mode 100644 index 0000000..7f19192 --- /dev/null +++ b/test/0001/dd79c8cfb8beeacd0460429944b4ecbe95a31561/state @@ -0,0 +1 @@ +wontdo diff --git a/test/0001/dd79c8cfb8beeacd0460429944b4ecbe95a31561/title b/test/0001/dd79c8cfb8beeacd0460429944b4ecbe95a31561/title new file mode 100644 index 0000000..ab5b4a9 --- /dev/null +++ b/test/0001/dd79c8cfb8beeacd0460429944b4ecbe95a31561/title @@ -0,0 +1 @@ +issues out the wazoo From 53c5d03ab760c62e3e9cbc56c07d5d97447e3ccc Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Thu, 3 Jul 2025 12:14:33 -0600 Subject: [PATCH 03/13] WIP start adding `ent` binary --- Cargo.toml | 2 ++ src/bin/ent/main.rs | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 src/bin/ent/main.rs diff --git a/Cargo.toml b/Cargo.toml index 2230bb4..c017ff3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,8 @@ version = "0.1.0" edition = "2024" [dependencies] +anyhow = "1.0.95" +clap = { version = "4.5.26", features = ["derive"] } serde = { version = "1.0.217", features = ["derive"] } thiserror = "2.0.11" toml = "0.8.19" diff --git a/src/bin/ent/main.rs b/src/bin/ent/main.rs new file mode 100644 index 0000000..83cb908 --- /dev/null +++ b/src/bin/ent/main.rs @@ -0,0 +1,36 @@ +use clap::Parser; + +#[derive(Debug, clap::Parser)] +#[command(version, about, long_about = None)] +struct Args { + /// Directory containing issues. + #[arg(short, long)] + issues_dir: String, + + /// Type of behavior/output. + #[command(subcommand)] + command: Commands, +} + +#[derive(clap::Subcommand, Debug)] +enum Commands { + /// List issues. + List, +} + +fn main() -> anyhow::Result<()> { + let args: Args = Args::parse(); + // println!("{:?}", args); + + match args.command { + Commands::List => { + let issues = + entomologist::issues::Issues::new_from_dir(std::path::Path::new(&args.issues_dir))?; + for (uuid, issue) in issues.issues.iter() { + println!("{} {} ({:?})", uuid, issue.title, issue.state); + } + } + } + + Ok(()) +} From 2772db19a9cf091056e29c8657dbc23c8c789c20 Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Thu, 3 Jul 2025 16:52:52 -0600 Subject: [PATCH 04/13] WIP ent --- src/bin/ent/main.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/bin/ent/main.rs b/src/bin/ent/main.rs index 83cb908..238bbe3 100644 --- a/src/bin/ent/main.rs +++ b/src/bin/ent/main.rs @@ -16,6 +16,14 @@ struct Args { enum Commands { /// List issues. List, + // Mdbook { + // /// The name of the recipe to create MD Book for. + // target: String, + // }, + // Info { + // /// The name of the recipe to show info for. + // target: String, + // }, } fn main() -> anyhow::Result<()> { From 7bd1773856d09f309cfc42c064408182bf838539 Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Thu, 3 Jul 2025 16:52:52 -0600 Subject: [PATCH 05/13] WIP `ent new` --- src/bin/ent/main.rs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/bin/ent/main.rs b/src/bin/ent/main.rs index 238bbe3..53ecf9d 100644 --- a/src/bin/ent/main.rs +++ b/src/bin/ent/main.rs @@ -16,14 +16,12 @@ struct Args { enum Commands { /// List issues. List, - // Mdbook { - // /// The name of the recipe to create MD Book for. - // target: String, - // }, - // Info { - // /// The name of the recipe to show info for. - // target: String, - // }, + + /// Create a new issue. + New { + title: Option, + description: Option, + }, } fn main() -> anyhow::Result<()> { @@ -38,6 +36,12 @@ fn main() -> anyhow::Result<()> { println!("{} {} ({:?})", uuid, issue.title, issue.state); } } + Commands::New { title, description } => { + println!( + "should make a new issue, title={:?}, description={:?}", + title, description + ); + } } Ok(()) From 5b73a6b34cf73a6c0648a049023dd6c468e9a5a7 Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Fri, 4 Jul 2025 00:51:12 -0600 Subject: [PATCH 06/13] add a Todo file, ironically --- Todo.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 Todo.md diff --git a/Todo.md b/Todo.md new file mode 100644 index 0000000..56728ad --- /dev/null +++ b/Todo.md @@ -0,0 +1,30 @@ +# To do + +* migrate this todo list into entomologist + +* teach it to work with a git branch + - unpack the branch to a directory with `git worktree ${TMPDIR} ${BRANCH}` + - operate on the issues in that worktree + - git commit the result back to ${BRANCH} + - delete and prune the worktree + +* implement `ent new` + +* implement user control over state transitions + +* implement `ent comment ${ISSUE} [-m ${MESSAGE}]` + - each issue dir has a `comments` subdir + - each comment is identified by a sha1-style uid + - each comment is a file or directory under the `${ISSUE}/comments` + - comments are ordered by ctime? + +* implement `ent edit ${ISSUE} [-t ${TITLE}] [-d ${DESCRIPTION}]` + - or would it be better to put the title and description together into a new `message`, like git commits? + +* implement `ent edit ${COMMENT}` + +* implement `ent attach ${ISSUE} ${FILE}` + - each issue has its own independent namespace for attached files + - issue description & comments can reference attached files via standard md links + +* write a manpage From 9d9a30d90a485588151757b528a753812a692eac Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Fri, 4 Jul 2025 13:07:10 -0600 Subject: [PATCH 07/13] WIP start adding git support --- src/git.rs | 47 +++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 2 files changed, 48 insertions(+) create mode 100644 src/git.rs diff --git a/src/git.rs b/src/git.rs new file mode 100644 index 0000000..4fb8ee4 --- /dev/null +++ b/src/git.rs @@ -0,0 +1,47 @@ +#[derive(Debug, thiserror::Error)] +pub enum GitError { + #[error(transparent)] + StdIoError(#[from] std::io::Error), + #[error("Oops, something went wrong")] + Oops, +} + +pub fn checkout_branch_in_worktree( + branch: &str, + worktree_dir: &std::path::Path, +) -> Result<(), GitError> { + let result = std::process::Command::new("git") + .args(["worktree", "add", &worktree_dir.to_string_lossy(), branch]) + .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 remove_worktree(worktree_dir: &std::path::Path) -> Result<(), GitError> { + std::fs::remove_dir_all(worktree_dir)?; + let result = std::process::Command::new("git") + .args(["worktree", "prune"]) + .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(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn worktree() { + let worktree_dir = std::path::Path::new("/tmp/boo"); + checkout_branch_in_worktree("main", worktree_dir).unwrap(); + remove_worktree(worktree_dir).unwrap(); + } +} diff --git a/src/lib.rs b/src/lib.rs index 713acd1..cca1c90 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,2 +1,3 @@ +pub mod git; pub mod issue; pub mod issues; From f467e35ff257c8b38ec895989a8d71b3ac726e81 Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Fri, 4 Jul 2025 13:13:08 -0600 Subject: [PATCH 08/13] WIP git start create_orphan_branch() --- src/git.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/git.rs b/src/git.rs index 4fb8ee4..3ca250f 100644 --- a/src/git.rs +++ b/src/git.rs @@ -34,6 +34,18 @@ pub fn remove_worktree(worktree_dir: &std::path::Path) -> Result<(), GitError> { Ok(()) } +pub fn create_orphan_branch(branch: &str) { + // git checkout --orphan orphan-branch + // git rm -rf . + // git clean -fdx . + // + // echo "hello world" > test-file + // git add test-file + // git commit -m 'first commit, again' + // + // git checkout dev +} + #[cfg(test)] mod tests { use super::*; From 0afdcc4aa9d55db40cb045f11600956866945946 Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Sat, 5 Jul 2025 10:30:07 -0600 Subject: [PATCH 09/13] WIP git::create_orphan_branch() --- Cargo.toml | 1 + src/git.rs | 73 ++++++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 64 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c017ff3..03db057 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" [dependencies] anyhow = "1.0.95" clap = { version = "4.5.26", features = ["derive"] } +mktemp = "0.5.1" serde = { version = "1.0.217", features = ["derive"] } thiserror = "2.0.11" toml = "0.8.19" diff --git a/src/git.rs b/src/git.rs index 3ca250f..adfb147 100644 --- a/src/git.rs +++ b/src/git.rs @@ -1,3 +1,5 @@ +use std::io::Write; + #[derive(Debug, thiserror::Error)] pub enum GitError { #[error(transparent)] @@ -34,16 +36,62 @@ pub fn remove_worktree(worktree_dir: &std::path::Path) -> Result<(), GitError> { Ok(()) } -pub fn create_orphan_branch(branch: &str) { - // git checkout --orphan orphan-branch - // git rm -rf . - // git clean -fdx . - // - // echo "hello world" > test-file - // git add test-file - // git commit -m 'first commit, again' - // - // git checkout dev +pub fn create_orphan_branch(branch: &str) -> Result<(), GitError> { + let tmp_worktree = mktemp::Temp::new_path(); + let tmp_worktree_pathbuf = tmp_worktree.to_path_buf(); + let Some(tmp_worktree_dir) = tmp_worktree_pathbuf.as_path().to_str() else { + return Err(GitError::Oops); + }; + + let result = std::process::Command::new("git") + .args([ + "worktree", + "add", + "--orphan", + "-b", + branch, + tmp_worktree_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 mut readme_filename = tmp_worktree_pathbuf.clone(); + readme_filename.push("README.md"); + println!("readme_filename: {:?}", readme_filename); + println!("tmp_worktree_pathbuf: {:?}", tmp_worktree_pathbuf); + let mut readme = std::fs::File::create(readme_filename)?; + write!( + readme, + "This branch is used by entomologist to track issues." + )?; + + let result = std::process::Command::new("git") + .args(["add", "README.md"]) + .current_dir(tmp_worktree_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", "create entomologist issue branch"]) + .current_dir(tmp_worktree_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); + } + + // git worktree prune + + Ok(()) } #[cfg(test)] @@ -56,4 +104,9 @@ mod tests { checkout_branch_in_worktree("main", worktree_dir).unwrap(); remove_worktree(worktree_dir).unwrap(); } + + #[test] + fn test_create_orphan_branch() { + create_orphan_branch("bloppers").unwrap(); + } } From 4fb4ed4ea30f497c733e4ca63a93743b3d2d7a39 Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Sat, 5 Jul 2025 10:30:35 -0600 Subject: [PATCH 10/13] fixup git::test_worktree() --- src/git.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/git.rs b/src/git.rs index adfb147..f8c5465 100644 --- a/src/git.rs +++ b/src/git.rs @@ -99,9 +99,9 @@ mod tests { use super::*; #[test] - fn worktree() { + fn test_worktree() { let worktree_dir = std::path::Path::new("/tmp/boo"); - checkout_branch_in_worktree("main", worktree_dir).unwrap(); + checkout_branch_in_worktree("origin/main", worktree_dir).unwrap(); remove_worktree(worktree_dir).unwrap(); } From b6787224c39cc2cd441f95879eabf02efda3efec Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Sat, 5 Jul 2025 11:10:38 -0600 Subject: [PATCH 11/13] fixup git --- src/git.rs | 48 ++++++++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/src/git.rs b/src/git.rs index f8c5465..b3e74ad 100644 --- a/src/git.rs +++ b/src/git.rs @@ -37,21 +37,14 @@ pub fn remove_worktree(worktree_dir: &std::path::Path) -> Result<(), GitError> { } pub fn create_orphan_branch(branch: &str) -> Result<(), GitError> { - let tmp_worktree = mktemp::Temp::new_path(); - let tmp_worktree_pathbuf = tmp_worktree.to_path_buf(); - let Some(tmp_worktree_dir) = tmp_worktree_pathbuf.as_path().to_str() else { - return Err(GitError::Oops); - }; + { + let tmp_worktree = mktemp::Temp::new_path(); + create_orphan_branch_at_path(branch, &tmp_worktree.to_path_buf())?; + } + // The temp dir is now removed / cleaned up. let result = std::process::Command::new("git") - .args([ - "worktree", - "add", - "--orphan", - "-b", - branch, - tmp_worktree_dir, - ]) + .args(["worktree", "prune"]) .output()?; if !result.status.success() { println!("stdout: {}", std::str::from_utf8(&result.stdout).unwrap()); @@ -59,10 +52,27 @@ pub fn create_orphan_branch(branch: &str) -> Result<(), GitError> { return Err(GitError::Oops); } - let mut readme_filename = tmp_worktree_pathbuf.clone(); + Ok(()) +} +pub fn create_orphan_branch_at_path( + branch: &str, + worktree_path: &std::path::PathBuf, +) -> Result<(), GitError> { + let Some(worktree_dir) = worktree_path.to_str() else { + return Err(GitError::Oops); + }; + + let result = std::process::Command::new("git") + .args(["worktree", "add", "--orphan", "-b", branch, worktree_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 mut readme_filename = worktree_path.clone(); readme_filename.push("README.md"); - println!("readme_filename: {:?}", readme_filename); - println!("tmp_worktree_pathbuf: {:?}", tmp_worktree_pathbuf); let mut readme = std::fs::File::create(readme_filename)?; write!( readme, @@ -71,7 +81,7 @@ pub fn create_orphan_branch(branch: &str) -> Result<(), GitError> { let result = std::process::Command::new("git") .args(["add", "README.md"]) - .current_dir(tmp_worktree_dir) + .current_dir(worktree_dir) .output()?; if !result.status.success() { println!("stdout: {}", std::str::from_utf8(&result.stdout).unwrap()); @@ -81,7 +91,7 @@ pub fn create_orphan_branch(branch: &str) -> Result<(), GitError> { let result = std::process::Command::new("git") .args(["commit", "-m", "create entomologist issue branch"]) - .current_dir(tmp_worktree_dir) + .current_dir(worktree_path) .output()?; if !result.status.success() { println!("stdout: {}", std::str::from_utf8(&result.stdout).unwrap()); @@ -89,8 +99,6 @@ pub fn create_orphan_branch(branch: &str) -> Result<(), GitError> { return Err(GitError::Oops); } - // git worktree prune - Ok(()) } From ba47a4bcd67cd78b5909b012b08c12498aab3383 Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Sat, 5 Jul 2025 11:39:45 -0600 Subject: [PATCH 12/13] better git testing --- Cargo.toml | 1 + src/git.rs | 30 ++++++++++++++++++++++++------ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 03db057..addd833 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" anyhow = "1.0.95" clap = { version = "4.5.26", features = ["derive"] } mktemp = "0.5.1" +rand = "0.9.1" serde = { version = "1.0.217", features = ["derive"] } thiserror = "2.0.11" toml = "0.8.19" diff --git a/src/git.rs b/src/git.rs index b3e74ad..8d1ce46 100644 --- a/src/git.rs +++ b/src/git.rs @@ -23,8 +23,7 @@ pub fn checkout_branch_in_worktree( Ok(()) } -pub fn remove_worktree(worktree_dir: &std::path::Path) -> Result<(), GitError> { - std::fs::remove_dir_all(worktree_dir)?; +pub fn git_worktree_prune() -> Result<(), GitError> { let result = std::process::Command::new("git") .args(["worktree", "prune"]) .output()?; @@ -36,6 +35,18 @@ pub fn remove_worktree(worktree_dir: &std::path::Path) -> Result<(), GitError> { Ok(()) } +pub fn git_remove_branch(branch: &str) -> Result<(), GitError> { + let result = std::process::Command::new("git") + .args(["branch", "-D", branch]) + .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 = mktemp::Temp::new_path(); @@ -108,13 +119,20 @@ mod tests { #[test] fn test_worktree() { - let worktree_dir = std::path::Path::new("/tmp/boo"); - checkout_branch_in_worktree("origin/main", worktree_dir).unwrap(); - remove_worktree(worktree_dir).unwrap(); + { + let temp_worktree = mktemp::Temp::new_path(); + checkout_branch_in_worktree("origin/main", temp_worktree.as_path()).unwrap(); + // The temporary worktree directory is removed when the Temp variable is dropped. + } + git_worktree_prune().unwrap(); } #[test] fn test_create_orphan_branch() { - create_orphan_branch("bloppers").unwrap(); + let rnd: u128 = rand::random(); + let mut branch = std::string::String::from("entomologist-test-branch-"); + branch.push_str(&format!("{:0x}", rnd)); + create_orphan_branch(&branch).unwrap(); + git_remove_branch(&branch).unwrap(); } } From 3bdaac19c4ae100aa3e1028797c8d36bdc77dc6c Mon Sep 17 00:00:00 2001 From: sigil-03 Date: Sat, 5 Jul 2025 13:55:49 -0600 Subject: [PATCH 13/13] add dependency tracking to issue type --- src/issue.rs | 18 +++++++- src/issues.rs | 45 +++++++++++++++++++ .../state | 1 + .../title | 1 + .../dependencies | 2 + .../description | 3 ++ .../state | 1 + .../title | 1 + test/0002/config.toml | 1 + .../description | 4 ++ .../state | 1 + .../title | 1 + 12 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 test/0002/3fa5bfd93317ad25772680071d5ac3259cd2384f/state create mode 100644 test/0002/3fa5bfd93317ad25772680071d5ac3259cd2384f/title create mode 100644 test/0002/a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7/dependencies create mode 100644 test/0002/a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7/description create mode 100644 test/0002/a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7/state create mode 100644 test/0002/a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7/title create mode 100644 test/0002/config.toml create mode 100644 test/0002/dd79c8cfb8beeacd0460429944b4ecbe95a31561/description create mode 100644 test/0002/dd79c8cfb8beeacd0460429944b4ecbe95a31561/state create mode 100644 test/0002/dd79c8cfb8beeacd0460429944b4ecbe95a31561/title diff --git a/src/issue.rs b/src/issue.rs index 6dde005..7171380 100644 --- a/src/issue.rs +++ b/src/issue.rs @@ -5,16 +5,20 @@ use std::str::FromStr; pub enum State { New, Backlog, + Blocked, InProgress, Done, WontDo, } +pub type IssueHandle = String; + #[derive(Debug, PartialEq)] pub struct Issue { pub title: String, pub description: Option, pub state: State, + pub dependencies: Option>, } #[derive(Debug, thiserror::Error)] @@ -33,6 +37,8 @@ impl FromStr for State { Ok(State::New) } else if s == "backlog" { Ok(State::Backlog) + } else if s == "blocked" { + Ok(State::Blocked) } else if s == "inprogress" { Ok(State::InProgress) } else if s == "done" { @@ -50,6 +56,7 @@ impl Issue { let mut title: Option = None; let mut description: Option = None; let mut state = State::New; // default state, if not specified in the issue + let mut dependencies: Option> = None; for direntry in dir.read_dir()? { if let Ok(direntry) = direntry { @@ -60,7 +67,13 @@ impl Issue { description = Some(std::fs::read_to_string(direntry.path())?); } else if file_name == "state" { let state_string = std::fs::read_to_string(direntry.path())?; - state = State::from_str(state_string.trim())?; + state = State::from_str(state_string.trim())?; + } else if file_name == "dependencies" { + let dep_strings = std::fs::read_to_string(direntry.path())?; + let deps: Vec = dep_strings.lines().map(|dep|{IssueHandle::from(dep)}).collect(); + if deps.len() > 0 { + dependencies = Some(deps); + } } else { println!("ignoring unknown file in issue directory: {:?}", file_name); } @@ -75,6 +88,7 @@ impl Issue { title: title.unwrap(), description: description, state: state, + dependencies, }) } } @@ -91,6 +105,7 @@ mod tests { title: String::from("this is the title of my issue"), description: Some(String::from("This 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, }; assert_eq!(issue, expected); } @@ -103,6 +118,7 @@ mod tests { title: String::from("minimal"), description: None, state: State::InProgress, + dependencies: None, }; assert_eq!(issue, expected); } diff --git a/src/issues.rs b/src/issues.rs index b5c1c87..35116e7 100644 --- a/src/issues.rs +++ b/src/issues.rs @@ -83,6 +83,7 @@ mod tests { title: String::from("minimal"), description: None, state: crate::issue::State::InProgress, + dependencies: None, }, ); expected.add_issue( @@ -91,6 +92,7 @@ mod tests { title: String::from("this is the title of my issue"), description: Some(String::from("This 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, } ); assert_eq!(issues, expected); @@ -108,6 +110,7 @@ mod tests { title: String::from("oh yeah we got titles"), description: None, state: crate::issue::State::Done, + dependencies: None, }, ); expected.add_issue( @@ -118,8 +121,50 @@ mod tests { "Lots of words\nthat don't say much\nbecause this is just\na test\n", )), state: crate::issue::State::WontDo, + dependencies: None, + }, + ); + assert_eq!(issues, expected); + } + + #[test] + fn read_issues_0002() { + let issues_dir = std::path::Path::new("test/0002/"); + let issues = Issues::new_from_dir(issues_dir).unwrap(); + + let mut expected = Issues::new(); + expected.add_issue( + String::from("3fa5bfd93317ad25772680071d5ac3259cd2384f"), + crate::issue::Issue { + title: String::from("oh yeah we got titles"), + description: None, + state: crate::issue::State::Done, + dependencies: None, + }, + ); + expected.add_issue( + String::from("dd79c8cfb8beeacd0460429944b4ecbe95a31561"), + crate::issue::Issue { + title: String::from("issues out the wazoo"), + description: Some(String::from( + "Lots of words\nthat don't say much\nbecause this is just\na test\n", + )), + state: crate::issue::State::WontDo, + dependencies: None, + }, + ); + expected.add_issue( + String::from("a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7"), + crate::issue::Issue { + title: String::from("issue with dependencies"), + description: Some(String::from( + "a test has begun\nfor dependencies we seek\nintertwining life", + )), + state: crate::issue::State::WontDo, + dependencies: Some(vec![crate::issue::IssueHandle::from("3fa5bfd93317ad25772680071d5ac3259cd2384f"), crate::issue::IssueHandle::from("dd79c8cfb8beeacd0460429944b4ecbe95a31561")]), }, ); assert_eq!(issues, expected); } } + diff --git a/test/0002/3fa5bfd93317ad25772680071d5ac3259cd2384f/state b/test/0002/3fa5bfd93317ad25772680071d5ac3259cd2384f/state new file mode 100644 index 0000000..19f86f4 --- /dev/null +++ b/test/0002/3fa5bfd93317ad25772680071d5ac3259cd2384f/state @@ -0,0 +1 @@ +done diff --git a/test/0002/3fa5bfd93317ad25772680071d5ac3259cd2384f/title b/test/0002/3fa5bfd93317ad25772680071d5ac3259cd2384f/title new file mode 100644 index 0000000..18a1926 --- /dev/null +++ b/test/0002/3fa5bfd93317ad25772680071d5ac3259cd2384f/title @@ -0,0 +1 @@ +oh yeah we got titles diff --git a/test/0002/a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7/dependencies b/test/0002/a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7/dependencies new file mode 100644 index 0000000..71e4ee3 --- /dev/null +++ b/test/0002/a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7/dependencies @@ -0,0 +1,2 @@ +3fa5bfd93317ad25772680071d5ac3259cd2384f +dd79c8cfb8beeacd0460429944b4ecbe95a31561 \ No newline at end of file diff --git a/test/0002/a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7/description b/test/0002/a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7/description new file mode 100644 index 0000000..049c15f --- /dev/null +++ b/test/0002/a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7/description @@ -0,0 +1,3 @@ +a test has begun +for dependencies we seek +intertwining life \ No newline at end of file diff --git a/test/0002/a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7/state b/test/0002/a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7/state new file mode 100644 index 0000000..7f19192 --- /dev/null +++ b/test/0002/a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7/state @@ -0,0 +1 @@ +wontdo diff --git a/test/0002/a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7/title b/test/0002/a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7/title new file mode 100644 index 0000000..7c150e7 --- /dev/null +++ b/test/0002/a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7/title @@ -0,0 +1 @@ +issue with dependencies diff --git a/test/0002/config.toml b/test/0002/config.toml new file mode 100644 index 0000000..dfc15f2 --- /dev/null +++ b/test/0002/config.toml @@ -0,0 +1 @@ +states = [ "open", "closed" ] diff --git a/test/0002/dd79c8cfb8beeacd0460429944b4ecbe95a31561/description b/test/0002/dd79c8cfb8beeacd0460429944b4ecbe95a31561/description new file mode 100644 index 0000000..010156b --- /dev/null +++ b/test/0002/dd79c8cfb8beeacd0460429944b4ecbe95a31561/description @@ -0,0 +1,4 @@ +Lots of words +that don't say much +because this is just +a test diff --git a/test/0002/dd79c8cfb8beeacd0460429944b4ecbe95a31561/state b/test/0002/dd79c8cfb8beeacd0460429944b4ecbe95a31561/state new file mode 100644 index 0000000..7f19192 --- /dev/null +++ b/test/0002/dd79c8cfb8beeacd0460429944b4ecbe95a31561/state @@ -0,0 +1 @@ +wontdo diff --git a/test/0002/dd79c8cfb8beeacd0460429944b4ecbe95a31561/title b/test/0002/dd79c8cfb8beeacd0460429944b4ecbe95a31561/title new file mode 100644 index 0000000..ab5b4a9 --- /dev/null +++ b/test/0002/dd79c8cfb8beeacd0460429944b4ecbe95a31561/title @@ -0,0 +1 @@ +issues out the wazoo