cli/pkg/cmd/search/shared/shared.go
Andy Feller 3eca268a7f Introduce color_labels support, update commands
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.
2025-04-02 18:24:20 -04:00

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 ""
}
}