This commit implements the actual changes around configuration setting / environment variable logic for displaying labels using their RGB hex color code in terminals with truecolor support. One of the subtler changes in this commit is renaming generic ColorScheme.HexToRGB logic to render truecolor to ColorScheme.Label as this feature was being used exclusively for labels. This is due to confusion about introducing the new `color_labels` config on top of generic coloring logic.
194 lines
4.5 KiB
Go
194 lines
4.5 KiB
Go
package shared
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/cli/cli/v2/internal/browser"
|
|
"github.com/cli/cli/v2/internal/tableprinter"
|
|
"github.com/cli/cli/v2/internal/text"
|
|
"github.com/cli/cli/v2/pkg/cmdutil"
|
|
"github.com/cli/cli/v2/pkg/iostreams"
|
|
"github.com/cli/cli/v2/pkg/search"
|
|
)
|
|
|
|
type EntityType int
|
|
|
|
const (
|
|
// Limitation of GitHub search see:
|
|
// https://docs.github.com/en/rest/reference/search
|
|
SearchMaxResults = 1000
|
|
|
|
Both EntityType = iota
|
|
Issues
|
|
PullRequests
|
|
)
|
|
|
|
type IssuesOptions struct {
|
|
Browser browser.Browser
|
|
Entity EntityType
|
|
Exporter cmdutil.Exporter
|
|
IO *iostreams.IOStreams
|
|
Now time.Time
|
|
Query search.Query
|
|
Searcher search.Searcher
|
|
WebMode bool
|
|
}
|
|
|
|
func Searcher(f *cmdutil.Factory) (search.Searcher, error) {
|
|
cfg, err := f.Config()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
host, _ := cfg.Authentication().DefaultHost()
|
|
client, err := f.HttpClient()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return search.NewSearcher(client, host), nil
|
|
}
|
|
|
|
func SearchIssues(opts *IssuesOptions) error {
|
|
io := opts.IO
|
|
if opts.WebMode {
|
|
url := opts.Searcher.URL(opts.Query)
|
|
if io.IsStdoutTTY() {
|
|
fmt.Fprintf(io.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(url))
|
|
}
|
|
return opts.Browser.Browse(url)
|
|
}
|
|
io.StartProgressIndicator()
|
|
result, err := opts.Searcher.Issues(opts.Query)
|
|
io.StopProgressIndicator()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(result.Items) == 0 && opts.Exporter == nil {
|
|
var msg string
|
|
switch opts.Entity {
|
|
case Both:
|
|
msg = "no issues or pull requests matched your search"
|
|
case Issues:
|
|
msg = "no issues matched your search"
|
|
case PullRequests:
|
|
msg = "no pull requests matched your search"
|
|
}
|
|
return cmdutil.NewNoResultsError(msg)
|
|
}
|
|
|
|
if err := io.StartPager(); err == nil {
|
|
defer io.StopPager()
|
|
} else {
|
|
fmt.Fprintf(io.ErrOut, "failed to start pager: %v\n", err)
|
|
}
|
|
|
|
if opts.Exporter != nil {
|
|
return opts.Exporter.Write(io, result.Items)
|
|
}
|
|
|
|
return displayIssueResults(io, opts.Now, opts.Entity, result)
|
|
}
|
|
|
|
func displayIssueResults(io *iostreams.IOStreams, now time.Time, et EntityType, results search.IssuesResult) error {
|
|
if now.IsZero() {
|
|
now = time.Now()
|
|
}
|
|
|
|
var headers []string
|
|
if et == Both {
|
|
headers = []string{"Kind", "Repo", "ID", "Title", "Labels", "Updated"}
|
|
} else {
|
|
headers = []string{"Repo", "ID", "Title", "Labels", "Updated"}
|
|
}
|
|
|
|
cs := io.ColorScheme()
|
|
tp := tableprinter.New(io, tableprinter.WithHeader(headers...))
|
|
for _, issue := range results.Items {
|
|
if et == Both {
|
|
kind := "issue"
|
|
if issue.IsPullRequest() {
|
|
kind = "pr"
|
|
}
|
|
tp.AddField(kind)
|
|
}
|
|
comp := strings.Split(issue.RepositoryURL, "/")
|
|
name := comp[len(comp)-2:]
|
|
tp.AddField(strings.Join(name, "/"))
|
|
issueNum := strconv.Itoa(issue.Number)
|
|
if tp.IsTTY() {
|
|
issueNum = "#" + issueNum
|
|
}
|
|
if issue.IsPullRequest() {
|
|
color := tableprinter.WithColor(cs.ColorFromString(colorForPRState(issue.State())))
|
|
tp.AddField(issueNum, color)
|
|
} else {
|
|
color := tableprinter.WithColor(cs.ColorFromString(colorForIssueState(issue.State(), issue.StateReason)))
|
|
tp.AddField(issueNum, color)
|
|
}
|
|
if !tp.IsTTY() {
|
|
tp.AddField(issue.State())
|
|
}
|
|
tp.AddField(text.RemoveExcessiveWhitespace(issue.Title))
|
|
tp.AddField(listIssueLabels(&issue, cs, tp.IsTTY()))
|
|
tp.AddTimeField(now, issue.UpdatedAt, cs.Gray)
|
|
tp.EndRow()
|
|
}
|
|
|
|
if tp.IsTTY() {
|
|
var header string
|
|
switch et {
|
|
case Both:
|
|
header = fmt.Sprintf("Showing %d of %d issues and pull requests\n\n", len(results.Items), results.Total)
|
|
case Issues:
|
|
header = fmt.Sprintf("Showing %d of %d issues\n\n", len(results.Items), results.Total)
|
|
case PullRequests:
|
|
header = fmt.Sprintf("Showing %d of %d pull requests\n\n", len(results.Items), results.Total)
|
|
}
|
|
fmt.Fprintf(io.Out, "\n%s", header)
|
|
}
|
|
return tp.Render()
|
|
}
|
|
|
|
func listIssueLabels(issue *search.Issue, cs *iostreams.ColorScheme, colorize bool) string {
|
|
if len(issue.Labels) == 0 {
|
|
return ""
|
|
}
|
|
labelNames := make([]string, 0, len(issue.Labels))
|
|
for _, label := range issue.Labels {
|
|
if colorize {
|
|
labelNames = append(labelNames, cs.Label(label.Color, label.Name))
|
|
} else {
|
|
labelNames = append(labelNames, label.Name)
|
|
}
|
|
}
|
|
return strings.Join(labelNames, ", ")
|
|
}
|
|
|
|
func colorForIssueState(state, reason string) string {
|
|
switch state {
|
|
case "open":
|
|
return "green"
|
|
case "closed":
|
|
if reason == "not_planned" {
|
|
return "gray"
|
|
}
|
|
return "magenta"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func colorForPRState(state string) string {
|
|
switch state {
|
|
case "open":
|
|
return "green"
|
|
case "closed":
|
|
return "red"
|
|
case "merged":
|
|
return "magenta"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|