Compare commits

..

11 commits

Author SHA1 Message Date
1b87de72d6 ent list: show assignee, if any 2025-07-08 18:45:05 -06:00
30bac14577 add ent assign ISSUE PERSON 2025-07-08 18:45:05 -06:00
6c791ef3c2 add optional 'assignee' to Issue 2025-07-08 18:45:05 -06:00
9aa0567d13 ent list: show comment count for each issue 2025-07-08 18:45:05 -06:00
80b842baaf make ent list sort issues first by state, then by ctime 2025-07-08 18:45:04 -06:00
316ca3a901 add author and timestamp to Issue 2025-07-08 18:42:14 -06:00
37b7eb341f add author to Comment 2025-07-08 18:42:14 -06:00
a2c7ce34a3 fix git::git_log_oldest_timestamp() when there are multiple log entries 2025-07-08 18:42:14 -06:00
be362517fb 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 18:42:14 -06:00
seb
431c67d43d Merge pull request 'ent list now accepts a filter, default "state=New,Backlog,Blocked,InProgress"' (#10) from filter-list into main
Reviewed-on: #10
2025-07-08 18:41:19 -06:00
7d9284bf91 ent list now accepts a filter, default "state=New,Backlog,Blocked,InProgress" 2025-07-07 23:46:14 -06:00
3 changed files with 55 additions and 8 deletions

View file

@ -23,7 +23,11 @@ struct Args {
#[derive(clap::Subcommand, Debug)] #[derive(clap::Subcommand, Debug)]
enum Commands { enum Commands {
/// List issues. /// List issues.
List, List {
/// Filter string, describes issues to include in the list.
#[arg(default_value_t = String::from("state=New,Backlog,Blocked,InProgress"))]
filter: String,
},
/// Create a new issue. /// Create a new issue.
New { description: Option<String> }, New { description: Option<String> },
@ -64,19 +68,21 @@ enum Commands {
fn handle_command(args: &Args, issues_dir: &std::path::Path) -> anyhow::Result<()> { fn handle_command(args: &Args, issues_dir: &std::path::Path) -> anyhow::Result<()> {
match &args.command { match &args.command {
Commands::List => { Commands::List { filter } => {
let issues = let issues =
entomologist::issues::Issues::new_from_dir(std::path::Path::new(issues_dir))?; entomologist::issues::Issues::new_from_dir(std::path::Path::new(issues_dir))?;
let filter = entomologist::parse_filter(filter)?;
let mut uuids_by_state = std::collections::HashMap::< let mut uuids_by_state = std::collections::HashMap::<
entomologist::issue::State, entomologist::issue::State,
Vec<&entomologist::issue::IssueHandle>, Vec<&entomologist::issue::IssueHandle>,
>::new(); >::new();
for (uuid, issue) in issues.issues.iter() { for (uuid, issue) in issues.issues.iter() {
uuids_by_state if filter.include_states.contains(&issue.state) {
.entry(issue.state.clone()) uuids_by_state
.or_default() .entry(issue.state.clone())
.push(uuid); .or_default()
.push(uuid);
}
} }
use entomologist::issue::State; use entomologist::issue::State;
@ -89,6 +95,9 @@ fn handle_command(args: &Args, issues_dir: &std::path::Path) -> anyhow::Result<(
State::WontDo, State::WontDo,
] { ] {
let these_uuids = uuids_by_state.entry(state.clone()).or_default(); let these_uuids = uuids_by_state.entry(state.clone()).or_default();
if these_uuids.len() == 0 {
continue;
}
these_uuids.sort_by(|a_id, b_id| { these_uuids.sort_by(|a_id, b_id| {
let a = issues.issues.get(*a_id).unwrap(); let a = issues.issues.get(*a_id).unwrap();
let b = issues.issues.get(*b_id).unwrap(); let b = issues.issues.get(*b_id).unwrap();

View file

@ -41,6 +41,8 @@ pub enum IssueError {
CommentError(#[from] crate::comment::CommentError), CommentError(#[from] crate::comment::CommentError),
#[error("Failed to parse issue")] #[error("Failed to parse issue")]
IssueParseError, IssueParseError,
#[error("Failed to parse state")]
StateParseError,
#[error("Failed to run git")] #[error("Failed to run git")]
GitError(#[from] crate::git::GitError), GitError(#[from] crate::git::GitError),
#[error("Failed to run editor")] #[error("Failed to run editor")]
@ -64,7 +66,7 @@ impl FromStr for State {
} else if s == "wontdo" { } else if s == "wontdo" {
Ok(State::WontDo) Ok(State::WontDo)
} else { } else {
Err(IssueError::IssueParseError) Err(IssueError::StateParseError)
} }
} }
} }

View file

@ -1,4 +1,40 @@
use std::str::FromStr;
pub mod comment; pub mod comment;
pub mod git; pub mod git;
pub mod issue; pub mod issue;
pub mod issues; pub mod issues;
#[derive(Debug, thiserror::Error)]
pub enum ParseFilterError {
#[error("Failed to parse filter")]
ParseError,
#[error(transparent)]
IssueParseError(#[from] crate::issue::IssueError),
}
// FIXME: It's easy to imagine a full dsl for filtering issues, for now
// i'm starting with obvious easy things. Chumsky looks appealing but
// more research is needed.
#[derive(Debug)]
pub struct Filter {
pub include_states: std::collections::HashSet<crate::issue::State>,
}
// Parses a filter description matching "state=STATE[,STATE*]"
pub fn parse_filter(filter_str: &str) -> Result<Filter, ParseFilterError> {
let tokens: Vec<&str> = filter_str.split("=").collect();
if tokens.len() != 2 {
return Err(ParseFilterError::ParseError);
}
if tokens[0] != "state" {
return Err(ParseFilterError::ParseError);
}
let mut include_states = std::collections::HashSet::<crate::issue::State>::new();
for s in tokens[1].split(",") {
include_states.insert(crate::issue::State::from_str(s)?);
}
Ok(Filter { include_states })
}