Merge pull request 'ent-new-edit-show' (#3) from ent-new-edit-show into main

Reviewed-on: #3
This commit is contained in:
seb 2025-07-07 12:04:49 -06:00
commit 0b5e6f7379
4 changed files with 188 additions and 23 deletions

View file

@ -22,10 +22,13 @@ enum Commands {
List, List,
/// Create a new issue. /// Create a new issue.
New { New { description: Option<String> },
title: Option<String>,
description: Option<String>, /// 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<()> { fn handle_command(args: &Args, issues_dir: &std::path::Path) -> anyhow::Result<()> {
@ -37,11 +40,44 @@ fn handle_command(args: &Args, issues_dir: &std::path::Path) -> anyhow::Result<(
println!("{} {} ({:?})", uuid, issue.title(), issue.state); println!("{} {} ({:?})", uuid, issue.title(), issue.state);
} }
} }
Commands::New { title, description } => { Commands::New {
println!( description: Some(description),
"should make a new issue, title={:?}, description={:?}", } => {
title, 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());
}
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);
}
}
}
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);
}
}
} }
} }

View file

@ -89,6 +89,41 @@ pub fn git_branch_exists(branch: &str) -> Result<bool, GitError> {
return Ok(result.status.success()); 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> { pub fn create_orphan_branch(branch: &str) -> Result<(), GitError> {
{ {
let tmp_worktree = tempfile::tempdir().unwrap(); let tmp_worktree = tempfile::tempdir().unwrap();

View file

@ -1,3 +1,4 @@
use std::io::Write;
use std::str::FromStr; use std::str::FromStr;
#[derive(Debug, PartialEq, serde::Deserialize)] #[derive(Debug, PartialEq, serde::Deserialize)]
@ -18,18 +19,26 @@ pub struct Issue {
pub description: String, pub description: String,
pub state: State, pub state: State,
pub dependencies: Option<Vec<IssueHandle>>, pub dependencies: Option<Vec<IssueHandle>>,
/// 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)] #[derive(Debug, thiserror::Error)]
pub enum ReadIssueError { pub enum IssueError {
#[error(transparent)] #[error(transparent)]
StdIoError(#[from] std::io::Error), StdIoError(#[from] std::io::Error),
#[error("Failed to parse issue")] #[error("Failed to parse issue")]
IssueParseError, IssueParseError,
#[error("Failed to run git")]
GitError(#[from] crate::git::GitError),
#[error("Failed to run editor")]
EditorError,
} }
impl FromStr for State { impl FromStr for State {
type Err = ReadIssueError; type Err = IssueError;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.to_lowercase(); let s = s.to_lowercase();
if s == "new" { if s == "new" {
@ -45,13 +54,13 @@ impl FromStr for State {
} else if s == "wontdo" { } else if s == "wontdo" {
Ok(State::WontDo) Ok(State::WontDo)
} else { } else {
Err(ReadIssueError::IssueParseError) Err(IssueError::IssueParseError)
} }
} }
} }
impl Issue { impl Issue {
pub fn new_from_dir(dir: &std::path::Path) -> Result<Self, ReadIssueError> { pub fn new_from_dir(dir: &std::path::Path) -> Result<Self, IssueError> {
let mut description: Option<String> = None; let mut description: Option<String> = None;
let mut state = State::New; // default state, if not specified in the issue let mut state = State::New; // default state, if not specified in the issue
let mut dependencies: Option<Vec<String>> = None; let mut dependencies: Option<Vec<String>> = None;
@ -80,16 +89,64 @@ impl Issue {
} }
if description == None { if description == None {
return Err(ReadIssueError::IssueParseError); return Err(IssueError::IssueParseError);
} }
Ok(Self { Ok(Self {
description: description.unwrap(), description: description.unwrap(),
state: state, state: state,
dependencies, dependencies,
dir: std::path::PathBuf::from(dir),
}) })
} }
pub fn new(dir: &std::path::Path) -> Result<Self, IssueError> {
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 { pub fn title<'a>(&'a self) -> &'a str {
match self.description.find("\n") { match self.description.find("\n") {
Some(index) => &self.description.as_str()[..index], Some(index) => &self.description.as_str()[..index],
@ -110,6 +167,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"), 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, state: State::New,
dependencies: None, dependencies: None,
dir: std::path::PathBuf::from(issue_dir),
}; };
assert_eq!(issue, expected); assert_eq!(issue, expected);
} }
@ -122,6 +180,7 @@ mod tests {
description: String::from("minimal"), description: String::from("minimal"),
state: State::InProgress, state: State::InProgress,
dependencies: None, dependencies: None,
dir: std::path::PathBuf::from(issue_dir),
}; };
assert_eq!(issue, expected); assert_eq!(issue, expected);
} }

View file

@ -12,8 +12,8 @@ pub struct Issues {
pub enum ReadIssuesError { pub enum ReadIssuesError {
#[error(transparent)] #[error(transparent)]
StdIoError(#[from] std::io::Error), StdIoError(#[from] std::io::Error),
#[error("Failed to parse issue")] #[error(transparent)]
IssueParseError(#[from] crate::issue::ReadIssueError), IssueError(#[from] crate::issue::IssueError),
#[error("cannot handle filename")] #[error("cannot handle filename")]
FilenameError(std::ffi::OsString), FilenameError(std::ffi::OsString),
#[error(transparent)] #[error(transparent)]
@ -77,20 +77,30 @@ mod tests {
let issues = Issues::new_from_dir(issues_dir).unwrap(); let issues = Issues::new_from_dir(issues_dir).unwrap();
let mut expected = Issues::new(); 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( expected.add_issue(
String::from("7792b063eef6d33e7da5dc1856750c149ba678c6"), uuid,
crate::issue::Issue { crate::issue::Issue {
description: String::from("minimal"), description: String::from("minimal"),
state: crate::issue::State::InProgress, state: crate::issue::State::InProgress,
dependencies: None, dependencies: None,
dir,
}, },
); );
let uuid = String::from("3943fc5c173fdf41c0a22251593cd476d96e6c9f");
let mut dir = std::path::PathBuf::from(issues_dir);
dir.push(&uuid);
expected.add_issue( expected.add_issue(
String::from("3943fc5c173fdf41c0a22251593cd476d96e6c9f"), uuid,
crate::issue::Issue { 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"), 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, state: crate::issue::State::New,
dependencies: None, dependencies: None,
dir,
} }
); );
assert_eq!(issues, expected); assert_eq!(issues, expected);
@ -102,20 +112,30 @@ mod tests {
let issues = Issues::new_from_dir(issues_dir).unwrap(); let issues = Issues::new_from_dir(issues_dir).unwrap();
let mut expected = Issues::new(); 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( expected.add_issue(
String::from("3fa5bfd93317ad25772680071d5ac3259cd2384f"), uuid,
crate::issue::Issue { crate::issue::Issue {
description: String::from("oh yeah we got titles"), description: String::from("oh yeah we got titles"),
state: crate::issue::State::Done, state: crate::issue::State::Done,
dependencies: None, dependencies: None,
dir,
}, },
); );
let uuid = String::from("dd79c8cfb8beeacd0460429944b4ecbe95a31561");
let mut dir = std::path::PathBuf::from(issues_dir);
dir.push(&uuid);
expected.add_issue( expected.add_issue(
String::from("dd79c8cfb8beeacd0460429944b4ecbe95a31561"), uuid,
crate::issue::Issue { 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"), 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, state: crate::issue::State::WontDo,
dependencies: None, dependencies: None,
dir,
}, },
); );
assert_eq!(issues, expected); assert_eq!(issues, expected);
@ -127,24 +147,38 @@ mod tests {
let issues = Issues::new_from_dir(issues_dir).unwrap(); let issues = Issues::new_from_dir(issues_dir).unwrap();
let mut expected = Issues::new(); 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( expected.add_issue(
String::from("3fa5bfd93317ad25772680071d5ac3259cd2384f"), uuid,
crate::issue::Issue { crate::issue::Issue {
description: String::from("oh yeah we got titles\n"), description: String::from("oh yeah we got titles\n"),
state: crate::issue::State::Done, state: crate::issue::State::Done,
dependencies: None, dependencies: None,
dir,
}, },
); );
let uuid = String::from("dd79c8cfb8beeacd0460429944b4ecbe95a31561");
let mut dir = std::path::PathBuf::from(issues_dir);
dir.push(&uuid);
expected.add_issue( expected.add_issue(
String::from("dd79c8cfb8beeacd0460429944b4ecbe95a31561"), uuid,
crate::issue::Issue { 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"), 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, state: crate::issue::State::WontDo,
dependencies: None, dependencies: None,
dir,
}, },
); );
let uuid = String::from("a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7");
let mut dir = std::path::PathBuf::from(issues_dir);
dir.push(&uuid);
expected.add_issue( expected.add_issue(
String::from("a85f81fc5f14cb5d4851dd445dc9744c7f16ccc7"), uuid,
crate::issue::Issue { crate::issue::Issue {
description: String::from("issue with dependencies\n\na test has begun\nfor dependencies we seek\nintertwining life"), description: String::from("issue with dependencies\n\na test has begun\nfor dependencies we seek\nintertwining life"),
state: crate::issue::State::WontDo, state: crate::issue::State::WontDo,
@ -152,6 +186,7 @@ mod tests {
crate::issue::IssueHandle::from("3fa5bfd93317ad25772680071d5ac3259cd2384f"), crate::issue::IssueHandle::from("3fa5bfd93317ad25772680071d5ac3259cd2384f"),
crate::issue::IssueHandle::from("dd79c8cfb8beeacd0460429944b4ecbe95a31561"), crate::issue::IssueHandle::from("dd79c8cfb8beeacd0460429944b4ecbe95a31561"),
]), ]),
dir,
}, },
); );
assert_eq!(issues, expected); assert_eq!(issues, expected);