cleanups-and-show-log-in-ent-sync #17
4 changed files with 318 additions and 137 deletions
|
|
@ -216,12 +216,7 @@ fn handle_command(
|
||||||
Commands::New { description } => {
|
Commands::New { description } => {
|
||||||
let issues_database =
|
let issues_database =
|
||||||
make_issues_database(issues_database_source, IssuesDatabaseAccess::ReadWrite)?;
|
make_issues_database(issues_database_source, IssuesDatabaseAccess::ReadWrite)?;
|
||||||
let mut issue = entomologist::issue::Issue::new(&issues_database.dir)?;
|
match entomologist::issue::Issue::new(&issues_database.dir, description) {
|
||||||
let r = match description {
|
|
||||||
Some(description) => issue.set_description(description),
|
|
||||||
None => issue.edit_description(),
|
|
||||||
};
|
|
||||||
match r {
|
|
||||||
Err(entomologist::issue::IssueError::EmptyDescription) => {
|
Err(entomologist::issue::IssueError::EmptyDescription) => {
|
||||||
println!("no new issue created");
|
println!("no new issue created");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
|
@ -229,7 +224,7 @@ fn handle_command(
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
return Err(e.into());
|
return Err(e.into());
|
||||||
}
|
}
|
||||||
Ok(()) => {
|
Ok(issue) => {
|
||||||
println!("created new issue '{}'", issue.title());
|
println!("created new issue '{}'", issue.title());
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
@ -332,21 +327,21 @@ fn handle_command(
|
||||||
let Some(issue) = issues.get_mut_issue(issue_id) else {
|
let Some(issue) = issues.get_mut_issue(issue_id) else {
|
||||||
return Err(anyhow::anyhow!("issue {} not found", issue_id));
|
return Err(anyhow::anyhow!("issue {} not found", issue_id));
|
||||||
};
|
};
|
||||||
let mut comment = issue.new_comment()?;
|
match issue.add_comment(description) {
|
||||||
let r = match description {
|
Err(entomologist::issue::IssueError::CommentError(
|
||||||
Some(description) => comment.set_description(description),
|
entomologist::comment::CommentError::EmptyDescription,
|
||||||
None => comment.edit_description(),
|
)) => {
|
||||||
};
|
|
||||||
match r {
|
|
||||||
Err(entomologist::comment::CommentError::EmptyDescription) => {
|
|
||||||
println!("aborted new comment");
|
println!("aborted new comment");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
return Err(e.into());
|
return Err(e.into());
|
||||||
}
|
}
|
||||||
Ok(()) => {
|
Ok(comment) => {
|
||||||
println!("created new comment {}", &comment.uuid);
|
println!(
|
||||||
|
"created new comment {} on issue {}",
|
||||||
|
&comment.uuid, &issue_id
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -368,31 +363,32 @@ fn handle_command(
|
||||||
issue_id,
|
issue_id,
|
||||||
new_assignee,
|
new_assignee,
|
||||||
} => {
|
} => {
|
||||||
let issues_database =
|
let issues = read_issues_database(issues_database_source)?;
|
||||||
make_issues_database(issues_database_source, IssuesDatabaseAccess::ReadWrite)?;
|
let Some(original_issue) = issues.issues.get(issue_id) else {
|
||||||
let mut issues = entomologist::issues::Issues::new_from_dir(&issues_database.dir)?;
|
|
||||||
let Some(issue) = issues.issues.get_mut(issue_id) else {
|
|
||||||
return Err(anyhow::anyhow!("issue {} not found", issue_id));
|
return Err(anyhow::anyhow!("issue {} not found", issue_id));
|
||||||
};
|
};
|
||||||
match (&issue.assignee, new_assignee) {
|
let old_assignee: String = match &original_issue.assignee {
|
||||||
(Some(old_assignee), Some(new_assignee)) => {
|
Some(assignee) => assignee.clone(),
|
||||||
println!("issue: {}", issue_id);
|
None => String::from("None"),
|
||||||
|
};
|
||||||
|
println!("issue: {}", issue_id);
|
||||||
|
match new_assignee {
|
||||||
|
Some(new_assignee) => {
|
||||||
|
let issues_database = make_issues_database(
|
||||||
|
issues_database_source,
|
||||||
|
IssuesDatabaseAccess::ReadWrite,
|
||||||
|
)?;
|
||||||
|
let mut issues =
|
||||||
|
entomologist::issues::Issues::new_from_dir(&issues_database.dir)?;
|
||||||
|
let Some(issue) = issues.get_mut_issue(issue_id) else {
|
||||||
|
return Err(anyhow::anyhow!("issue {} not found", issue_id));
|
||||||
|
};
|
||||||
println!("assignee: {} -> {}", old_assignee, new_assignee);
|
println!("assignee: {} -> {}", old_assignee, new_assignee);
|
||||||
issue.set_assignee(new_assignee)?;
|
issue.set_assignee(new_assignee)?;
|
||||||
}
|
}
|
||||||
(Some(old_assignee), None) => {
|
None => {
|
||||||
println!("issue: {}", issue_id);
|
|
||||||
println!("assignee: {}", old_assignee);
|
println!("assignee: {}", old_assignee);
|
||||||
}
|
}
|
||||||
(None, Some(new_assignee)) => {
|
|
||||||
println!("issue: {}", issue_id);
|
|
||||||
println!("assignee: None -> {}", new_assignee);
|
|
||||||
issue.set_assignee(new_assignee)?;
|
|
||||||
}
|
|
||||||
(None, None) => {
|
|
||||||
println!("issue: {}", issue_id);
|
|
||||||
println!("assignee: None");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
127
src/comment.rs
127
src/comment.rs
|
|
@ -16,6 +16,8 @@ pub struct Comment {
|
||||||
pub enum CommentError {
|
pub enum CommentError {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
StdIoError(#[from] std::io::Error),
|
StdIoError(#[from] std::io::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
EnvVarError(#[from] std::env::VarError),
|
||||||
#[error("Failed to parse comment")]
|
#[error("Failed to parse comment")]
|
||||||
CommentParseError,
|
CommentParseError,
|
||||||
#[error("Failed to run git")]
|
#[error("Failed to run git")]
|
||||||
|
|
@ -61,17 +63,56 @@ impl Comment {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_description(&mut self, description: &str) -> Result<(), CommentError> {
|
/// Create a new Comment on the specified Issue. Commits.
|
||||||
if description.len() == 0 {
|
pub fn new(
|
||||||
return Err(CommentError::EmptyDescription);
|
issue: &crate::issue::Issue,
|
||||||
|
description: &Option<String>,
|
||||||
|
) -> Result<crate::comment::Comment, CommentError> {
|
||||||
|
let mut dir = std::path::PathBuf::from(&issue.dir);
|
||||||
|
dir.push("comments");
|
||||||
|
if !dir.exists() {
|
||||||
|
std::fs::create_dir(&dir)?;
|
||||||
}
|
}
|
||||||
self.description = String::from(description);
|
|
||||||
let mut description_filename = std::path::PathBuf::from(&self.dir);
|
let rnd: u128 = rand::random();
|
||||||
description_filename.push("description");
|
let uuid = format!("{:032x}", rnd);
|
||||||
let mut description_file = std::fs::File::create(&description_filename)?;
|
dir.push(&uuid);
|
||||||
write!(description_file, "{}", description)?;
|
std::fs::create_dir(&dir)?;
|
||||||
crate::git::git_commit_file(&description_filename)?;
|
|
||||||
Ok(())
|
let mut comment = crate::comment::Comment {
|
||||||
|
uuid,
|
||||||
|
author: String::from(""), // this will be updated from git when we re-read this comment
|
||||||
|
timestamp: chrono::Local::now(),
|
||||||
|
description: String::from(""), // this will be set immediately below
|
||||||
|
dir: dir.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
match description {
|
||||||
|
Some(description) => {
|
||||||
|
if description.len() == 0 {
|
||||||
|
return Err(CommentError::EmptyDescription);
|
||||||
|
}
|
||||||
|
comment.description = String::from(description);
|
||||||
|
let description_filename = comment.description_filename();
|
||||||
|
let mut description_file = std::fs::File::create(&description_filename)?;
|
||||||
|
write!(description_file, "{}", description)?;
|
||||||
|
}
|
||||||
|
None => comment.edit_description_file()?,
|
||||||
|
};
|
||||||
|
|
||||||
|
crate::git::add(&dir)?;
|
||||||
|
if crate::git::worktree_is_dirty(&dir.to_string_lossy())? {
|
||||||
|
crate::git::commit(
|
||||||
|
&dir,
|
||||||
|
&format!(
|
||||||
|
"add comment {} on issue {}",
|
||||||
|
comment.uuid,
|
||||||
|
issue.dir.file_name().unwrap().to_string_lossy(),
|
||||||
|
),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(comment)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_description(&mut self) -> Result<(), CommentError> {
|
pub fn read_description(&mut self) -> Result<(), CommentError> {
|
||||||
|
|
@ -82,11 +123,39 @@ impl Comment {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn edit_description(&mut self) -> Result<(), CommentError> {
|
pub fn edit_description(&mut self) -> Result<(), CommentError> {
|
||||||
let mut description_filename = std::path::PathBuf::from(&self.dir);
|
self.edit_description_file()?;
|
||||||
description_filename.push("description");
|
let description_filename = self.description_filename();
|
||||||
|
crate::git::add(&description_filename)?;
|
||||||
|
if crate::git::worktree_is_dirty(&self.dir.to_string_lossy())? {
|
||||||
|
crate::git::commit(
|
||||||
|
&description_filename.parent().unwrap(),
|
||||||
|
&format!(
|
||||||
|
"edit comment {} on issue FIXME", // FIXME: name the issue that the comment is on
|
||||||
|
self.dir.file_name().unwrap().to_string_lossy()
|
||||||
|
),
|
||||||
|
)?;
|
||||||
|
self.read_description()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Opens the Comment's `description` file in an editor. Validates
|
||||||
|
/// the editor's exit code. Updates the Comment's internal
|
||||||
|
/// description from what the user saved in the file.
|
||||||
|
///
|
||||||
|
/// Used by Issue::add_comment() when no description is supplied,
|
||||||
|
/// and (FIXME: in the future) used by `ent edit COMMENT`.
|
||||||
|
pub fn edit_description_file(&mut self) -> Result<(), CommentError> {
|
||||||
|
let description_filename = self.description_filename();
|
||||||
let exists = description_filename.exists();
|
let exists = description_filename.exists();
|
||||||
let result = std::process::Command::new("vi")
|
|
||||||
.arg(&description_filename.as_mut_os_str())
|
let editor = match std::env::var("EDITOR") {
|
||||||
|
Ok(editor) => editor,
|
||||||
|
Err(std::env::VarError::NotPresent) => String::from("vi"),
|
||||||
|
Err(e) => return Err(e.into()),
|
||||||
|
};
|
||||||
|
let result = std::process::Command::new(editor)
|
||||||
|
.arg(&description_filename.as_os_str())
|
||||||
.spawn()?
|
.spawn()?
|
||||||
.wait_with_output()?;
|
.wait_with_output()?;
|
||||||
if !result.status.success() {
|
if !result.status.success() {
|
||||||
|
|
@ -94,9 +163,8 @@ impl Comment {
|
||||||
println!("stderr: {}", std::str::from_utf8(&result.stderr).unwrap());
|
println!("stderr: {}", std::str::from_utf8(&result.stderr).unwrap());
|
||||||
return Err(CommentError::EditorError);
|
return Err(CommentError::EditorError);
|
||||||
}
|
}
|
||||||
if description_filename.exists() && description_filename.metadata()?.len() > 0 {
|
|
||||||
crate::git::add(&description_filename)?;
|
if !description_filename.exists() || description_filename.metadata()?.len() == 0 {
|
||||||
} else {
|
|
||||||
// User saved an empty file, which means they changed their
|
// User saved an empty file, which means they changed their
|
||||||
// mind and no longer want to edit the description.
|
// mind and no longer want to edit the description.
|
||||||
if exists {
|
if exists {
|
||||||
|
|
@ -104,25 +172,20 @@ impl Comment {
|
||||||
}
|
}
|
||||||
return Err(CommentError::EmptyDescription);
|
return Err(CommentError::EmptyDescription);
|
||||||
}
|
}
|
||||||
if crate::git::worktree_is_dirty(&self.dir.to_string_lossy())? {
|
self.read_description()?;
|
||||||
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is the private, internal API.
|
||||||
|
impl Comment {
|
||||||
|
fn description_filename(&self) -> std::path::PathBuf {
|
||||||
|
let mut description_filename = std::path::PathBuf::from(&self.dir);
|
||||||
|
description_filename.push("description");
|
||||||
|
description_filename
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
||||||
56
src/git.rs
56
src/git.rs
|
|
@ -233,6 +233,62 @@ pub fn sync(dir: &std::path::Path, remote: &str, branch: &str) -> Result<(), Git
|
||||||
|
|
||||||
git_fetch(dir, remote)?;
|
git_fetch(dir, remote)?;
|
||||||
|
|
||||||
|
// FIXME: Possible things to add:
|
||||||
|
// * `git log -p` shows diff
|
||||||
|
// * `git log --numstat` shows machine-readable diffstat
|
||||||
|
|
||||||
|
// Show what we just fetched from the remote.
|
||||||
|
let result = std::process::Command::new("git")
|
||||||
|
.args([
|
||||||
|
"log",
|
||||||
|
"--no-merges",
|
||||||
|
"--pretty=format:%an: %s",
|
||||||
|
&format!("{}/{}", remote, branch),
|
||||||
|
&format!("^{}", branch),
|
||||||
|
])
|
||||||
|
.current_dir(dir)
|
||||||
|
.output()?;
|
||||||
|
if !result.status.success() {
|
||||||
|
println!(
|
||||||
|
"Sync failed! 'git log' error! Help, a human needs to fix the mess in {:?}",
|
||||||
|
dir
|
||||||
|
);
|
||||||
|
println!("stdout: {}", std::str::from_utf8(&result.stdout).unwrap());
|
||||||
|
println!("stderr: {}", std::str::from_utf8(&result.stderr).unwrap());
|
||||||
|
return Err(GitError::Oops);
|
||||||
|
}
|
||||||
|
if result.stdout.len() > 0 {
|
||||||
|
println!("Changes fetched from remote {}:", remote);
|
||||||
|
println!("{}", std::str::from_utf8(&result.stdout).unwrap());
|
||||||
|
println!("");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show what we are about to push to the remote.
|
||||||
|
let result = std::process::Command::new("git")
|
||||||
|
.args([
|
||||||
|
"log",
|
||||||
|
"--no-merges",
|
||||||
|
"--pretty=format:%an: %s",
|
||||||
|
&format!("{}", branch),
|
||||||
|
&format!("^{}/{}", remote, branch),
|
||||||
|
])
|
||||||
|
.current_dir(dir)
|
||||||
|
.output()?;
|
||||||
|
if !result.status.success() {
|
||||||
|
println!(
|
||||||
|
"Sync failed! 'git log' error! Help, a human needs to fix the mess in {:?}",
|
||||||
|
dir
|
||||||
|
);
|
||||||
|
println!("stdout: {}", std::str::from_utf8(&result.stdout).unwrap());
|
||||||
|
println!("stderr: {}", std::str::from_utf8(&result.stderr).unwrap());
|
||||||
|
return Err(GitError::Oops);
|
||||||
|
}
|
||||||
|
if result.stdout.len() > 0 {
|
||||||
|
println!("Changes to push to remote {}:", remote);
|
||||||
|
println!("{}", std::str::from_utf8(&result.stdout).unwrap());
|
||||||
|
println!("");
|
||||||
|
}
|
||||||
|
|
||||||
// Merge remote branch into local.
|
// Merge remote branch into local.
|
||||||
let result = std::process::Command::new("git")
|
let result = std::process::Command::new("git")
|
||||||
.args(["merge", &format!("{}/{}", remote, branch)])
|
.args(["merge", &format!("{}/{}", remote, branch)])
|
||||||
|
|
|
||||||
208
src/issue.rs
208
src/issue.rs
|
|
@ -38,6 +38,8 @@ pub enum IssueError {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
StdIoError(#[from] std::io::Error),
|
StdIoError(#[from] std::io::Error),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
|
EnvVarError(#[from] std::env::VarError),
|
||||||
|
#[error(transparent)]
|
||||||
CommentError(#[from] crate::comment::CommentError),
|
CommentError(#[from] crate::comment::CommentError),
|
||||||
#[error("Failed to parse issue")]
|
#[error("Failed to parse issue")]
|
||||||
IssueParseError,
|
IssueParseError,
|
||||||
|
|
@ -87,6 +89,7 @@ impl fmt::Display for State {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is the public API of Issue.
|
||||||
impl Issue {
|
impl Issue {
|
||||||
pub fn new_from_dir(dir: &std::path::Path) -> Result<Self, IssueError> {
|
pub fn new_from_dir(dir: &std::path::Path) -> Result<Self, IssueError> {
|
||||||
let mut description: Option<String> = None;
|
let mut description: Option<String> = None;
|
||||||
|
|
@ -158,33 +161,35 @@ impl Issue {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_comment(&mut self) -> Result<crate::comment::Comment, IssueError> {
|
/// Add a new Comment to the Issue. Commits.
|
||||||
let mut dir = std::path::PathBuf::from(&self.dir);
|
pub fn add_comment(
|
||||||
dir.push("comments");
|
&mut self,
|
||||||
if !dir.exists() {
|
description: &Option<String>,
|
||||||
std::fs::create_dir(&dir)?;
|
) -> Result<crate::comment::Comment, IssueError> {
|
||||||
}
|
let comment = crate::comment::Comment::new(self, description)?;
|
||||||
|
Ok(comment)
|
||||||
let rnd: u128 = rand::random();
|
|
||||||
let uuid = format!("{:032x}", rnd);
|
|
||||||
dir.push(&uuid);
|
|
||||||
std::fs::create_dir(&dir)?;
|
|
||||||
|
|
||||||
Ok(crate::comment::Comment {
|
|
||||||
uuid,
|
|
||||||
author: String::from("Sebastian Kuzminsky <seb@highlab.com>"),
|
|
||||||
timestamp: chrono::Local::now(),
|
|
||||||
description: String::from(""), // FIXME
|
|
||||||
dir,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(dir: &std::path::Path) -> Result<Self, IssueError> {
|
/// Create a new Issue in an Issues database specified by a directory.
|
||||||
|
/// The new Issue will live in a new subdirectory, named by a unique
|
||||||
|
/// Issue identifier.
|
||||||
|
///
|
||||||
|
/// If a description string is supplied, the new Issue's description
|
||||||
|
/// will be initialized from it with no user interaction.
|
||||||
|
///
|
||||||
|
/// If no description is supplied, the user will be prompted to
|
||||||
|
/// input one into an editor.
|
||||||
|
///
|
||||||
|
/// On success, the new Issue with its valid description is committed
|
||||||
|
/// to the Issues database.
|
||||||
|
pub fn new(dir: &std::path::Path, description: &Option<String>) -> Result<Self, IssueError> {
|
||||||
let mut issue_dir = std::path::PathBuf::from(dir);
|
let mut issue_dir = std::path::PathBuf::from(dir);
|
||||||
let rnd: u128 = rand::random();
|
let rnd: u128 = rand::random();
|
||||||
issue_dir.push(&format!("{:032x}", rnd));
|
let issue_id = format!("{:032x}", rnd);
|
||||||
|
issue_dir.push(&issue_id);
|
||||||
std::fs::create_dir(&issue_dir)?;
|
std::fs::create_dir(&issue_dir)?;
|
||||||
Ok(Self {
|
|
||||||
|
let mut issue = Self {
|
||||||
author: String::from(""),
|
author: String::from(""),
|
||||||
timestamp: chrono::Local::now(),
|
timestamp: chrono::Local::now(),
|
||||||
state: State::New,
|
state: State::New,
|
||||||
|
|
@ -192,58 +197,38 @@ impl Issue {
|
||||||
assignee: None,
|
assignee: None,
|
||||||
description: String::from(""), // FIXME: kind of bogus to use the empty string as None
|
description: String::from(""), // FIXME: kind of bogus to use the empty string as None
|
||||||
comments: Vec::<crate::comment::Comment>::new(),
|
comments: Vec::<crate::comment::Comment>::new(),
|
||||||
dir: issue_dir,
|
dir: issue_dir.clone(),
|
||||||
})
|
};
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_description(&mut self, description: &str) -> Result<(), IssueError> {
|
match description {
|
||||||
if description.len() == 0 {
|
Some(description) => {
|
||||||
return Err(IssueError::EmptyDescription);
|
if description.len() == 0 {
|
||||||
}
|
return Err(IssueError::EmptyDescription);
|
||||||
self.description = String::from(description);
|
}
|
||||||
let mut description_filename = std::path::PathBuf::from(&self.dir);
|
issue.description = String::from(description);
|
||||||
description_filename.push("description");
|
let description_filename = issue.description_filename();
|
||||||
let mut description_file = std::fs::File::create(&description_filename)?;
|
let mut description_file = std::fs::File::create(&description_filename)?;
|
||||||
write!(description_file, "{}", description)?;
|
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 exists = description_filename.exists();
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
if description_filename.exists() && description_filename.metadata()?.len() > 0 {
|
|
||||||
crate::git::add(&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);
|
None => issue.edit_description_file()?,
|
||||||
}
|
};
|
||||||
|
|
||||||
|
crate::git::add(&issue_dir)?;
|
||||||
|
crate::git::commit(&issue_dir, &format!("create new issue {}", issue_id))?;
|
||||||
|
|
||||||
|
Ok(issue)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Interactively edit the description of an existing Issue.
|
||||||
|
pub fn edit_description(&mut self) -> Result<(), IssueError> {
|
||||||
|
self.edit_description_file()?;
|
||||||
|
let description_filename = self.description_filename();
|
||||||
|
crate::git::add(&description_filename)?;
|
||||||
if crate::git::worktree_is_dirty(&self.dir.to_string_lossy())? {
|
if crate::git::worktree_is_dirty(&self.dir.to_string_lossy())? {
|
||||||
crate::git::commit(
|
crate::git::commit(
|
||||||
&description_filename.parent().unwrap(),
|
&description_filename.parent().unwrap(),
|
||||||
&format!(
|
&format!(
|
||||||
"new description for issue {}",
|
"edit description of issue {}",
|
||||||
description_filename
|
description_filename
|
||||||
.parent()
|
.parent()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
|
@ -252,11 +237,11 @@ impl Issue {
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
),
|
),
|
||||||
)?;
|
)?;
|
||||||
self.read_description()?;
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the Issue title (first line of the description).
|
||||||
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],
|
||||||
|
|
@ -264,12 +249,23 @@ impl Issue {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Change the State of the Issue.
|
||||||
pub fn set_state(&mut self, new_state: State) -> Result<(), IssueError> {
|
pub fn set_state(&mut self, new_state: State) -> Result<(), IssueError> {
|
||||||
let mut state_filename = std::path::PathBuf::from(&self.dir);
|
let mut state_filename = std::path::PathBuf::from(&self.dir);
|
||||||
state_filename.push("state");
|
state_filename.push("state");
|
||||||
let mut state_file = std::fs::File::create(&state_filename)?;
|
let mut state_file = std::fs::File::create(&state_filename)?;
|
||||||
write!(state_file, "{}", new_state)?;
|
write!(state_file, "{}", new_state)?;
|
||||||
crate::git::git_commit_file(&state_filename)?;
|
crate::git::add(&state_filename)?;
|
||||||
|
if crate::git::worktree_is_dirty(&self.dir.to_string_lossy())? {
|
||||||
|
crate::git::commit(
|
||||||
|
&self.dir,
|
||||||
|
&format!(
|
||||||
|
"change state of issue {} to {}",
|
||||||
|
self.dir.file_name().unwrap().to_string_lossy(),
|
||||||
|
new_state,
|
||||||
|
),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -281,12 +277,82 @@ impl Issue {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the Assignee of an Issue.
|
||||||
pub fn set_assignee(&mut self, new_assignee: &str) -> Result<(), IssueError> {
|
pub fn set_assignee(&mut self, new_assignee: &str) -> Result<(), IssueError> {
|
||||||
|
let old_assignee = match &self.assignee {
|
||||||
|
Some(assignee) => assignee.clone(),
|
||||||
|
None => String::from("None"),
|
||||||
|
};
|
||||||
let mut assignee_filename = std::path::PathBuf::from(&self.dir);
|
let mut assignee_filename = std::path::PathBuf::from(&self.dir);
|
||||||
assignee_filename.push("assignee");
|
assignee_filename.push("assignee");
|
||||||
let mut assignee_file = std::fs::File::create(&assignee_filename)?;
|
let mut assignee_file = std::fs::File::create(&assignee_filename)?;
|
||||||
write!(assignee_file, "{}", new_assignee)?;
|
write!(assignee_file, "{}", new_assignee)?;
|
||||||
crate::git::git_commit_file(&assignee_filename)?;
|
crate::git::add(&assignee_filename)?;
|
||||||
|
if crate::git::worktree_is_dirty(&self.dir.to_string_lossy())? {
|
||||||
|
crate::git::commit(
|
||||||
|
&self.dir,
|
||||||
|
&format!(
|
||||||
|
"change assignee of issue {}, {} -> {}",
|
||||||
|
self.dir.file_name().unwrap().to_string_lossy(),
|
||||||
|
old_assignee,
|
||||||
|
new_assignee,
|
||||||
|
),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is the internal/private API of Issue.
|
||||||
|
impl Issue {
|
||||||
|
fn description_filename(&self) -> std::path::PathBuf {
|
||||||
|
let mut description_filename = std::path::PathBuf::from(&self.dir);
|
||||||
|
description_filename.push("description");
|
||||||
|
description_filename
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the Issue's description file into the internal Issue representation.
|
||||||
|
fn read_description(&mut self) -> Result<(), IssueError> {
|
||||||
|
let description_filename = self.description_filename();
|
||||||
|
self.description = std::fs::read_to_string(description_filename)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Opens the Issue's `description` file in an editor. Validates the
|
||||||
|
/// editor's exit code. Updates the Issue's internal description
|
||||||
|
/// from what the user saved in the file.
|
||||||
|
///
|
||||||
|
/// Used by Issue::new() when no description is supplied, and also
|
||||||
|
/// used by `ent edit ISSUE`.
|
||||||
|
fn edit_description_file(&mut self) -> Result<(), IssueError> {
|
||||||
|
let description_filename = self.description_filename();
|
||||||
|
let exists = description_filename.exists();
|
||||||
|
let editor = match std::env::var("EDITOR") {
|
||||||
|
Ok(editor) => editor,
|
||||||
|
Err(std::env::VarError::NotPresent) => String::from("vi"),
|
||||||
|
Err(e) => return Err(e.into()),
|
||||||
|
};
|
||||||
|
let result = std::process::Command::new(editor)
|
||||||
|
.arg(&description_filename.as_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);
|
||||||
|
}
|
||||||
|
if !description_filename.exists() || description_filename.metadata()?.len() == 0 {
|
||||||
|
// User saved an empty file, or exited without saving while
|
||||||
|
// editing a new description file. Both means they changed
|
||||||
|
// their mind and no longer want to edit the description.
|
||||||
|
if exists {
|
||||||
|
// File existed before the user emptied it, so restore
|
||||||
|
// the original.
|
||||||
|
crate::git::restore_file(&description_filename)?;
|
||||||
|
}
|
||||||
|
return Err(IssueError::EmptyDescription);
|
||||||
|
}
|
||||||
|
self.read_description()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue