PR listing: add --fields & expose additional fields (#415)

This PR adds the `--fields` flag to `tea pr ls` (#342), and exposes more fields specific to the `PullRequest` type:
```
   --fields value, -f value   Comma-separated list of fields to print.
                              Available values:
                              index,state,author,author-id,url,title,body,mergeable,base,base-commit,head,diff,patch,created,updated,deadline,assignees,milestone,labels,comments
                              (default: "index,title,state,author,milestone,updated,labels")

```

Co-authored-by: justusbunsi <61625851+justusbunsi@users.noreply.github.com>
Co-authored-by: Norwin <git@nroo.de>
Reviewed-on: https://gitea.com/gitea/tea/pulls/415
Reviewed-by: Norwin <noerw@noreply.gitea.io>
Reviewed-by: techknowlogick <techknowlogick@gitea.io>
Reviewed-by: 6543 <6543@obermui.de>
Co-authored-by: justusbunsi <justusbunsi@noreply.gitea.io>
Co-committed-by: justusbunsi <justusbunsi@noreply.gitea.io>
This commit is contained in:
justusbunsi 2021-09-29 04:36:33 +08:00 committed by 6543
parent 1e59dee685
commit 3cf084cb96
7 changed files with 167 additions and 54 deletions

View File

@ -13,6 +13,10 @@ import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
var pullFieldsFlag = flags.FieldsFlag(print.PullFields, []string{
"index", "title", "state", "author", "milestone", "updated", "labels",
})
// CmdPullsList represents a sub command of issues to list pulls // CmdPullsList represents a sub command of issues to list pulls
var CmdPullsList = cli.Command{ var CmdPullsList = cli.Command{
Name: "list", Name: "list",
@ -20,7 +24,7 @@ var CmdPullsList = cli.Command{
Usage: "List pull requests of the repository", Usage: "List pull requests of the repository",
Description: `List pull requests of the repository`, Description: `List pull requests of the repository`,
Action: RunPullsList, Action: RunPullsList,
Flags: flags.IssuePRFlags, Flags: append([]cli.Flag{pullFieldsFlag}, flags.IssuePRFlags...),
} }
// RunPullsList return list of pulls // RunPullsList return list of pulls
@ -46,6 +50,11 @@ func RunPullsList(cmd *cli.Context) error {
return err return err
} }
print.PullsList(prs, ctx.Output) fields, err := pullFieldsFlag.GetValues(cmd)
if err != nil {
return err
}
print.PullsList(prs, ctx.Output, fields)
return nil return nil
} }

View File

@ -72,3 +72,16 @@ func formatUserName(u *gitea.User) string {
} }
return u.FullName return u.FullName
} }
func formatBoolean(b bool, allowIcons bool) string {
if !allowIcons {
return fmt.Sprintf("%v", b)
}
styled := "✔"
if !b {
styled = "✖"
}
return styled
}

View File

@ -54,19 +54,20 @@ var IssueFields = []string{
func printIssues(issues []*gitea.Issue, output string, fields []string) { func printIssues(issues []*gitea.Issue, output string, fields []string) {
labelMap := map[int64]string{} labelMap := map[int64]string{}
var printables = make([]printable, len(issues)) var printables = make([]printable, len(issues))
machineReadable := isMachineReadable(output)
for i, x := range issues { for i, x := range issues {
// pre-serialize labels for performance // pre-serialize labels for performance
for _, label := range x.Labels { for _, label := range x.Labels {
if _, ok := labelMap[label.ID]; !ok { if _, ok := labelMap[label.ID]; !ok {
labelMap[label.ID] = formatLabel(label, !isMachineReadable(output), "") labelMap[label.ID] = formatLabel(label, !machineReadable, "")
} }
} }
// store items with printable interface // store items with printable interface
printables[i] = &printableIssue{x, &labelMap} printables[i] = &printableIssue{x, &labelMap}
} }
t := tableFromItems(fields, printables) t := tableFromItems(fields, printables, machineReadable)
t.print(output) t.print(output)
} }
@ -75,7 +76,7 @@ type printableIssue struct {
formattedLabels *map[int64]string formattedLabels *map[int64]string
} }
func (x printableIssue) FormatField(field string) string { func (x printableIssue) FormatField(field string, machineReadable bool) string {
switch field { switch field {
case "index": case "index":
return fmt.Sprintf("%d", x.Index) return fmt.Sprintf("%d", x.Index)

View File

@ -6,7 +6,6 @@ package print
import ( import (
"fmt" "fmt"
"strconv"
"strings" "strings"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
@ -23,19 +22,8 @@ var ciStatusSymbols = map[gitea.StatusState]string{
// PullDetails print an pull rendered to stdout // PullDetails print an pull rendered to stdout
func PullDetails(pr *gitea.PullRequest, reviews []*gitea.PullReview, ciStatus *gitea.CombinedStatus) { func PullDetails(pr *gitea.PullRequest, reviews []*gitea.PullReview, ciStatus *gitea.CombinedStatus) {
base := pr.Base.Name base := pr.Base.Name
head := pr.Head.Name head := formatPRHead(pr)
if pr.Head.RepoID != pr.Base.RepoID { state := formatPRState(pr)
if pr.Head.Repository != nil {
head = pr.Head.Repository.Owner.UserName + ":" + head
} else {
head = "delete:" + head
}
}
state := pr.State
if pr.Merged != nil {
state = "merged"
}
out := fmt.Sprintf( out := fmt.Sprintf(
"# #%d %s (%s)\n@%s created %s\t**%s** <- **%s**\n\n%s\n\n", "# #%d %s (%s)\n@%s created %s\t**%s** <- **%s**\n\n%s\n\n",
@ -79,6 +67,25 @@ func PullDetails(pr *gitea.PullRequest, reviews []*gitea.PullReview, ciStatus *g
outputMarkdown(out, pr.HTMLURL) outputMarkdown(out, pr.HTMLURL)
} }
func formatPRHead(pr *gitea.PullRequest) string {
head := pr.Head.Name
if pr.Head.RepoID != pr.Base.RepoID {
if pr.Head.Repository != nil {
head = pr.Head.Repository.Owner.UserName + ":" + head
} else {
head = "delete:" + head
}
}
return head
}
func formatPRState(pr *gitea.PullRequest) string {
if pr.Merged != nil {
return "merged"
}
return string(pr.State)
}
func formatReviews(reviews []*gitea.PullReview) string { func formatReviews(reviews []*gitea.PullReview) string {
result := "" result := ""
if len(reviews) == 0 { if len(reviews) == 0 {
@ -114,37 +121,120 @@ func formatReviews(reviews []*gitea.PullReview) string {
} }
// PullsList prints a listing of pulls // PullsList prints a listing of pulls
func PullsList(prs []*gitea.PullRequest, output string) { func PullsList(prs []*gitea.PullRequest, output string, fields []string) {
t := tableWithHeader( printPulls(prs, output, fields)
"Index", }
"Title",
"State",
"Author",
"Milestone",
"Updated",
)
for _, pr := range prs { // PullFields are all available fields to print with PullsList()
if pr == nil { var PullFields = []string{
continue "index",
"state",
"author",
"author-id",
"url",
"title",
"body",
"mergeable",
"base",
"base-commit",
"head",
"diff",
"patch",
"created",
"updated",
"deadline",
"assignees",
"milestone",
"labels",
"comments",
}
func printPulls(pulls []*gitea.PullRequest, output string, fields []string) {
labelMap := map[int64]string{}
var printables = make([]printable, len(pulls))
machineReadable := isMachineReadable(output)
for i, x := range pulls {
// pre-serialize labels for performance
for _, label := range x.Labels {
if _, ok := labelMap[label.ID]; !ok {
labelMap[label.ID] = formatLabel(label, !machineReadable, "")
}
} }
author := pr.Poster.FullName // store items with printable interface
if len(author) == 0 { printables[i] = &printablePull{x, &labelMap}
author = pr.Poster.UserName
}
mile := ""
if pr.Milestone != nil {
mile = pr.Milestone.Title
}
t.addRow(
strconv.FormatInt(pr.Index, 10),
pr.Title,
string(pr.State),
author,
mile,
FormatTime(*pr.Updated),
)
} }
t := tableFromItems(fields, printables, machineReadable)
t.print(output) t.print(output)
} }
type printablePull struct {
*gitea.PullRequest
formattedLabels *map[int64]string
}
func (x printablePull) FormatField(field string, machineReadable bool) string {
switch field {
case "index":
return fmt.Sprintf("%d", x.Index)
case "state":
return formatPRState(x.PullRequest)
case "author":
return formatUserName(x.Poster)
case "author-id":
return x.Poster.UserName
case "url":
return x.HTMLURL
case "title":
return x.Title
case "body":
return x.Body
case "created":
return FormatTime(*x.Created)
case "updated":
return FormatTime(*x.Updated)
case "deadline":
if x.Deadline == nil {
return ""
}
return FormatTime(*x.Deadline)
case "milestone":
if x.Milestone != nil {
return x.Milestone.Title
}
return ""
case "labels":
var labels = make([]string, len(x.Labels))
for i, l := range x.Labels {
labels[i] = (*x.formattedLabels)[l.ID]
}
return strings.Join(labels, " ")
case "assignees":
var assignees = make([]string, len(x.Assignees))
for i, a := range x.Assignees {
assignees[i] = formatUserName(a)
}
return strings.Join(assignees, " ")
case "comments":
return fmt.Sprintf("%d", x.Comments)
case "mergeable":
isMergeable := x.Mergeable && x.State == gitea.StateOpen
return formatBoolean(isMergeable, !machineReadable)
case "base":
return x.Base.Ref
case "base-commit":
return x.MergeBase
case "head":
return formatPRHead(x.PullRequest)
case "diff":
return x.DiffURL
case "patch":
return x.PatchURL
}
return ""
}

View File

@ -18,7 +18,7 @@ func ReposList(repos []*gitea.Repository, output string, fields []string) {
for i, r := range repos { for i, r := range repos {
printables[i] = &printableRepo{r} printables[i] = &printableRepo{r}
} }
t := tableFromItems(fields, printables) t := tableFromItems(fields, printables, isMachineReadable(output))
t.print(output) t.print(output)
} }
@ -107,7 +107,7 @@ var RepoFields = []string{
type printableRepo struct{ *gitea.Repository } type printableRepo struct{ *gitea.Repository }
func (x printableRepo) FormatField(field string) string { func (x printableRepo) FormatField(field string, machineReadable bool) string {
switch field { switch field {
case "description": case "description":
return x.Description return x.Description

View File

@ -24,16 +24,16 @@ type table struct {
// printable can be implemented for structs to put fields dynamically into a table // printable can be implemented for structs to put fields dynamically into a table
type printable interface { type printable interface {
FormatField(field string) string FormatField(field string, machineReadable bool) string
} }
// high level api to print a table of items with dynamic fields // high level api to print a table of items with dynamic fields
func tableFromItems(fields []string, values []printable) table { func tableFromItems(fields []string, values []printable, machineReadable bool) table {
t := table{headers: fields} t := table{headers: fields}
for _, v := range values { for _, v := range values {
row := make([]string, len(fields)) row := make([]string, len(fields))
for i, f := range fields { for i, f := range fields {
row[i] = v.FormatField(f) row[i] = v.FormatField(f, machineReadable)
} }
t.addRowSlice(row) t.addRowSlice(row)
} }

View File

@ -18,7 +18,7 @@ func TrackedTimesList(times []*gitea.TrackedTime, outputType string, fields []st
totalDuration += t.Time totalDuration += t.Time
printables[i] = &printableTrackedTime{t, outputType} printables[i] = &printableTrackedTime{t, outputType}
} }
t := tableFromItems(fields, printables) t := tableFromItems(fields, printables, isMachineReadable(outputType))
if printTotal { if printTotal {
total := make([]string, len(fields)) total := make([]string, len(fields))
@ -45,7 +45,7 @@ type printableTrackedTime struct {
outputFormat string outputFormat string
} }
func (t printableTrackedTime) FormatField(field string) string { func (t printableTrackedTime) FormatField(field string, machineReadable bool) string {
switch field { switch field {
case "id": case "id":
return fmt.Sprintf("%d", t.ID) return fmt.Sprintf("%d", t.ID)