Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/deployment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ jobs:

- name: Add App Store Connect API key to keychain
if: inputs.environment == 'production'
uses: nodeselector/setup-apple-codesign@ab275d0f6fb63ef9e20b12b42ea0d567f935723c
uses: nodeselector/setup-apple-codesign@309922bbe4c7277c477635e68d3a1af52d8ad06b
id: setup-apple-codesign
with:
asset-type: "app-store-connect-api-key"
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ require (
github.com/creack/pty v1.1.24
github.com/digitorus/timestamp v0.0.0-20250524132541-c45532741eea
github.com/distribution/reference v0.6.0
github.com/gabriel-vasile/mimetype v1.4.13
github.com/gabriel-vasile/mimetype v1.4.14
github.com/gdamore/tcell/v2 v2.13.10
github.com/google/go-cmp v0.7.0
github.com/google/go-containerregistry v0.21.7
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gabriel-vasile/mimetype v1.4.14 h1:8eyElddS5wbWNDG4sIupw+IX2jEjHX2aqAAq/9C3M8s=
github.com/gabriel-vasile/mimetype v1.4.14/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
github.com/gdamore/tcell/v2 v2.13.10 h1:Afs3JKt83HnhuUKdZ3MnxUgOqQRWftj5JyDqv1LLynA=
Expand Down
101 changes: 98 additions & 3 deletions pkg/cmd/project/item-list/item_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ type listOpts struct {
owner string
number int32
query string
fields []string
fieldIDs []string
exporter cmdutil.Exporter
}

Expand Down Expand Up @@ -55,6 +57,9 @@ func NewCmdList(f *cmdutil.Factory, runF func(config listConfig) error) *cobra.C

# List items with the "bug" label that are not done
$ gh project item-list 1 --owner "@me" --query "label:bug -status:Done"

# Show the "Status" and "Priority" field values as extra columns
$ gh project item-list 1 --owner "@me" --field "Status" --field "Priority"
`),
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
Expand All @@ -71,6 +76,22 @@ func NewCmdList(f *cmdutil.Factory, runF func(config listConfig) error) *cobra.C
opts.number = int32(num)
}

if err := cmdutil.MutuallyExclusive(
"only one of `--field` or `--field-id` may be used",
len(opts.fields) > 0,
len(opts.fieldIDs) > 0,
); err != nil {
return err
}

if err := cmdutil.MutuallyExclusive(
"cannot use `--format` with `--field` or `--field-id`",
opts.exporter != nil,
len(opts.fields) > 0 || len(opts.fieldIDs) > 0,
); err != nil {
return err
}

config := listConfig{
io: f.IOStreams,
client: client,
Expand Down Expand Up @@ -101,6 +122,8 @@ func NewCmdList(f *cmdutil.Factory, runF func(config listConfig) error) *cobra.C

listCmd.Flags().StringVar(&opts.owner, "owner", "", "Login of the owner. Use \"@me\" for the current user")
listCmd.Flags().StringVar(&opts.query, "query", "", `Filter items using the Projects filter syntax, e.g. "assignee:octocat -status:Done"`)
listCmd.Flags().StringArrayVar(&opts.fields, "field", nil, "Name of a field to show as an extra column")
listCmd.Flags().StringArrayVar(&opts.fieldIDs, "field-id", nil, "ID of a field to show as an extra column")
cmdutil.AddFormatFlags(listCmd, &opts.exporter)
listCmd.Flags().IntVarP(&opts.limit, "limit", "L", queries.LimitDefault, "Maximum number of items to fetch")

Expand Down Expand Up @@ -142,15 +165,73 @@ func runList(config listConfig) error {
return config.opts.exporter.Write(config.io, project.DetailedItems())
}

return printResults(config, project.Items.Nodes, owner.Login)
// Resolve any requested extra field columns to (header, fieldID) pairs. Name
// columns are resolved against the fields already returned with the items, so
// there is no separate field-ID preflight lookup. When extra columns are
// requested for a project whose field list spans more than one page, fetch the
// complete field list first so resolution does not miss fields beyond the
// first page.
fields := project.Fields.Nodes
if (len(config.opts.fields) > 0 || len(config.opts.fieldIDs) > 0) && project.Fields.PageInfo.HasNextPage {
withFields, err := config.client.ProjectFields(owner, config.opts.number, project.Fields.TotalCount)
if err != nil {
return err
}
fields = withFields.Fields.Nodes
}

extraFields, err := resolveFieldColumns(config.opts, fields)
if err != nil {
return err
}

return printResults(config, project.Items.Nodes, owner.Login, extraFields)
}

// fieldColumn identifies an extra table column to render for each item: Header
// is the column title and FieldID is the field node ID whose value to show.
type fieldColumn struct {
Header string
FieldID string
}

// resolveFieldColumns turns the --field (names) and --field-id (node IDs) flags
// into ordered column descriptors. Names are resolved with ResolveFieldByName so
// unknown or ambiguous names fail with candidate-carrying errors; IDs are
// validated against the project's fields so an unknown ID also fails clearly.
func resolveFieldColumns(opts listOpts, fields []queries.ProjectField) ([]fieldColumn, error) {
columns := make([]fieldColumn, 0, len(opts.fields)+len(opts.fieldIDs))

for _, name := range opts.fields {
field, err := queries.ResolveFieldByName(fields, name)
if err != nil {
return nil, err
}
columns = append(columns, fieldColumn{Header: field.Name(), FieldID: field.ID()})
}

for _, id := range opts.fieldIDs {
field, err := queries.ResolveFieldByID(fields, id)
if err != nil {
return nil, err
}
columns = append(columns, fieldColumn{Header: field.Name(), FieldID: field.ID()})
}

return columns, nil
}

func printResults(config listConfig, items []queries.ProjectItem, login string) error {
func printResults(config listConfig, items []queries.ProjectItem, login string, extraFields []fieldColumn) error {
if len(items) == 0 {
return cmdutil.NewNoResultsError(fmt.Sprintf("Project %d for owner %s has no items", config.opts.number, login))
}

tp := tableprinter.New(config.io, tableprinter.WithHeader("Type", "Title", "Number", "Repository", "ID"))
headers := []string{"Type", "Title", "Number", "Repository", "ID"}
for _, f := range extraFields {
headers = append(headers, f.Header)
}

tp := tableprinter.New(config.io, tableprinter.WithHeader(headers...))

for _, i := range items {
tp.AddField(i.Type())
Expand All @@ -162,8 +243,22 @@ func printResults(config listConfig, items []queries.ProjectItem, login string)
}
tp.AddField(i.Repo())
tp.AddField(i.ID(), tableprinter.WithTruncate(nil))
for _, f := range extraFields {
tp.AddField(fieldValueForItem(i, f.FieldID))
}
tp.EndRow()
}

return tp.Render()
}

// fieldValueForItem returns the display value of the field identified by fieldID
// on item i, or an empty string when the item has no value for that field.
func fieldValueForItem(i queries.ProjectItem, fieldID string) string {
for _, v := range i.FieldValues.Nodes {
if v.ID() == fieldID {
return v.DisplayValue()
}
}
return ""
}
Loading
Loading