@mbe24/99problems 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/ISSUE_TEMPLATE/01-feature.yml +65 -0
- package/.github/ISSUE_TEMPLATE/02-bug.yml +122 -0
- package/.github/ISSUE_TEMPLATE/99-custom.yml +33 -0
- package/.github/ISSUE_TEMPLATE/config.yml +1 -0
- package/.github/dependabot.yml +20 -0
- package/.github/scripts/publish.js +1 -1
- package/.github/workflows/ci.yml +32 -6
- package/.github/workflows/man-drift.yml +26 -0
- package/.github/workflows/release.yml +49 -9
- package/CONTRIBUTING.md +38 -50
- package/Cargo.lock +151 -108
- package/Cargo.toml +8 -3
- package/README.md +107 -85
- package/artifacts/binary-aarch64-apple-darwin/99problems +0 -0
- package/artifacts/binary-aarch64-unknown-linux-gnu/99problems +0 -0
- package/artifacts/binary-x86_64-apple-darwin/99problems +0 -0
- package/artifacts/binary-x86_64-pc-windows-msvc/99problems.exe +0 -0
- package/artifacts/binary-x86_64-unknown-linux-gnu/99problems +0 -0
- package/docs/man/99problems-completions.1 +31 -0
- package/docs/man/99problems-config.1 +50 -0
- package/docs/man/99problems-get.1 +114 -0
- package/docs/man/99problems-help.1 +25 -0
- package/docs/man/99problems-man.1 +32 -0
- package/docs/man/99problems.1 +52 -0
- package/npm/install.js +90 -3
- package/package.json +7 -7
- package/rust-toolchain.toml +4 -0
- package/src/cmd/config/key.rs +126 -0
- package/src/cmd/config/mod.rs +218 -0
- package/src/cmd/config/render.rs +33 -0
- package/src/cmd/config/store.rs +318 -0
- package/src/cmd/config/write.rs +173 -0
- package/src/cmd/get.rs +658 -0
- package/src/cmd/man.rs +117 -0
- package/src/cmd/mod.rs +3 -0
- package/src/config.rs +618 -118
- package/src/error.rs +254 -0
- package/src/format/json.rs +59 -18
- package/src/format/jsonl.rs +52 -0
- package/src/format/mod.rs +25 -3
- package/src/format/text.rs +73 -0
- package/src/format/yaml.rs +64 -15
- package/src/lib.rs +1 -0
- package/src/logging.rs +54 -0
- package/src/main.rs +225 -138
- package/src/model.rs +9 -1
- package/src/source/bitbucket/cloud/api.rs +67 -0
- package/src/source/bitbucket/cloud/mod.rs +178 -0
- package/src/source/bitbucket/cloud/model.rs +211 -0
- package/src/source/bitbucket/datacenter/api.rs +74 -0
- package/src/source/bitbucket/datacenter/mod.rs +181 -0
- package/src/source/bitbucket/datacenter/model.rs +327 -0
- package/src/source/bitbucket/mod.rs +90 -0
- package/src/source/bitbucket/query.rs +169 -0
- package/src/source/bitbucket/shared/auth.rs +54 -0
- package/src/source/bitbucket/shared/http.rs +59 -0
- package/src/source/bitbucket/shared/mod.rs +5 -0
- package/src/source/github/api.rs +128 -0
- package/src/source/github/mod.rs +191 -0
- package/src/source/github/model.rs +84 -0
- package/src/source/github/query.rs +50 -0
- package/src/source/gitlab/api.rs +282 -0
- package/src/source/gitlab/mod.rs +225 -0
- package/src/source/gitlab/model.rs +102 -0
- package/src/source/gitlab/query.rs +177 -0
- package/src/source/jira/api.rs +222 -0
- package/src/source/jira/mod.rs +161 -0
- package/src/source/jira/model.rs +99 -0
- package/src/source/jira/query.rs +153 -0
- package/src/source/mod.rs +65 -7
- package/tests/integration.rs +404 -33
- package/src/source/github_issues.rs +0 -227
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
use anyhow::Result;
|
|
2
|
+
|
|
3
|
+
use crate::error::AppError;
|
|
4
|
+
use crate::source::ContentKind;
|
|
5
|
+
|
|
6
|
+
#[derive(Debug)]
|
|
7
|
+
pub(super) struct JiraFilters {
|
|
8
|
+
pub(super) repo: Option<String>,
|
|
9
|
+
pub(super) kind: ContentKind,
|
|
10
|
+
pub(super) state: Option<String>,
|
|
11
|
+
pub(super) labels: Vec<String>,
|
|
12
|
+
pub(super) author: Option<String>,
|
|
13
|
+
pub(super) since: Option<String>,
|
|
14
|
+
pub(super) milestone: Option<String>,
|
|
15
|
+
pub(super) search_terms: Vec<String>,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
impl Default for JiraFilters {
|
|
19
|
+
fn default() -> Self {
|
|
20
|
+
Self {
|
|
21
|
+
repo: None,
|
|
22
|
+
kind: ContentKind::Issue,
|
|
23
|
+
state: None,
|
|
24
|
+
labels: vec![],
|
|
25
|
+
author: None,
|
|
26
|
+
since: None,
|
|
27
|
+
milestone: None,
|
|
28
|
+
search_terms: vec![],
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
pub(super) fn parse_jira_query(raw_query: &str) -> JiraFilters {
|
|
34
|
+
let mut filters = JiraFilters::default();
|
|
35
|
+
|
|
36
|
+
for token in raw_query.split_whitespace() {
|
|
37
|
+
if token == "is:issue" {
|
|
38
|
+
filters.kind = ContentKind::Issue;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if token == "is:pr" {
|
|
42
|
+
filters.kind = ContentKind::Pr;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if let Some(kind) = token.strip_prefix("type:") {
|
|
46
|
+
if kind == "issue" {
|
|
47
|
+
filters.kind = ContentKind::Issue;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if kind == "pr" {
|
|
51
|
+
filters.kind = ContentKind::Pr;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if let Some(repo) = token.strip_prefix("repo:") {
|
|
56
|
+
filters.repo = Some(repo.to_string());
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if let Some(state) = token.strip_prefix("state:") {
|
|
60
|
+
filters.state = Some(state.to_string());
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if let Some(label) = token.strip_prefix("label:") {
|
|
64
|
+
filters.labels.push(label.to_string());
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if let Some(author) = token.strip_prefix("author:") {
|
|
68
|
+
filters.author = Some(author.to_string());
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if let Some(since) = token.strip_prefix("created:>=") {
|
|
72
|
+
filters.since = Some(since.to_string());
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if let Some(milestone) = token.strip_prefix("milestone:") {
|
|
76
|
+
filters.milestone = Some(milestone.to_string());
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
filters.search_terms.push(token.to_string());
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
filters
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
pub(super) fn build_jql(filters: &JiraFilters) -> Result<String> {
|
|
87
|
+
let project = filters.repo.as_deref().ok_or_else(|| {
|
|
88
|
+
AppError::usage("No repo: found in query. Use --repo with Jira project key.")
|
|
89
|
+
})?;
|
|
90
|
+
|
|
91
|
+
let mut clauses = vec![format!("project = {}", quote_jql(project))];
|
|
92
|
+
if let Some(state) = filters.state.as_deref() {
|
|
93
|
+
match state.to_ascii_lowercase().as_str() {
|
|
94
|
+
"open" | "opened" => clauses.push("statusCategory != Done".into()),
|
|
95
|
+
"closed" => clauses.push("statusCategory = Done".into()),
|
|
96
|
+
"all" => {}
|
|
97
|
+
_ => clauses.push(format!("status = {}", quote_jql(state))),
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for label in &filters.labels {
|
|
102
|
+
clauses.push(format!("labels = {}", quote_jql(label)));
|
|
103
|
+
}
|
|
104
|
+
if let Some(author) = &filters.author {
|
|
105
|
+
clauses.push(format!("reporter = {}", quote_jql(author)));
|
|
106
|
+
}
|
|
107
|
+
if let Some(since) = &filters.since {
|
|
108
|
+
clauses.push(format!("created >= {}", quote_jql(since)));
|
|
109
|
+
}
|
|
110
|
+
if let Some(milestone) = &filters.milestone {
|
|
111
|
+
clauses.push(format!("fixVersion = {}", quote_jql(milestone)));
|
|
112
|
+
}
|
|
113
|
+
if !filters.search_terms.is_empty() {
|
|
114
|
+
clauses.push(format!(
|
|
115
|
+
"text ~ {}",
|
|
116
|
+
quote_jql(&filters.search_terms.join(" "))
|
|
117
|
+
));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
Ok(clauses.join(" AND "))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
fn quote_jql(value: &str) -> String {
|
|
124
|
+
format!("\"{}\"", value.replace('"', "\\\""))
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
#[cfg(test)]
|
|
128
|
+
mod tests {
|
|
129
|
+
use super::*;
|
|
130
|
+
|
|
131
|
+
#[test]
|
|
132
|
+
fn parse_jira_query_extracts_filters() {
|
|
133
|
+
let q = parse_jira_query(
|
|
134
|
+
"repo:CLOUD state:closed label:api author:alice created:>=2024-01-01 milestone:v1 text",
|
|
135
|
+
);
|
|
136
|
+
assert_eq!(q.repo.as_deref(), Some("CLOUD"));
|
|
137
|
+
assert!(matches!(q.kind, ContentKind::Issue));
|
|
138
|
+
assert_eq!(q.state.as_deref(), Some("closed"));
|
|
139
|
+
assert_eq!(q.labels, vec!["api"]);
|
|
140
|
+
assert_eq!(q.author.as_deref(), Some("alice"));
|
|
141
|
+
assert_eq!(q.since.as_deref(), Some("2024-01-01"));
|
|
142
|
+
assert_eq!(q.milestone.as_deref(), Some("v1"));
|
|
143
|
+
assert_eq!(q.search_terms, vec!["text"]);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
#[test]
|
|
147
|
+
fn build_jql_maps_closed_to_done_category() {
|
|
148
|
+
let q = parse_jira_query("repo:CLOUD state:closed");
|
|
149
|
+
let jql = build_jql(&q).unwrap();
|
|
150
|
+
assert!(jql.contains("project = \"CLOUD\""));
|
|
151
|
+
assert!(jql.contains("statusCategory = Done"));
|
|
152
|
+
}
|
|
153
|
+
}
|
package/src/source/mod.rs
CHANGED
|
@@ -1,12 +1,69 @@
|
|
|
1
1
|
use crate::model::Conversation;
|
|
2
2
|
use anyhow::Result;
|
|
3
3
|
|
|
4
|
-
pub mod
|
|
4
|
+
pub mod bitbucket;
|
|
5
|
+
pub mod github;
|
|
6
|
+
pub mod gitlab;
|
|
7
|
+
pub mod jira;
|
|
5
8
|
|
|
6
|
-
/// A pluggable data source that fetches issue conversations.
|
|
9
|
+
/// A pluggable data source that fetches issue/PR conversations.
|
|
7
10
|
pub trait Source {
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
/// Fetch issue or pull request conversations for a request target and emit
|
|
12
|
+
/// each conversation incrementally.
|
|
13
|
+
///
|
|
14
|
+
/// # Errors
|
|
15
|
+
///
|
|
16
|
+
/// Returns an error when request validation fails, authentication fails,
|
|
17
|
+
/// or the remote platform returns a non-success/invalid response.
|
|
18
|
+
fn fetch_stream(
|
|
19
|
+
&self,
|
|
20
|
+
req: &FetchRequest,
|
|
21
|
+
emit: &mut dyn FnMut(Conversation) -> Result<()>,
|
|
22
|
+
) -> Result<usize>;
|
|
23
|
+
|
|
24
|
+
/// Fetch all conversations and collect them into memory.
|
|
25
|
+
///
|
|
26
|
+
/// # Errors
|
|
27
|
+
///
|
|
28
|
+
/// Returns an error when request validation fails, authentication fails,
|
|
29
|
+
/// remote calls fail, or emitted conversations cannot be collected.
|
|
30
|
+
fn fetch(&self, req: &FetchRequest) -> Result<Vec<Conversation>> {
|
|
31
|
+
let mut conversations = Vec::new();
|
|
32
|
+
self.fetch_stream(req, &mut |conversation| {
|
|
33
|
+
conversations.push(conversation);
|
|
34
|
+
Ok(())
|
|
35
|
+
})?;
|
|
36
|
+
Ok(conversations)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
41
|
+
pub enum ContentKind {
|
|
42
|
+
Issue,
|
|
43
|
+
Pr,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
#[derive(Debug, Clone)]
|
|
47
|
+
pub enum FetchTarget {
|
|
48
|
+
Search {
|
|
49
|
+
raw_query: String,
|
|
50
|
+
},
|
|
51
|
+
Id {
|
|
52
|
+
repo: String,
|
|
53
|
+
id: String,
|
|
54
|
+
kind: ContentKind,
|
|
55
|
+
allow_fallback_to_pr: bool,
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#[derive(Debug, Clone)]
|
|
60
|
+
pub struct FetchRequest {
|
|
61
|
+
pub target: FetchTarget,
|
|
62
|
+
pub per_page: u32,
|
|
63
|
+
pub token: Option<String>,
|
|
64
|
+
pub account_email: Option<String>,
|
|
65
|
+
pub include_comments: bool,
|
|
66
|
+
pub include_review_comments: bool,
|
|
10
67
|
}
|
|
11
68
|
|
|
12
69
|
/// Parsed search parameters passed to a Source.
|
|
@@ -22,6 +79,7 @@ pub struct Query {
|
|
|
22
79
|
impl Query {
|
|
23
80
|
/// Build a query by merging a raw string with convenience shorthands.
|
|
24
81
|
/// Shorthands are only appended if their qualifier isn't already present in the raw string.
|
|
82
|
+
#[must_use]
|
|
25
83
|
#[allow(clippy::too_many_arguments)]
|
|
26
84
|
pub fn build(
|
|
27
85
|
raw: Option<String>,
|
|
@@ -100,10 +158,10 @@ mod tests {
|
|
|
100
158
|
|
|
101
159
|
fn build(raw: Option<&str>, kind: &str, repo: Option<&str>, state: Option<&str>) -> Query {
|
|
102
160
|
Query::build(
|
|
103
|
-
raw.map(
|
|
161
|
+
raw.map(std::convert::Into::into),
|
|
104
162
|
kind,
|
|
105
|
-
repo.map(
|
|
106
|
-
state.map(
|
|
163
|
+
repo.map(std::convert::Into::into),
|
|
164
|
+
state.map(std::convert::Into::into),
|
|
107
165
|
None,
|
|
108
166
|
None,
|
|
109
167
|
None,
|