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