diff --git a/src/bin/ent/main.rs b/src/bin/ent/main.rs index 65e36dc..3561f4c 100644 --- a/src/bin/ent/main.rs +++ b/src/bin/ent/main.rs @@ -22,10 +22,13 @@ enum Commands { List, /// Create a new issue. - New { - title: Option, - description: Option, - }, + New { description: Option }, + + /// 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<()> { @@ -37,11 +40,44 @@ 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()); + } + 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); + } + } } } 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(); diff --git a/src/issue.rs b/src/issue.rs index 939af47..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)] @@ -18,18 +19,26 @@ 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)] -pub enum ReadIssueError { +pub enum IssueError { #[error(transparent)] 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 { - type Err = ReadIssueError; + type Err = IssueError; fn from_str(s: &str) -> Result { let s = s.to_lowercase(); if s == "new" { @@ -45,13 +54,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; @@ -80,16 +89,64 @@ impl Issue { } if description == None { - return Err(ReadIssueError::IssueParseError); + return Err(IssueError::IssueParseError); } Ok(Self { description: description.unwrap(), state: state, dependencies, + dir: std::path::PathBuf::from(dir), }) } + 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], @@ -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"), state: State::New, dependencies: None, + dir: std::path::PathBuf::from(issue_dir), }; assert_eq!(issue, expected); } @@ -122,6 +180,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..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)] @@ -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);