github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/search/shared/shared.go (about)

     1  package shared
     2  
     3  import (
     4  	"fmt"
     5  	"strconv"
     6  	"strings"
     7  	"time"
     8  
     9  	"github.com/ungtb10d/cli/v2/internal/browser"
    10  	"github.com/ungtb10d/cli/v2/internal/text"
    11  	"github.com/ungtb10d/cli/v2/pkg/cmdutil"
    12  	"github.com/ungtb10d/cli/v2/pkg/iostreams"
    13  	"github.com/ungtb10d/cli/v2/pkg/search"
    14  	"github.com/ungtb10d/cli/v2/utils"
    15  )
    16  
    17  type EntityType int
    18  
    19  const (
    20  	// Limitation of GitHub search see:
    21  	// https://docs.github.com/en/rest/reference/search
    22  	SearchMaxResults = 1000
    23  
    24  	Both EntityType = iota
    25  	Issues
    26  	PullRequests
    27  )
    28  
    29  type IssuesOptions struct {
    30  	Browser  browser.Browser
    31  	Entity   EntityType
    32  	Exporter cmdutil.Exporter
    33  	IO       *iostreams.IOStreams
    34  	Now      time.Time
    35  	Query    search.Query
    36  	Searcher search.Searcher
    37  	WebMode  bool
    38  }
    39  
    40  func Searcher(f *cmdutil.Factory) (search.Searcher, error) {
    41  	cfg, err := f.Config()
    42  	if err != nil {
    43  		return nil, err
    44  	}
    45  	host, _ := cfg.DefaultHost()
    46  	client, err := f.HttpClient()
    47  	if err != nil {
    48  		return nil, err
    49  	}
    50  	return search.NewSearcher(client, host), nil
    51  }
    52  
    53  func SearchIssues(opts *IssuesOptions) error {
    54  	io := opts.IO
    55  	if opts.WebMode {
    56  		url := opts.Searcher.URL(opts.Query)
    57  		if io.IsStdoutTTY() {
    58  			fmt.Fprintf(io.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(url))
    59  		}
    60  		return opts.Browser.Browse(url)
    61  	}
    62  	io.StartProgressIndicator()
    63  	result, err := opts.Searcher.Issues(opts.Query)
    64  	io.StopProgressIndicator()
    65  	if err != nil {
    66  		return err
    67  	}
    68  	if len(result.Items) == 0 && opts.Exporter == nil {
    69  		var msg string
    70  		switch opts.Entity {
    71  		case Both:
    72  			msg = "no issues or pull requests matched your search"
    73  		case Issues:
    74  			msg = "no issues matched your search"
    75  		case PullRequests:
    76  			msg = "no pull requests matched your search"
    77  		}
    78  		return cmdutil.NewNoResultsError(msg)
    79  	}
    80  
    81  	if err := io.StartPager(); err == nil {
    82  		defer io.StopPager()
    83  	} else {
    84  		fmt.Fprintf(io.ErrOut, "failed to start pager: %v\n", err)
    85  	}
    86  
    87  	if opts.Exporter != nil {
    88  		return opts.Exporter.Write(io, result.Items)
    89  	}
    90  
    91  	return displayIssueResults(io, opts.Now, opts.Entity, result)
    92  }
    93  
    94  func displayIssueResults(io *iostreams.IOStreams, now time.Time, et EntityType, results search.IssuesResult) error {
    95  	if now.IsZero() {
    96  		now = time.Now()
    97  	}
    98  	cs := io.ColorScheme()
    99  	//nolint:staticcheck // SA1019: utils.NewTablePrinter is deprecated: use internal/tableprinter
   100  	tp := utils.NewTablePrinter(io)
   101  	for _, issue := range results.Items {
   102  		if et == Both {
   103  			kind := "issue"
   104  			if issue.IsPullRequest() {
   105  				kind = "pr"
   106  			}
   107  			tp.AddField(kind, nil, nil)
   108  		}
   109  		comp := strings.Split(issue.RepositoryURL, "/")
   110  		name := comp[len(comp)-2:]
   111  		tp.AddField(strings.Join(name, "/"), nil, nil)
   112  		issueNum := strconv.Itoa(issue.Number)
   113  		if tp.IsTTY() {
   114  			issueNum = "#" + issueNum
   115  		}
   116  		if issue.IsPullRequest() {
   117  			tp.AddField(issueNum, nil, cs.ColorFromString(colorForPRState(issue.State())))
   118  		} else {
   119  			tp.AddField(issueNum, nil, cs.ColorFromString(colorForIssueState(issue.State(), issue.StateReason)))
   120  		}
   121  		if !tp.IsTTY() {
   122  			tp.AddField(issue.State(), nil, nil)
   123  		}
   124  		tp.AddField(text.RemoveExcessiveWhitespace(issue.Title), nil, nil)
   125  		tp.AddField(listIssueLabels(&issue, cs, tp.IsTTY()), nil, nil)
   126  		if tp.IsTTY() {
   127  			tp.AddField(text.FuzzyAgo(now, issue.UpdatedAt), nil, cs.Gray)
   128  		} else {
   129  			tp.AddField(issue.UpdatedAt.String(), nil, nil)
   130  		}
   131  		tp.EndRow()
   132  	}
   133  
   134  	if io.IsStdoutTTY() {
   135  		var header string
   136  		switch et {
   137  		case Both:
   138  			header = fmt.Sprintf("Showing %d of %d issues and pull requests\n\n", len(results.Items), results.Total)
   139  		case Issues:
   140  			header = fmt.Sprintf("Showing %d of %d issues\n\n", len(results.Items), results.Total)
   141  		case PullRequests:
   142  			header = fmt.Sprintf("Showing %d of %d pull requests\n\n", len(results.Items), results.Total)
   143  		}
   144  		fmt.Fprintf(io.Out, "\n%s", header)
   145  	}
   146  	return tp.Render()
   147  }
   148  
   149  func listIssueLabels(issue *search.Issue, cs *iostreams.ColorScheme, colorize bool) string {
   150  	if len(issue.Labels) == 0 {
   151  		return ""
   152  	}
   153  	labelNames := make([]string, 0, len(issue.Labels))
   154  	for _, label := range issue.Labels {
   155  		if colorize {
   156  			labelNames = append(labelNames, cs.HexToRGB(label.Color, label.Name))
   157  		} else {
   158  			labelNames = append(labelNames, label.Name)
   159  		}
   160  	}
   161  	return strings.Join(labelNames, ", ")
   162  }
   163  
   164  func colorForIssueState(state, reason string) string {
   165  	switch state {
   166  	case "open":
   167  		return "green"
   168  	case "closed":
   169  		if reason == "not_planned" {
   170  			return "gray"
   171  		}
   172  		return "magenta"
   173  	default:
   174  		return ""
   175  	}
   176  }
   177  
   178  func colorForPRState(state string) string {
   179  	switch state {
   180  	case "open":
   181  		return "green"
   182  	case "closed":
   183  		return "red"
   184  	case "merged":
   185  		return "magenta"
   186  	default:
   187  		return ""
   188  	}
   189  }