entomologist/src/bin/ent/main.rs
Sebastian Kuzminsky 1fa3aae2c0 give Comment a timestamp, display in chronological order
This commit makes a couple of changes:

- `ent show ISSUE` now displays the Issue's Comments in chronological
  order

- the Comment struct now includes a timestamp, which is the Author Time
  of the oldest commit that touches the comment's directory

- the Issue struct now stores its Comments in a sorted Vec, not in
  a HashMap

- The Comment's uuid moved into the Comment struct itself, instead of
  being the key in the Issue's HashMap of Comments
2025-07-08 12:29:24 -06:00

220 lines
7.2 KiB
Rust

use clap::Parser;
use entomologist::issue::State;
#[cfg(feature = "log")]
use simple_logger;
#[derive(Debug, clap::Parser)]
#[command(version, about, long_about = None)]
struct Args {
/// Directory containing issues.
#[arg(short = 'd', long)]
issues_dir: Option<String>,
/// Branch containing issues.
#[arg(short = 'b', long)]
issues_branch: Option<String>,
/// Type of behavior/output.
#[command(subcommand)]
command: Commands,
}
#[derive(clap::Subcommand, Debug)]
enum Commands {
/// List issues.
List,
/// Create a new issue.
New { description: Option<String> },
/// Edit the description of an issue.
Edit { issue_id: String },
/// Show the full description of an issue.
Show { issue_id: String },
/// Modify the state of an issue
State {
issue_id: String,
new_state: Option<State>,
},
/// Create a new comment on an issue.
Comment {
issue_id: String,
description: Option<String>,
},
/// Sync entomologist data with remote. This fetches from the remote,
/// merges the remote entomologist data branch with the local one,
/// and pushes the result back to the remote.
Sync {
/// Name of the git remote to sync with.
#[arg(default_value_t = String::from("origin"))]
remote: String,
},
}
fn handle_command(args: &Args, issues_dir: &std::path::Path) -> anyhow::Result<()> {
match &args.command {
Commands::List => {
let issues =
entomologist::issues::Issues::new_from_dir(std::path::Path::new(issues_dir))?;
for (uuid, issue) in issues.issues.iter() {
println!("{} {} ({:?})", uuid, issue.title(), issue.state);
}
}
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.get_mut_issue(issue_id) {
Some(issue) => {
issue.edit_description()?;
}
None => {
return Err(anyhow::anyhow!("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.get_issue(issue_id) {
Some(issue) => {
println!("issue {}", issue_id);
println!("state: {:?}", issue.state);
if let Some(dependencies) = &issue.dependencies {
println!("dependencies: {:?}", dependencies);
}
println!("");
println!("{}", issue.description);
for comment in &issue.comments {
println!("");
println!("comment: {}", comment.uuid);
println!("timestamp: {}", comment.timestamp);
println!("{}", comment.description);
}
}
None => {
return Err(anyhow::anyhow!("issue {} not found", issue_id));
}
}
}
Commands::State {
issue_id,
new_state,
} => {
let mut issues =
entomologist::issues::Issues::new_from_dir(std::path::Path::new(issues_dir))?;
match issues.issues.get_mut(issue_id) {
Some(issue) => {
let current_state = issue.state.clone();
match new_state {
Some(s) => {
issue.set_state(s.clone())?;
println!("issue: {}", issue_id);
println!("state: {} -> {}", current_state, s);
}
None => {
println!("issue: {}", issue_id);
println!("state: {}", current_state);
}
}
}
None => {
return Err(anyhow::anyhow!("issue {} not found", issue_id));
}
}
}
Commands::Comment {
issue_id,
description,
} => {
let mut issues =
entomologist::issues::Issues::new_from_dir(std::path::Path::new(issues_dir))?;
let Some(issue) = issues.get_mut_issue(issue_id) else {
return Err(anyhow::anyhow!("issue {} not found", issue_id));
};
let mut comment = issue.new_comment()?;
match description {
Some(description) => {
comment.set_description(description)?;
}
None => {
comment.edit_description()?;
}
}
}
Commands::Sync { remote } => {
if args.issues_dir.is_some() {
return Err(anyhow::anyhow!(
"`sync` operates on a branch, don't specify `issues_dir`"
));
}
// FIXME: Kinda bogus to re-do this thing we just did in
// `main()`. Maybe `main()` shouldn't create the worktree,
// maybe we should do it here in `handle_command()`?
// That way also each command could decide if it wants a
// read-only worktree or a read/write one.
let branch = match &args.issues_branch {
Some(branch) => branch,
None => "entomologist-data",
};
entomologist::git::sync(issues_dir, remote, branch)?;
println!("synced {:?} with {:?}", branch, remote);
}
}
Ok(())
}
fn main() -> anyhow::Result<()> {
#[cfg(feature = "log")]
simple_logger::SimpleLogger::new().env().init().unwrap();
let args: Args = Args::parse();
// println!("{:?}", args);
if let (Some(_), Some(_)) = (&args.issues_dir, &args.issues_branch) {
return Err(anyhow::anyhow!(
"don't specify both `--issues-dir` and `--issues-branch`"
));
}
if let Some(dir) = &args.issues_dir {
let dir = std::path::Path::new(dir);
handle_command(&args, dir)?;
} else {
let branch = match &args.issues_branch {
Some(branch) => branch,
None => "entomologist-data",
};
if !entomologist::git::git_branch_exists(branch)? {
entomologist::git::create_orphan_branch(branch)?;
}
let worktree = entomologist::git::Worktree::new(branch)?;
handle_command(&args, worktree.path())?;
}
Ok(())
}