Compare commits
6 commits
61232b70fc
...
a3077ca313
| Author | SHA1 | Date | |
|---|---|---|---|
| a3077ca313 | |||
| bc2b1bd3c1 | |||
| 3b33ed41f5 | |||
| 3df76b89df | |||
| 20c17f281b | |||
| 5e5508a2ee |
2 changed files with 86 additions and 66 deletions
|
|
@ -24,22 +24,31 @@ struct Args {
|
||||||
enum Commands {
|
enum Commands {
|
||||||
/// List issues.
|
/// List issues.
|
||||||
List {
|
List {
|
||||||
/// Filter string, describes issues to include in the list.
|
/// Filter strings, describes issues to include in the list.
|
||||||
/// The filter string is composed of chunks separated by ":".
|
/// Each filter string is of the form "name=condition".
|
||||||
/// Each chunk is of the form "name=condition". The supported
|
/// The supported names and their matching conditions are:
|
||||||
/// names and their matching conditions are:
|
|
||||||
///
|
///
|
||||||
/// "state": Comma-separated list of states to list.
|
/// "state": Comma-separated list of states to list.
|
||||||
|
/// Example: "state=new,backlog". Defaults to
|
||||||
|
/// "new,backlog,blocked,inprogress".
|
||||||
///
|
///
|
||||||
/// "assignee": Comma-separated list of assignees to list.
|
/// "assignee": Comma-separated list of assignees to include in
|
||||||
/// Defaults to all assignees if not set.
|
/// the list. The empty string includes issues with no assignee.
|
||||||
|
/// Example: "assignee=seb," lists issues assigned to "seb" and
|
||||||
|
/// issues without an assignee. Defaults to include all issues.
|
||||||
///
|
///
|
||||||
/// "tag": Comma-separated list of tags to include or exclude
|
/// "tag": Comma-separated list of tags to include, or exclude
|
||||||
/// (if prefixed with "-"). If omitted, defaults to including
|
/// if prefixed with "-". Example: "tag=bug,-docs" shows issues
|
||||||
/// all tags and excluding none.
|
/// that are tagged "bug" and not tagged "docs". Defaults to
|
||||||
|
/// including all tags and excluding none.
|
||||||
///
|
///
|
||||||
#[arg(default_value_t = String::from("state=New,Backlog,Blocked,InProgress"))]
|
/// "done-time": Time range of issue completion, in the form
|
||||||
filter: String,
|
/// "[START]..[END]". Includes issues that were marked Done
|
||||||
|
/// between START and END. START and END are both in RFC 3339
|
||||||
|
/// format, e.g. "YYYY-MM-DDTHH:MM:SS[+-]HH:MM". If START
|
||||||
|
/// is omitted, defaults to the beginning of time. If END is
|
||||||
|
/// omitted, defaults to the end of time.
|
||||||
|
filter: Vec<String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Create a new issue.
|
/// Create a new issue.
|
||||||
|
|
@ -85,7 +94,7 @@ enum Commands {
|
||||||
tag: Option<String>,
|
tag: Option<String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Set the `done_time` of the Issue.
|
/// Get or set the `done_time` of the Issue.
|
||||||
DoneTime {
|
DoneTime {
|
||||||
issue_id: String,
|
issue_id: String,
|
||||||
done_time: Option<String>,
|
done_time: Option<String>,
|
||||||
|
|
@ -99,7 +108,14 @@ fn handle_command(
|
||||||
match &args.command {
|
match &args.command {
|
||||||
Commands::List { filter } => {
|
Commands::List { filter } => {
|
||||||
let issues = entomologist::database::read_issues_database(issues_database_source)?;
|
let issues = entomologist::database::read_issues_database(issues_database_source)?;
|
||||||
let filter = entomologist::Filter::new_from_str(filter)?;
|
let filter = {
|
||||||
|
let mut f = entomologist::Filter::new();
|
||||||
|
for filter_str in filter {
|
||||||
|
f.parse(filter_str)?;
|
||||||
|
}
|
||||||
|
f
|
||||||
|
};
|
||||||
|
|
||||||
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>,
|
||||||
|
|
|
||||||
110
src/lib.rs
110
src/lib.rs
|
|
@ -6,6 +6,8 @@ pub mod git;
|
||||||
pub mod issue;
|
pub mod issue;
|
||||||
pub mod issues;
|
pub mod issues;
|
||||||
|
|
||||||
|
use crate::issue::State;
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum ParseFilterError {
|
pub enum ParseFilterError {
|
||||||
#[error("Failed to parse filter")]
|
#[error("Failed to parse filter")]
|
||||||
|
|
@ -30,9 +32,8 @@ pub struct Filter<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Filter<'a> {
|
impl<'a> Filter<'a> {
|
||||||
pub fn new_from_str(filter_str: &'a str) -> Result<Filter<'a>, ParseFilterError> {
|
pub fn new() -> Filter<'a> {
|
||||||
use crate::issue::State;
|
Self {
|
||||||
let mut f = Filter {
|
|
||||||
include_states: std::collections::HashSet::<crate::issue::State>::from([
|
include_states: std::collections::HashSet::<crate::issue::State>::from([
|
||||||
State::InProgress,
|
State::InProgress,
|
||||||
State::Blocked,
|
State::Blocked,
|
||||||
|
|
@ -44,70 +45,73 @@ impl<'a> Filter<'a> {
|
||||||
exclude_tags: std::collections::HashSet::<&'a str>::new(),
|
exclude_tags: std::collections::HashSet::<&'a str>::new(),
|
||||||
start_done_time: None,
|
start_done_time: None,
|
||||||
end_done_time: None,
|
end_done_time: None,
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for filter_chunk_str in filter_str.split(" ") {
|
pub fn parse(&mut self, filter_str: &'a str) -> Result<(), ParseFilterError> {
|
||||||
let tokens: Vec<&str> = filter_chunk_str.split("=").collect();
|
let tokens: Vec<&str> = filter_str.split("=").collect();
|
||||||
if tokens.len() != 2 {
|
if tokens.len() != 2 {
|
||||||
return Err(ParseFilterError::ParseError);
|
return Err(ParseFilterError::ParseError);
|
||||||
|
}
|
||||||
|
|
||||||
|
match tokens[0] {
|
||||||
|
"state" => {
|
||||||
|
self.include_states.clear();
|
||||||
|
for s in tokens[1].split(",") {
|
||||||
|
self.include_states
|
||||||
|
.insert(crate::issue::State::from_str(s)?);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match tokens[0] {
|
"assignee" => {
|
||||||
"state" => {
|
self.include_assignees.clear();
|
||||||
f.include_states.clear();
|
for s in tokens[1].split(",") {
|
||||||
for s in tokens[1].split(",") {
|
self.include_assignees.insert(s);
|
||||||
f.include_states.insert(crate::issue::State::from_str(s)?);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
"assignee" => {
|
"tag" => {
|
||||||
f.include_assignees.clear();
|
self.include_tags.clear();
|
||||||
for s in tokens[1].split(",") {
|
self.exclude_tags.clear();
|
||||||
f.include_assignees.insert(s);
|
for s in tokens[1].split(",") {
|
||||||
}
|
if s.len() == 0 {
|
||||||
}
|
|
||||||
|
|
||||||
"tag" => {
|
|
||||||
f.include_tags.clear();
|
|
||||||
f.exclude_tags.clear();
|
|
||||||
for s in tokens[1].split(",") {
|
|
||||||
if s.len() == 0 {
|
|
||||||
return Err(ParseFilterError::ParseError);
|
|
||||||
}
|
|
||||||
if s.chars().nth(0).unwrap() == '-' {
|
|
||||||
f.exclude_tags.insert(&s[1..]);
|
|
||||||
} else {
|
|
||||||
f.include_tags.insert(s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
"done-time" => {
|
|
||||||
let times: Vec<&str> = tokens[1].split("..").collect();
|
|
||||||
if times.len() > 2 {
|
|
||||||
return Err(ParseFilterError::ParseError);
|
return Err(ParseFilterError::ParseError);
|
||||||
}
|
}
|
||||||
if times[0].len() != 0 {
|
if s.chars().nth(0).unwrap() == '-' {
|
||||||
f.start_done_time = Some(
|
self.exclude_tags.insert(&s[1..]);
|
||||||
chrono::DateTime::parse_from_rfc3339(times[0])?
|
} else {
|
||||||
.with_timezone(&chrono::Local),
|
self.include_tags.insert(s);
|
||||||
);
|
|
||||||
}
|
|
||||||
if times[1].len() != 0 {
|
|
||||||
f.end_done_time = Some(
|
|
||||||
chrono::DateTime::parse_from_rfc3339(times[1])?
|
|
||||||
.with_timezone(&chrono::Local),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_ => {
|
"done-time" => {
|
||||||
println!("unknown filter chunk '{}'", filter_chunk_str);
|
self.start_done_time = None;
|
||||||
|
self.end_done_time = None;
|
||||||
|
let times: Vec<&str> = tokens[1].split("..").collect();
|
||||||
|
if times.len() > 2 {
|
||||||
return Err(ParseFilterError::ParseError);
|
return Err(ParseFilterError::ParseError);
|
||||||
}
|
}
|
||||||
|
if times[0].len() != 0 {
|
||||||
|
self.start_done_time = Some(
|
||||||
|
chrono::DateTime::parse_from_rfc3339(times[0])?
|
||||||
|
.with_timezone(&chrono::Local),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if times[1].len() != 0 {
|
||||||
|
self.end_done_time = Some(
|
||||||
|
chrono::DateTime::parse_from_rfc3339(times[1])?
|
||||||
|
.with_timezone(&chrono::Local),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
println!("unknown filter string '{}'", filter_str);
|
||||||
|
return Err(ParseFilterError::ParseError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(f)
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue