Repository: ankitpokhrel/jira-cli
Pull request #103 (2021-05-30 03:37:15)

feat: Support raw JQL
You can execute raw JQL within a given project context using `--jql/-q` option. For instance, the following command will list issues in current project whose summary has a word `cli`.

```sh
$ jira issue list -q "summary ~ cli"
```




Patch:
diff --git a/README.md b/README.md
index 41251bc..75ef073 100644
--- a/README.md
+++ b/README.md
@@ -77,6 +77,11 @@ $ jira issue list -s"To Do"
 
 # List recent issues in plain mode 
 $ jira issue list --plain
+
+# You can execute raw JQL within a given project context using `--jql/-q` option.
+# For instance, the following command will list issues in current project whose
+# summary has a word cli.
+$ jira issue list -q "summary ~ cli"
 ```
 
 Check some more examples/use-cases below.
diff --git a/internal/cmd/issue/list/list.go b/internal/cmd/issue/list/list.go
index 61118b0..76bc0b0 100644
--- a/internal/cmd/issue/list/list.go
+++ b/internal/cmd/issue/list/list.go
@@ -140,6 +140,7 @@ func SetFlags(cmd *cobra.Command) {
 	cmd.Flags().String("updated-after", "", "Filter by issues updated after certain date")
 	cmd.Flags().String("created-before", "", "Filter by issues created before certain date")
 	cmd.Flags().String("updated-before", "", "Filter by issues updated before certain date")
+	cmd.Flags().StringP("jql", "q", "", "Run a raw JQL query in a given project context")
 	cmd.Flags().Bool("reverse", false, "Reverse the display order (default is DESC)")
 	cmd.Flags().Uint("limit", 100, "Number of results to return")
 	cmd.Flags().Bool("plain", false, "Display output in plain mode")
diff --git a/internal/query/issue.go b/internal/query/issue.go
index f141239..4b04f04 100644
--- a/internal/query/issue.go
+++ b/internal/query/issue.go
@@ -66,6 +66,9 @@ func (i *Issue) Get() string {
 	if i.params.debug {
 		fmt.Printf("JQL: %s\n", q.String())
 	}
+	if i.params.jql != "" {
+		q.And(func() { q.Raw(i.params.jql) })
+	}
 	return q.String()
 }
 
@@ -137,6 +140,7 @@ type IssueParams struct {
 	UpdatedAfter  string
 	CreatedBefore string
 	UpdatedBefore string
+	jql           string
 	Labels        []string
 	Reverse       bool
 	Limit         uint
@@ -150,6 +154,7 @@ func (ip *IssueParams) init(flags FlagParser) error {
 	stringParams := []string{
 		"resolution", "type", "status", "priority", "reporter", "assignee", "component",
 		"created", "created-after", "created-before", "updated", "updated-after", "updated-before",
+		"jql",
 	}
 
 	boolParamsMap := make(map[string]bool)
@@ -227,6 +232,8 @@ func (ip *IssueParams) setStringParams(paramsMap map[string]string) {
 			ip.UpdatedAfter = v
 		case "updated-before":
 			ip.UpdatedBefore = v
+		case "jql":
+			ip.jql = v
 		}
 	}
 }
diff --git a/pkg/jql/jql.go b/pkg/jql/jql.go
index 8a5fcdf..8169004 100644
--- a/pkg/jql/jql.go
+++ b/pkg/jql/jql.go
@@ -156,6 +156,15 @@ func (j *JQL) Or(fn GroupFunc) *JQL {
 	return j
 }
 
+// Raw sets the passed JQL query along with project context.
+func (j *JQL) Raw(q string) *JQL {
+	if q == "" {
+		return j
+	}
+	j.filters = append(j.filters, q)
+	return j
+}
+
 // String returns the constructed query.
 func (j *JQL) String() string {
 	return j.compile()


Test patch:
diff --git a/internal/query/issue_test.go b/internal/query/issue_test.go
index 42431d8..608703b 100644
--- a/internal/query/issue_test.go
+++ b/internal/query/issue_test.go
@@ -31,6 +31,7 @@ type issueFlagParser struct {
 	createdBefore string
 	updatedAfter  string
 	updatedBefore string
+	jql           string
 }
 
 func (tfp issueFlagParser) GetBool(name string) (bool, error) {
@@ -52,6 +53,7 @@ func (tfp issueFlagParser) GetBool(name string) (bool, error) {
 	return true, nil
 }
 
+//nolint:gocyclo
 func (tfp issueFlagParser) GetString(name string) (string, error) {
 	if tfp.err.resolution && name == "resolution" {
 		return "", fmt.Errorf("oops! couldn't fetch resolution flag")
@@ -68,6 +70,9 @@ func (tfp issueFlagParser) GetString(name string) (string, error) {
 	if tfp.emptyType && name == "type" {
 		return "", nil
 	}
+	if name == "jql" {
+		return tfp.jql, nil
+	}
 	if strings.HasPrefix(name, "created") {
 		if tfp.withCreated {
 			switch name {
@@ -376,6 +381,17 @@ func TestIssueGet(t *testing.T) {
 				`AND updatedDate>"2020-11-31" AND updatedDate<"2020-12-31" ` +
 				`ORDER BY updated ASC`,
 		},
+		{
+			name: "query with jql parameter",
+			initialize: func() *Issue {
+				i, err := NewIssue("TEST", &issueFlagParser{jql: "summary ~ cli OR x = y"})
+				assert.NoError(t, err)
+				return i
+			},
+			expected: `project="TEST" AND issue IN issueHistory() AND issue IN watchedIssues() AND ` +
+				`type="test" AND resolution="test" AND status="test" AND priority="test" AND reporter="test" ` +
+				`AND assignee="test" AND component="test" AND summary ~ cli OR x = y ORDER BY lastViewed ASC`,
+		},
 	}
 
 	for _, tc := range cases {
diff --git a/pkg/jql/jql_test.go b/pkg/jql/jql_test.go
index 6de73fa..3add3f9 100644
--- a/pkg/jql/jql_test.go
+++ b/pkg/jql/jql_test.go
@@ -210,6 +210,32 @@ func TestJQL(t *testing.T) {
 			},
 			expected: "project=\"TEST\" OR type=\"Story\" OR labels IN (\"first\", \"second\", \"third\")",
 		},
+		{
+			name: "it queries with raw jql",
+			initialize: func() *JQL {
+				jql := NewJQL("TEST")
+				jql.And(func() {
+					jql.
+						FilterBy("type", "Story").
+						FilterBy("resolution", "Done").
+						Raw("summary !~ cli AND priority = high")
+				})
+				return jql
+			},
+			expected: "project=\"TEST\" AND type=\"Story\" AND resolution=\"Done\" AND summary !~ cli AND priority = high",
+		},
+		{
+			name: "it queries with raw jql and or filter",
+			initialize: func() *JQL {
+				jql := NewJQL("TEST")
+				jql.Or(func() {
+					jql.FilterBy("type", "Story").
+						Raw("summary ~ cli")
+				})
+				return jql
+			},
+			expected: "project=\"TEST\" OR type=\"Story\" OR summary ~ cli",
+		},
 	}
 
 	for _, tc := range cases {