1// Copyright 2013 The go-github AUTHORS. All rights reserved. 2// 3// Use of this source code is governed by a BSD-style 4// license that can be found in the LICENSE file. 5 6package github 7 8import ( 9 "context" 10 "fmt" 11 "strconv" 12 13 qs "github.com/google/go-querystring/query" 14) 15 16// SearchService provides access to the search related functions 17// in the GitHub API. 18// 19// Each method takes a query string defining the search keywords and any search qualifiers. 20// For example, when searching issues, the query "gopher is:issue language:go" will search 21// for issues containing the word "gopher" in Go repositories. The method call 22// opts := &github.SearchOptions{Sort: "created", Order: "asc"} 23// cl.Search.Issues(ctx, "gopher is:issue language:go", opts) 24// will search for such issues, sorting by creation date in ascending order 25// (i.e., oldest first). 26// 27// If query includes multiple conditions, it MUST NOT include "+" as the condition separator. 28// You have to use " " as the separator instead. 29// For example, querying with "language:c++" and "leveldb", then query should be 30// "language:c++ leveldb" but not "language:c+++leveldb". 31// 32// GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/search/ 33type SearchService service 34 35// SearchOptions specifies optional parameters to the SearchService methods. 36type SearchOptions struct { 37 // How to sort the search results. Possible values are: 38 // - for repositories: stars, fork, updated 39 // - for commits: author-date, committer-date 40 // - for code: indexed 41 // - for issues: comments, created, updated 42 // - for users: followers, repositories, joined 43 // 44 // Default is to sort by best match. 45 Sort string `url:"sort,omitempty"` 46 47 // Sort order if sort parameter is provided. Possible values are: asc, 48 // desc. Default is desc. 49 Order string `url:"order,omitempty"` 50 51 // Whether to retrieve text match metadata with a query 52 TextMatch bool `url:"-"` 53 54 ListOptions 55} 56 57// Common search parameters. 58type searchParameters struct { 59 Query string 60 RepositoryID *int64 // Sent if non-nil. 61} 62 63// RepositoriesSearchResult represents the result of a repositories search. 64type RepositoriesSearchResult struct { 65 Total *int `json:"total_count,omitempty"` 66 IncompleteResults *bool `json:"incomplete_results,omitempty"` 67 Repositories []*Repository `json:"items,omitempty"` 68} 69 70// Repositories searches repositories via various criteria. 71// 72// GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/search/#search-repositories 73func (s *SearchService) Repositories(ctx context.Context, query string, opts *SearchOptions) (*RepositoriesSearchResult, *Response, error) { 74 result := new(RepositoriesSearchResult) 75 resp, err := s.search(ctx, "repositories", &searchParameters{Query: query}, opts, result) 76 return result, resp, err 77} 78 79// TopicsSearchResult represents the result of a topics search. 80type TopicsSearchResult struct { 81 Total *int `json:"total_count,omitempty"` 82 IncompleteResults *bool `json:"incomplete_results,omitempty"` 83 Topics []*TopicResult `json:"items,omitempty"` 84} 85 86type TopicResult struct { 87 Name *string `json:"name,omitempty"` 88 DisplayName *string `json:"display_name,omitempty"` 89 ShortDescription *string `json:"short_description,omitempty"` 90 Description *string `json:"description,omitempty"` 91 CreatedBy *string `json:"created_by,omitempty"` 92 CreatedAt *Timestamp `json:"created_at,omitempty"` 93 UpdatedAt *string `json:"updated_at,omitempty"` 94 Featured *bool `json:"featured,omitempty"` 95 Curated *bool `json:"curated,omitempty"` 96 Score *float64 `json:"score,omitempty"` 97} 98 99// Topics finds topics via various criteria. Results are sorted by best match. 100// Please see https://help.github.com/en/articles/searching-topics for more 101// information about search qualifiers. 102// 103// GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/search/#search-topics 104func (s *SearchService) Topics(ctx context.Context, query string, opts *SearchOptions) (*TopicsSearchResult, *Response, error) { 105 result := new(TopicsSearchResult) 106 resp, err := s.search(ctx, "topics", &searchParameters{Query: query}, opts, result) 107 return result, resp, err 108} 109 110// CommitsSearchResult represents the result of a commits search. 111type CommitsSearchResult struct { 112 Total *int `json:"total_count,omitempty"` 113 IncompleteResults *bool `json:"incomplete_results,omitempty"` 114 Commits []*CommitResult `json:"items,omitempty"` 115} 116 117// CommitResult represents a commit object as returned in commit search endpoint response. 118type CommitResult struct { 119 SHA *string `json:"sha,omitempty"` 120 Commit *Commit `json:"commit,omitempty"` 121 Author *User `json:"author,omitempty"` 122 Committer *User `json:"committer,omitempty"` 123 Parents []*Commit `json:"parents,omitempty"` 124 HTMLURL *string `json:"html_url,omitempty"` 125 URL *string `json:"url,omitempty"` 126 CommentsURL *string `json:"comments_url,omitempty"` 127 128 Repository *Repository `json:"repository,omitempty"` 129 Score *float64 `json:"score,omitempty"` 130} 131 132// Commits searches commits via various criteria. 133// 134// GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/search/#search-commits 135func (s *SearchService) Commits(ctx context.Context, query string, opts *SearchOptions) (*CommitsSearchResult, *Response, error) { 136 result := new(CommitsSearchResult) 137 resp, err := s.search(ctx, "commits", &searchParameters{Query: query}, opts, result) 138 return result, resp, err 139} 140 141// IssuesSearchResult represents the result of an issues search. 142type IssuesSearchResult struct { 143 Total *int `json:"total_count,omitempty"` 144 IncompleteResults *bool `json:"incomplete_results,omitempty"` 145 Issues []*Issue `json:"items,omitempty"` 146} 147 148// Issues searches issues via various criteria. 149// 150// GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/search/#search-issues-and-pull-requests 151func (s *SearchService) Issues(ctx context.Context, query string, opts *SearchOptions) (*IssuesSearchResult, *Response, error) { 152 result := new(IssuesSearchResult) 153 resp, err := s.search(ctx, "issues", &searchParameters{Query: query}, opts, result) 154 return result, resp, err 155} 156 157// UsersSearchResult represents the result of a users search. 158type UsersSearchResult struct { 159 Total *int `json:"total_count,omitempty"` 160 IncompleteResults *bool `json:"incomplete_results,omitempty"` 161 Users []*User `json:"items,omitempty"` 162} 163 164// Users searches users via various criteria. 165// 166// GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/search/#search-users 167func (s *SearchService) Users(ctx context.Context, query string, opts *SearchOptions) (*UsersSearchResult, *Response, error) { 168 result := new(UsersSearchResult) 169 resp, err := s.search(ctx, "users", &searchParameters{Query: query}, opts, result) 170 return result, resp, err 171} 172 173// Match represents a single text match. 174type Match struct { 175 Text *string `json:"text,omitempty"` 176 Indices []int `json:"indices,omitempty"` 177} 178 179// TextMatch represents a text match for a SearchResult 180type TextMatch struct { 181 ObjectURL *string `json:"object_url,omitempty"` 182 ObjectType *string `json:"object_type,omitempty"` 183 Property *string `json:"property,omitempty"` 184 Fragment *string `json:"fragment,omitempty"` 185 Matches []*Match `json:"matches,omitempty"` 186} 187 188func (tm TextMatch) String() string { 189 return Stringify(tm) 190} 191 192// CodeSearchResult represents the result of a code search. 193type CodeSearchResult struct { 194 Total *int `json:"total_count,omitempty"` 195 IncompleteResults *bool `json:"incomplete_results,omitempty"` 196 CodeResults []*CodeResult `json:"items,omitempty"` 197} 198 199// CodeResult represents a single search result. 200type CodeResult struct { 201 Name *string `json:"name,omitempty"` 202 Path *string `json:"path,omitempty"` 203 SHA *string `json:"sha,omitempty"` 204 HTMLURL *string `json:"html_url,omitempty"` 205 Repository *Repository `json:"repository,omitempty"` 206 TextMatches []*TextMatch `json:"text_matches,omitempty"` 207} 208 209func (c CodeResult) String() string { 210 return Stringify(c) 211} 212 213// Code searches code via various criteria. 214// 215// GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/search/#search-code 216func (s *SearchService) Code(ctx context.Context, query string, opts *SearchOptions) (*CodeSearchResult, *Response, error) { 217 result := new(CodeSearchResult) 218 resp, err := s.search(ctx, "code", &searchParameters{Query: query}, opts, result) 219 return result, resp, err 220} 221 222// LabelsSearchResult represents the result of a code search. 223type LabelsSearchResult struct { 224 Total *int `json:"total_count,omitempty"` 225 IncompleteResults *bool `json:"incomplete_results,omitempty"` 226 Labels []*LabelResult `json:"items,omitempty"` 227} 228 229// LabelResult represents a single search result. 230type LabelResult struct { 231 ID *int64 `json:"id,omitempty"` 232 URL *string `json:"url,omitempty"` 233 Name *string `json:"name,omitempty"` 234 Color *string `json:"color,omitempty"` 235 Default *bool `json:"default,omitempty"` 236 Description *string `json:"description,omitempty"` 237 Score *float64 `json:"score,omitempty"` 238} 239 240func (l LabelResult) String() string { 241 return Stringify(l) 242} 243 244// Labels searches labels in the repository with ID repoID via various criteria. 245// 246// GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/search/#search-labels 247func (s *SearchService) Labels(ctx context.Context, repoID int64, query string, opts *SearchOptions) (*LabelsSearchResult, *Response, error) { 248 result := new(LabelsSearchResult) 249 resp, err := s.search(ctx, "labels", &searchParameters{RepositoryID: &repoID, Query: query}, opts, result) 250 return result, resp, err 251} 252 253// Helper function that executes search queries against different 254// GitHub search types (repositories, commits, code, issues, users, labels) 255// 256// If searchParameters.Query includes multiple condition, it MUST NOT include "+" as condition separator. 257// For example, querying with "language:c++" and "leveldb", then searchParameters.Query should be "language:c++ leveldb" but not "language:c+++leveldb". 258func (s *SearchService) search(ctx context.Context, searchType string, parameters *searchParameters, opts *SearchOptions, result interface{}) (*Response, error) { 259 params, err := qs.Values(opts) 260 if err != nil { 261 return nil, err 262 } 263 if parameters.RepositoryID != nil { 264 params.Set("repository_id", strconv.FormatInt(*parameters.RepositoryID, 10)) 265 } 266 params.Set("q", parameters.Query) 267 u := fmt.Sprintf("search/%s?%s", searchType, params.Encode()) 268 269 req, err := s.client.NewRequest("GET", u, nil) 270 if err != nil { 271 return nil, err 272 } 273 274 switch { 275 case searchType == "commits": 276 // Accept header for search commits preview endpoint 277 // TODO: remove custom Accept header when this API fully launches. 278 req.Header.Set("Accept", mediaTypeCommitSearchPreview) 279 case searchType == "topics": 280 // Accept header for search repositories based on topics preview endpoint 281 // TODO: remove custom Accept header when this API fully launches. 282 req.Header.Set("Accept", mediaTypeTopicsPreview) 283 case searchType == "repositories": 284 // Accept header for search repositories based on topics preview endpoint 285 // TODO: remove custom Accept header when this API fully launches. 286 req.Header.Set("Accept", mediaTypeTopicsPreview) 287 case searchType == "issues": 288 // Accept header for search issues based on reactions preview endpoint 289 // TODO: remove custom Accept header when this API fully launches. 290 req.Header.Set("Accept", mediaTypeReactionsPreview) 291 case opts != nil && opts.TextMatch: 292 // Accept header defaults to "application/vnd.github.v3+json" 293 // We change it here to fetch back text-match metadata 294 req.Header.Set("Accept", "application/vnd.github.v3.text-match+json") 295 } 296 297 return s.client.Do(ctx, req, result) 298} 299