Skip to content

Commit f32f2d8

Browse files
authored
feat: Allow project filter in raw jql (#395)
1 parent d75d29d commit f32f2d8

File tree

5 files changed

+129
-9
lines changed

5 files changed

+129
-9
lines changed

internal/cmd/issue/list/list.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@ $ jira issue list --plain --no-truncate
4747
$ jira issue list -tEpic -sDone
4848
4949
# List issues in status other than "Open" and is assigned to no one
50-
$ jira issue list -s~Open -ax`
50+
$ jira issue list -s~Open -ax
51+
52+
# List issues from all projects
53+
$ jira issue list -q"project IS NOT EMPTY"`
5154
)
5255

5356
// NewCmdList is a list command.

internal/query/issue.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,25 @@ func NewIssue(project string, flags FlagParser) (*Issue, error) {
3434

3535
// Get returns constructed jql query.
3636
func (i *Issue) Get() string {
37+
var q *jql.JQL
38+
39+
defer func() {
40+
if i.params.debug {
41+
fmt.Printf("JQL: %s\n", q.String())
42+
}
43+
}()
44+
3745
q, obf := jql.NewJQL(i.Project), i.params.OrderBy
3846
if obf == "created" &&
3947
(i.params.Updated != "" || i.params.UpdatedBefore != "" || i.params.UpdatedAfter != "") &&
4048
(i.params.Created == "" && i.params.CreatedBefore == "" && i.params.CreatedAfter == "") {
4149
obf = "updated"
4250
}
51+
52+
if i.params.jql != "" {
53+
q.Raw(i.params.jql)
54+
}
55+
4356
q.And(func() {
4457
if i.params.Latest {
4558
q.History()
@@ -65,17 +78,13 @@ func (i *Issue) Get() string {
6578
q.In("labels", i.params.Labels...)
6679
}
6780
})
81+
6882
if i.params.Reverse {
6983
q.OrderBy(obf, jql.DirectionAscending)
7084
} else {
7185
q.OrderBy(obf, jql.DirectionDescending)
7286
}
73-
if i.params.debug {
74-
fmt.Printf("JQL: %s\n", q.String())
75-
}
76-
if i.params.jql != "" {
77-
q.And(func() { q.Raw(i.params.jql) })
78-
}
87+
7988
return q.String()
8089
}
8190

internal/query/issue_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -396,9 +396,9 @@ func TestIssueGet(t *testing.T) {
396396
assert.NoError(t, err)
397397
return i
398398
},
399-
expected: `project="TEST" AND issue IN issueHistory() AND issue IN watchedIssues() AND ` +
399+
expected: `project="TEST" AND summary ~ cli OR x = y AND issue IN issueHistory() AND issue IN watchedIssues() AND ` +
400400
`type="test" AND resolution="test" AND status="test" AND priority="test" AND reporter="test" ` +
401-
`AND assignee="test" AND component="test" AND parent="test" AND summary ~ cli OR x = y ORDER BY lastViewed ASC`,
401+
`AND assignee="test" AND component="test" AND parent="test" ORDER BY lastViewed ASC`,
402402
},
403403
}
404404

pkg/jql/jql.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package jql
22

33
import (
44
"fmt"
5+
"regexp"
56
"strings"
67
)
78

@@ -158,9 +159,13 @@ func (j *JQL) Or(fn GroupFunc) *JQL {
158159

159160
// Raw sets the passed JQL query along with project context.
160161
func (j *JQL) Raw(q string) *JQL {
162+
q = strings.TrimSpace(q)
161163
if q == "" {
162164
return j
163165
}
166+
if hasProjectFilter(q) {
167+
j.filters = j.filters[1:]
168+
}
164169
j.filters = append(j.filters, q)
165170
return j
166171
}
@@ -196,5 +201,12 @@ func (j *JQL) compile() string {
196201
if j.orderBy != "" {
197202
q += " " + j.orderBy
198203
}
204+
199205
return q
200206
}
207+
208+
func hasProjectFilter(str string) bool {
209+
regx := "(?i)((project)[\\s]*?={0,1}\\b)[^'.']"
210+
m, _ := regexp.MatchString(regx, str)
211+
return m
212+
}

pkg/jql/jql_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,20 @@ func TestJQL(t *testing.T) {
224224
},
225225
expected: "project=\"TEST\" AND type=\"Story\" AND resolution=\"Done\" AND summary !~ cli AND priority = high",
226226
},
227+
{
228+
name: "it queries with raw jql and project filter",
229+
initialize: func() *JQL {
230+
jql := NewJQL("TEST")
231+
jql.And(func() {
232+
jql.
233+
FilterBy("type", "Story").
234+
FilterBy("resolution", "Done").
235+
Raw("summary !~ cli AND project = TEST1")
236+
})
237+
return jql
238+
},
239+
expected: "type=\"Story\" AND resolution=\"Done\" AND summary !~ cli AND project = TEST1",
240+
},
227241
{
228242
name: "it queries with raw jql and or filter",
229243
initialize: func() *JQL {
@@ -236,6 +250,18 @@ func TestJQL(t *testing.T) {
236250
},
237251
expected: "project=\"TEST\" OR type=\"Story\" OR summary ~ cli",
238252
},
253+
{
254+
name: "it queries with raw jql and project filter in or condition",
255+
initialize: func() *JQL {
256+
jql := NewJQL("TEST")
257+
jql.Or(func() {
258+
jql.FilterBy("type", "Story").
259+
Raw("summary ~ cli AND project IN (TEST1,TEST2)")
260+
})
261+
return jql
262+
},
263+
expected: "type=\"Story\" OR summary ~ cli AND project IN (TEST1,TEST2)",
264+
},
239265
}
240266

241267
for _, tc := range cases {
@@ -249,3 +275,73 @@ func TestJQL(t *testing.T) {
249275
})
250276
}
251277
}
278+
279+
func TestHasProject(t *testing.T) {
280+
cases := []struct {
281+
input string
282+
expected bool
283+
}{
284+
{
285+
input: "project=",
286+
expected: true,
287+
},
288+
{
289+
input: "project = TEST",
290+
expected: true,
291+
},
292+
{
293+
input: "project = TEST",
294+
expected: true,
295+
},
296+
{
297+
input: " assigned = abc and PROJECT = TEST ",
298+
expected: true,
299+
},
300+
{
301+
input: "assigned = abc and project = TEST and project.property=abc",
302+
expected: true,
303+
},
304+
{
305+
input: "PROJECT IS NOT EMPTY AND assignee IN (currentUser())",
306+
expected: true,
307+
},
308+
{
309+
input: "PROJECT IN (TEST, TEST1) AND assignee IN (currentUser())",
310+
expected: true,
311+
},
312+
{
313+
input: "PROJECT NOT IN (TEST,TEST1) AND assignee IN (currentUser())",
314+
expected: true,
315+
},
316+
{
317+
input: "PROJECT != TEST AND projectType=\"classic\" AND assignee IS EMPTY",
318+
expected: true,
319+
},
320+
{
321+
input: "project",
322+
expected: false,
323+
},
324+
{
325+
input: "projectType",
326+
expected: false,
327+
},
328+
{
329+
input: "project.property = ABC",
330+
expected: false,
331+
},
332+
{
333+
input: "projectType=\"classic\" AND type=\"Story\" AND assignee IS EMPTY",
334+
expected: false,
335+
},
336+
}
337+
338+
for _, tc := range cases {
339+
tc := tc
340+
341+
t.Run("", func(t *testing.T) {
342+
t.Parallel()
343+
344+
assert.Equal(t, tc.expected, hasProjectFilter(tc.input))
345+
})
346+
}
347+
}

0 commit comments

Comments
 (0)