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 }