Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,12 @@ The `list` command lets you search and navigate the issues. The issues are sorte
# List recent issues
$ jira issue list

# List issues with pagination (default: 100 items)
$ jira issue list --paginate 50

# Get 20 items starting from offset 10
$ jira issue list --paginate 10:20

# List issues created in last 7 days
$ jira issue list --created -7d

Expand Down
2 changes: 1 addition & 1 deletion api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ func ProxySearch(c *jira.Client, jql string, from, limit uint) (*jira.SearchResu
if it == jira.InstallationTypeLocal {
issues, err = c.SearchV2(jql, from, limit)
} else {
issues, err = c.Search(jql, limit)
issues, err = c.Search(jql, from, limit)
}

return issues, err
Expand Down
4 changes: 2 additions & 2 deletions internal/cmd/epic/list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ func singleEpicView(flags query.FlagParser, key, project, projectType, server st
q.Params().Parent = key
q.Params().IssueType = ""

resp, err = client.Search(q.Get(), q.Params().Limit)
resp, err = client.Search(q.Get(), q.Params().From, q.Params().Limit)
} else {
resp, err = client.EpicIssues(key, q.Get(), q.Params().From, q.Params().Limit)
}
Expand Down Expand Up @@ -209,7 +209,7 @@ func epicExplorerView(cmd *cobra.Command, flags query.FlagParser, project, proje
q.Params().Parent = key
q.Params().IssueType = ""

resp, err = client.Search(q.Get(), q.Params().Limit)
resp, err = client.Search(q.Get(), q.Params().From, q.Params().Limit)
} else {
resp, err = client.EpicIssues(key, "", q.Params().From, q.Params().Limit)
}
Expand Down
70 changes: 66 additions & 4 deletions pkg/jira/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,79 @@ import (
"net/url"
)

const (
// maxSearchPageSize is the maximum number of results per page for the Jira search API.
maxSearchPageSize = 100
)

// SearchResult struct holds response from /search endpoint.
type SearchResult struct {
IsLast bool `json:"isLast"`
NextPageToken string `json:"nextPageToken"`
Issues []*Issue `json:"issues"`
}

// Search searches for issues using v3 version of the Jira GET /search endpoint.
func (c *Client) Search(jql string, limit uint) (*SearchResult, error) {
path := fmt.Sprintf("/search/jql?jql=%s&maxResults=%d&fields=*all", url.QueryEscape(jql), limit)
return c.search(path, apiVersion3)
// Search searches for issues using v3 version of the Jira GET /search/jql endpoint.
//
// It supports cursor-based pagination using nextPageToken from the API response.
// The from parameter specifies how many items to skip, and limit specifies the
// maximum number of items to return. For large offsets, multiple API requests
// may be made internally to reach the requested position.
func (c *Client) Search(jql string, from, limit uint) (*SearchResult, error) {
var (
allIssues []*Issue
skipped uint
nextToken string
)

// Determine API page size: use requested limit if no offset, otherwise use max for efficiency.
// When we need to skip items (from > 0), larger pages reduce the number of API calls needed.
pageSize := limit
if from > 0 && pageSize < maxSearchPageSize {
pageSize = maxSearchPageSize
}

for {
path := fmt.Sprintf("/search/jql?jql=%s&maxResults=%d&fields=*all", url.QueryEscape(jql), pageSize)
if nextToken != "" {
path += "&nextPageToken=" + url.QueryEscape(nextToken)
}

result, err := c.search(path, apiVersion3)
if err != nil {
return nil, err
}

for _, issue := range result.Issues {
// Skip items until we reach the 'from' offset
if skipped < from {
skipped++
continue
}

allIssues = append(allIssues, issue)

// Stop if we've collected enough items
if uint(len(allIssues)) >= limit {
return &SearchResult{
Issues: allIssues,
IsLast: false, // We stopped early, so there may be more results
}, nil
}
}

// If this is the last page, we're done
if result.IsLast || result.NextPageToken == "" {
break
}

nextToken = result.NextPageToken
}

return &SearchResult{
Issues: allIssues,
IsLast: true,
}, nil
}

// SearchV2 searches an issues using v2 version of the Jira GET /search endpoint.
Expand Down
Loading