github.com/google/go-github/v49@v49.1.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/en/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/en/rest/search#search-repositories 76 func (s *SearchService) Repositories(ctx context.Context, query string, opts *SearchOptions) (*RepositoriesSearchResult, *Response, error) { 77 result := new(RepositoriesSearchResult) 78 resp, err := s.search(ctx, "repositories", &searchParameters{Query: query}, opts, result) 79 if err != nil { 80 return nil, resp, err 81 } 82 83 return result, resp, nil 84 } 85 86 // TopicsSearchResult represents the result of a topics search. 87 type TopicsSearchResult struct { 88 Total *int `json:"total_count,omitempty"` 89 IncompleteResults *bool `json:"incomplete_results,omitempty"` 90 Topics []*TopicResult `json:"items,omitempty"` 91 } 92 93 type TopicResult struct { 94 Name *string `json:"name,omitempty"` 95 DisplayName *string `json:"display_name,omitempty"` 96 ShortDescription *string `json:"short_description,omitempty"` 97 Description *string `json:"description,omitempty"` 98 CreatedBy *string `json:"created_by,omitempty"` 99 CreatedAt *Timestamp `json:"created_at,omitempty"` 100 UpdatedAt *string `json:"updated_at,omitempty"` 101 Featured *bool `json:"featured,omitempty"` 102 Curated *bool `json:"curated,omitempty"` 103 Score *float64 `json:"score,omitempty"` 104 } 105 106 // Topics finds topics via various criteria. Results are sorted by best match. 107 // Please see https://help.github.com/en/articles/searching-topics for more 108 // information about search qualifiers. 109 // 110 // GitHub API docs: https://docs.github.com/en/rest/search#search-topics 111 func (s *SearchService) Topics(ctx context.Context, query string, opts *SearchOptions) (*TopicsSearchResult, *Response, error) { 112 result := new(TopicsSearchResult) 113 resp, err := s.search(ctx, "topics", &searchParameters{Query: query}, opts, result) 114 if err != nil { 115 return nil, resp, err 116 } 117 118 return result, resp, nil 119 } 120 121 // CommitsSearchResult represents the result of a commits search. 122 type CommitsSearchResult struct { 123 Total *int `json:"total_count,omitempty"` 124 IncompleteResults *bool `json:"incomplete_results,omitempty"` 125 Commits []*CommitResult `json:"items,omitempty"` 126 } 127 128 // CommitResult represents a commit object as returned in commit search endpoint response. 129 type CommitResult struct { 130 SHA *string `json:"sha,omitempty"` 131 Commit *Commit `json:"commit,omitempty"` 132 Author *User `json:"author,omitempty"` 133 Committer *User `json:"committer,omitempty"` 134 Parents []*Commit `json:"parents,omitempty"` 135 HTMLURL *string `json:"html_url,omitempty"` 136 URL *string `json:"url,omitempty"` 137 CommentsURL *string `json:"comments_url,omitempty"` 138 139 Repository *Repository `json:"repository,omitempty"` 140 Score *float64 `json:"score,omitempty"` 141 } 142 143 // Commits searches commits via various criteria. 144 // 145 // GitHub API docs: https://docs.github.com/en/rest/search#search-commits 146 func (s *SearchService) Commits(ctx context.Context, query string, opts *SearchOptions) (*CommitsSearchResult, *Response, error) { 147 result := new(CommitsSearchResult) 148 resp, err := s.search(ctx, "commits", &searchParameters{Query: query}, opts, result) 149 if err != nil { 150 return nil, resp, err 151 } 152 153 return result, resp, nil 154 } 155 156 // IssuesSearchResult represents the result of an issues search. 157 type IssuesSearchResult struct { 158 Total *int `json:"total_count,omitempty"` 159 IncompleteResults *bool `json:"incomplete_results,omitempty"` 160 Issues []*Issue `json:"items,omitempty"` 161 } 162 163 // Issues searches issues via various criteria. 164 // 165 // GitHub API docs: https://docs.github.com/en/rest/search#search-issues-and-pull-requests 166 func (s *SearchService) Issues(ctx context.Context, query string, opts *SearchOptions) (*IssuesSearchResult, *Response, error) { 167 result := new(IssuesSearchResult) 168 resp, err := s.search(ctx, "issues", &searchParameters{Query: query}, opts, result) 169 if err != nil { 170 return nil, resp, err 171 } 172 173 return result, resp, nil 174 } 175 176 // UsersSearchResult represents the result of a users search. 177 type UsersSearchResult struct { 178 Total *int `json:"total_count,omitempty"` 179 IncompleteResults *bool `json:"incomplete_results,omitempty"` 180 Users []*User `json:"items,omitempty"` 181 } 182 183 // Users searches users via various criteria. 184 // 185 // GitHub API docs: https://docs.github.com/en/rest/search#search-users 186 func (s *SearchService) Users(ctx context.Context, query string, opts *SearchOptions) (*UsersSearchResult, *Response, error) { 187 result := new(UsersSearchResult) 188 resp, err := s.search(ctx, "users", &searchParameters{Query: query}, opts, result) 189 if err != nil { 190 return nil, resp, err 191 } 192 193 return result, resp, nil 194 } 195 196 // Match represents a single text match. 197 type Match struct { 198 Text *string `json:"text,omitempty"` 199 Indices []int `json:"indices,omitempty"` 200 } 201 202 // TextMatch represents a text match for a SearchResult 203 type TextMatch struct { 204 ObjectURL *string `json:"object_url,omitempty"` 205 ObjectType *string `json:"object_type,omitempty"` 206 Property *string `json:"property,omitempty"` 207 Fragment *string `json:"fragment,omitempty"` 208 Matches []*Match `json:"matches,omitempty"` 209 } 210 211 func (tm TextMatch) String() string { 212 return Stringify(tm) 213 } 214 215 // CodeSearchResult represents the result of a code search. 216 type CodeSearchResult struct { 217 Total *int `json:"total_count,omitempty"` 218 IncompleteResults *bool `json:"incomplete_results,omitempty"` 219 CodeResults []*CodeResult `json:"items,omitempty"` 220 } 221 222 // CodeResult represents a single search result. 223 type CodeResult struct { 224 Name *string `json:"name,omitempty"` 225 Path *string `json:"path,omitempty"` 226 SHA *string `json:"sha,omitempty"` 227 HTMLURL *string `json:"html_url,omitempty"` 228 Repository *Repository `json:"repository,omitempty"` 229 TextMatches []*TextMatch `json:"text_matches,omitempty"` 230 } 231 232 func (c CodeResult) String() string { 233 return Stringify(c) 234 } 235 236 // Code searches code via various criteria. 237 // 238 // GitHub API docs: https://docs.github.com/en/rest/search#search-code 239 func (s *SearchService) Code(ctx context.Context, query string, opts *SearchOptions) (*CodeSearchResult, *Response, error) { 240 result := new(CodeSearchResult) 241 resp, err := s.search(ctx, "code", &searchParameters{Query: query}, opts, result) 242 if err != nil { 243 return nil, resp, err 244 } 245 246 return result, resp, nil 247 } 248 249 // LabelsSearchResult represents the result of a code search. 250 type LabelsSearchResult struct { 251 Total *int `json:"total_count,omitempty"` 252 IncompleteResults *bool `json:"incomplete_results,omitempty"` 253 Labels []*LabelResult `json:"items,omitempty"` 254 } 255 256 // LabelResult represents a single search result. 257 type LabelResult struct { 258 ID *int64 `json:"id,omitempty"` 259 URL *string `json:"url,omitempty"` 260 Name *string `json:"name,omitempty"` 261 Color *string `json:"color,omitempty"` 262 Default *bool `json:"default,omitempty"` 263 Description *string `json:"description,omitempty"` 264 Score *float64 `json:"score,omitempty"` 265 } 266 267 func (l LabelResult) String() string { 268 return Stringify(l) 269 } 270 271 // Labels searches labels in the repository with ID repoID via various criteria. 272 // 273 // GitHub API docs: https://docs.github.com/en/rest/search#search-labels 274 func (s *SearchService) Labels(ctx context.Context, repoID int64, query string, opts *SearchOptions) (*LabelsSearchResult, *Response, error) { 275 result := new(LabelsSearchResult) 276 resp, err := s.search(ctx, "labels", &searchParameters{RepositoryID: &repoID, Query: query}, opts, result) 277 if err != nil { 278 return nil, resp, err 279 } 280 281 return result, resp, nil 282 } 283 284 // Helper function that executes search queries against different 285 // GitHub search types (repositories, commits, code, issues, users, labels) 286 // 287 // If searchParameters.Query includes multiple condition, it MUST NOT include "+" as condition separator. 288 // For example, querying with "language:c++" and "leveldb", then searchParameters.Query should be "language:c++ leveldb" but not "language:c+++leveldb". 289 func (s *SearchService) search(ctx context.Context, searchType string, parameters *searchParameters, opts *SearchOptions, result interface{}) (*Response, error) { 290 params, err := qs.Values(opts) 291 if err != nil { 292 return nil, err 293 } 294 295 if parameters.RepositoryID != nil { 296 params.Set("repository_id", strconv.FormatInt(*parameters.RepositoryID, 10)) 297 } 298 params.Set("q", parameters.Query) 299 u := fmt.Sprintf("search/%s?%s", searchType, params.Encode()) 300 301 req, err := s.client.NewRequest("GET", u, nil) 302 if err != nil { 303 return nil, err 304 } 305 var acceptHeaders []string 306 switch { 307 case searchType == "commits": 308 // Accept header for search commits preview endpoint 309 // TODO: remove custom Accept header when this API fully launches. 310 acceptHeaders = append(acceptHeaders, mediaTypeCommitSearchPreview) 311 case searchType == "topics": 312 // Accept header for search repositories based on topics preview endpoint 313 // TODO: remove custom Accept header when this API fully launches. 314 acceptHeaders = append(acceptHeaders, mediaTypeTopicsPreview) 315 case searchType == "repositories": 316 // Accept header for search repositories based on topics preview endpoint 317 // TODO: remove custom Accept header when this API fully launches. 318 acceptHeaders = append(acceptHeaders, mediaTypeTopicsPreview) 319 case searchType == "issues": 320 // Accept header for search issues based on reactions preview endpoint 321 // TODO: remove custom Accept header when this API fully launches. 322 acceptHeaders = append(acceptHeaders, mediaTypeReactionsPreview) 323 } 324 // https://docs.github.com/en/rest/search#search-repositories 325 // Accept header defaults to "application/vnd.github.v3+json" 326 // We change it here to fetch back text-match metadata 327 if opts != nil && opts.TextMatch { 328 acceptHeaders = append(acceptHeaders, "application/vnd.github.v3.text-match+json") 329 } 330 req.Header.Set("Accept", strings.Join(acceptHeaders, ", ")) 331 332 return s.client.Do(ctx, req, result) 333 }