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
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,28 @@ $ jira issue list --plain
# List recent issues in raw JSON format
$ jira issue list --raw

# List issues in JSON with human-readable custom field names
$ jira issue list --json

# Filter JSON output to specific fields returned by the API
$ jira issue list --json --json-filter "key,fields.summary,fields.status.name,fields.storyPoints"

# Use customfield IDs in filter to bypass naming collisions
$ jira issue list --json --json-filter "key,fields.customfield_10001"

# Suppress collision warnings if you don't care about skipped fields
$ jira issue list --json --no-warnings

# Fetch only specific fields from Jira API to improve performance
# Note: For list operations, --api-fields can only reduce fields returned, not add new ones
$ jira issue list --json --api-fields "key,summary,description"

# Request custom fields (if they're in the default API response)
$ jira issue list --json --api-fields "key,summary,customfield_10001"

# Combine both for maximum API efficiency and output precision
$ jira issue list --json --api-fields "key,summary,status" --json-filter "key,fields.status.statusCategory.name"

# List recent issues in csv format
$ jira issue list --csv

Expand Down Expand Up @@ -437,6 +459,27 @@ not be the latest one if you for some reason have more than 5k comments in a tic
```sh
# Show 5 recent comments when viewing the issue
$ jira issue view ISSUE-1 --comments 5

# Get the raw JSON data
$ jira issue view ISSUE-1 --raw

# Get raw JSON with only specific fields
$ jira issue view ISSUE-1 --raw --api-fields "key,summary,status"

# Get JSON output with human-readable custom field names
$ jira issue view ISSUE-1 --json

# Filter JSON to specific fields returned by the API
$ jira issue view ISSUE-1 --json --json-filter "key,fields.summary,fields.storyPoints,fields.status.name"

# Suppress collision warnings
$ jira issue view ISSUE-1 --json --no-warnings

# Fetch only specific fields from Jira API to improve performance
$ jira issue view ISSUE-1 --json --api-fields "key,summary,Story Points,status"

# Combine both for maximum API efficiency and output precision
$ jira issue view ISSUE-1 --json --api-fields "key,summary,status" --json-filter "key,fields.status.statusCategory.name"
```

#### Link
Expand Down
14 changes: 8 additions & 6 deletions api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,13 @@ func ProxyCreate(c *jira.Client, cr *jira.CreateRequest) (*jira.CreateResponse,
}

// ProxyGetIssueRaw executes the same request as ProxyGetIssue but returns raw API response body string.
func ProxyGetIssueRaw(c *jira.Client, key string) (string, error) {
// The fields parameter restricts which fields to fetch from Jira. Empty string returns all fields.
func ProxyGetIssueRaw(c *jira.Client, key string, fields string) (string, error) {
it := viper.GetString("installation")
if it == jira.InstallationTypeLocal {
return c.GetIssueV2Raw(key)
return c.GetIssueV2Raw(key, fields)
}
return c.GetIssueRaw(key)
return c.GetIssueRaw(key, fields)
}

// ProxyGetIssue uses either a v2 or v3 version of the Jira GET /issue/{key}
Expand All @@ -127,7 +128,8 @@ func ProxyGetIssue(c *jira.Client, key string, opts ...filter.Filter) (*jira.Iss
// ProxySearch uses either a v2 or v3 version of the Jira GET /search endpoint
// to search for the relevant issues based on configured installation type.
// Defaults to v3 if installation type is not defined in the config.
func ProxySearch(c *jira.Client, jql string, from, limit uint) (*jira.SearchResult, error) {
// The fields parameter controls which fields to fetch from Jira. Empty string uses defaults.
func ProxySearch(c *jira.Client, jql string, from, limit uint, fields string) (*jira.SearchResult, error) {
var (
issues *jira.SearchResult
err error
Expand All @@ -136,9 +138,9 @@ func ProxySearch(c *jira.Client, jql string, from, limit uint) (*jira.SearchResu
it := viper.GetString("installation")

if it == jira.InstallationTypeLocal {
issues, err = c.SearchV2(jql, from, limit)
issues, err = c.SearchV2(jql, from, limit, fields)
} else {
issues, err = c.Search(jql, limit)
issues, err = c.Search(jql, limit, fields)
}

return issues, err
Expand Down
6 changes: 3 additions & 3 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().Limit, "")
} else {
resp, err = client.EpicIssues(key, q.Get(), q.Params().From, q.Params().Limit)
}
Expand Down Expand Up @@ -181,7 +181,7 @@ func epicExplorerView(cmd *cobra.Command, flags query.FlagParser, project, proje
s := cmdutil.Info("Fetching epics...")
defer s.Stop()

resp, err := api.ProxySearch(client, q.Get(), q.Params().From, q.Params().Limit)
resp, err := api.ProxySearch(client, q.Get(), q.Params().From, q.Params().Limit, "")
if err != nil {
return nil, err
}
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().Limit, "")
} else {
resp, err = client.EpicIssues(key, "", q.Params().From, q.Params().Limit)
}
Expand Down
105 changes: 101 additions & 4 deletions internal/cmd/issue/list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/spf13/viper"

"github.com/ankitpokhrel/jira-cli/api"
"github.com/ankitpokhrel/jira-cli/internal/cmdcommon"
"github.com/ankitpokhrel/jira-cli/internal/cmdutil"
"github.com/ankitpokhrel/jira-cli/internal/query"
"github.com/ankitpokhrel/jira-cli/internal/view"
Expand Down Expand Up @@ -56,6 +57,19 @@ $ jira issue list --plain --delimeter "|"
# List issues as raw JSON data
$ jira issue list --raw

# List issues in JSON with human-readable custom field names
$ jira issue list --json

# List issues in JSON, and filter output to specific nested paths (only restricts output, cannot add additional fields)
$ jira issue list --json --json-filter "key,fields.summary,fields.assignee.displayName"

# List issues in JSON, requesting only specific fields from API
# Note: --api-fields can only reduce fields returned, not add new ones
$ jira issue list --json --api-fields "key,summary,description"

# Combine both for maximum API efficiency and output precision
$ jira issue list --json --api-fields "key,summary,status" --json-filter "key,fields.status.statusCategory.name"

# List issues of type "Epic" in status "Done"
$ jira issue list -tEpic -sDone

Expand Down Expand Up @@ -109,6 +123,25 @@ func loadList(cmd *cobra.Command, args []string) {
cmdutil.ExitIfError(cmd.Flags().Set("jql", searchQuery))
}

// Check for --json and --raw flags to determine API field filtering
jsonOutput, err := cmd.Flags().GetBool("json")
cmdutil.ExitIfError(err)

rawOutput, err := cmd.Flags().GetBool("raw")
cmdutil.ExitIfError(err)

var apiFields string
if jsonOutput || rawOutput {
// For --json or --raw output, use --api-fields for API-level filtering (optional)
fieldsStr, err := cmd.Flags().GetString("api-fields")
cmdutil.ExitIfError(err)

// Translate human-readable field names to field IDs
if fieldsStr != "" {
apiFields = cmdcommon.TranslateFieldNames(fieldsStr)
}
}

issues, err := func() ([]*jira.Issue, error) {
s := cmdutil.Info("Fetching issues...")
defer s.Stop()
Expand All @@ -118,7 +151,7 @@ func loadList(cmd *cobra.Command, args []string) {
return nil, err
}

resp, err := api.ProxySearch(api.DefaultClient(debug), q.Get(), q.Params().From, q.Params().Limit)
resp, err := api.ProxySearch(api.DefaultClient(debug), q.Get(), q.Params().From, q.Params().Limit, apiFields)
if err != nil {
return nil, err
}
Expand All @@ -133,10 +166,32 @@ func loadList(cmd *cobra.Command, args []string) {
return
}

raw, err := cmd.Flags().GetBool("raw")
cmdutil.ExitIfError(err)
if jsonOutput {
// Get json-filter for output-level filtering (optional)
jsonFilter, err := cmd.Flags().GetString("json-filter")
cmdutil.ExitIfError(err)

var filterFields []string
if jsonFilter != "" {
// For json-filter, the user provides direct JSON paths
// Split by comma and trim spaces
filterFields = []string{}
for _, field := range strings.Split(jsonFilter, ",") {
field = strings.TrimSpace(field)
if field != "" {
filterFields = append(filterFields, field)
}
}
}

noWarnings, err := cmd.Flags().GetBool("no-warnings")
cmdutil.ExitIfError(err)

if raw {
outputJSON(issues, filterFields, noWarnings)
return
}

if rawOutput {
outputRawJSON(issues)
return
}
Expand Down Expand Up @@ -208,6 +263,38 @@ func outputRawJSON(issues []*jira.Issue) {
fmt.Println(string(data))
}

func outputJSON(issues []*jira.Issue, filter []string, noWarnings bool) {
// Marshal issues to JSON first
rawJSON, err := json.Marshal(issues)
if err != nil {
cmdutil.Failed("Failed to marshal issues: %s", err)
return
}

// Get field mappings
fieldMappings, err := cmdcommon.GetConfiguredCustomFields()
if err != nil {
cmdutil.Warn("Unable to load custom field mappings: %s", err)
fieldMappings = []jira.IssueTypeField{}
}

// Convert custom field IDs to readable names and apply output filter
result, err := jira.TransformIssueFields(rawJSON, fieldMappings, filter)
if err != nil {
cmdutil.Failed("Failed to format JSON output: %s", err)
return
}

// Display warnings if any (unless suppressed)
if !noWarnings {
for _, warning := range result.Warnings {
cmdutil.Warn(warning)
}
}

fmt.Println(string(result.Data))
}

// SetFlags sets flags supported by a list command.
func SetFlags(cmd *cobra.Command) {
cmd.Flags().SortFlags = false
Expand Down Expand Up @@ -245,6 +332,16 @@ func SetFlags(cmd *cobra.Command) {
cmd.Flags().String("delimiter", "\t", "Custom delimeter for columns in plain mode. Works only with --plain")
cmd.Flags().Uint("comments", 1, "Show N comments when viewing the issue")
cmd.Flags().Bool("raw", false, "Print raw JSON output")
cmd.Flags().Bool("json", false, "Print JSON output with human-readable custom field names")
cmd.Flags().String("api-fields", "", "Comma-separated list of fields to fetch from Jira API (e.g., 'key,summary,description'). "+
"Use Jira field names, human-readable names from your config (e.g., 'Story Points', 'Sprint'), "+
"custom field IDs (e.g., 'customfield_10001'), or special values like '*navigable' (common fields), '*all' (most fields). "+
"Note: For 'issue list', this can only reduce fields returned by the API, not add new ones. "+
"Only works with --json or --raw. If not specified, uses Jira's default field selection.")
cmd.Flags().String("json-filter", "", "Comma-separated list of JSON paths to include in output (e.g., 'key,fields.summary,fields.status.statusCategory.name'). "+
"Allows precise filtering of nested JSON fields after API response. "+
"Only works with --json. If not specified, includes all fields from API response.")
cmd.Flags().Bool("no-warnings", false, "Suppress warnings about field name collisions. Only works with --json")
cmd.Flags().Bool("csv", false, "Print output in CSV format")

if cmd.HasParent() && cmd.Parent().Name() != "sprint" {
Expand Down
Loading