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 "net/url" 12 "strconv" 13 "strings" 14 15 qs "github.com/google/go-querystring/query" 16) 17 18// SearchService provides access to the search related functions 19// in the GitHub API. 20// 21// Each method takes a query string defining the search keywords and any search qualifiers. 22// For example, when searching issues, the query "gopher is:issue language:go" will search 23// for issues containing the word "gopher" in Go repositories. The method call 24// opts := &github.SearchOptions{Sort: "created", Order: "asc"} 25// cl.Search.Issues(ctx, "gopher is:issue language:go", opts) 26// will search for such issues, sorting by creation date in ascending order 27// (i.e., oldest first). 28// 29// GitHub API docs: https://developer.github.com/v3/search/ 30type SearchService service 31 32// SearchOptions specifies optional parameters to the SearchService methods. 33type SearchOptions struct { 34 // How to sort the search results. Possible values are: 35 // - for repositories: stars, fork, updated 36 // - for commits: author-date, committer-date 37 // - for code: indexed 38 // - for issues: comments, created, updated 39 // - for users: followers, repositories, joined 40 // 41 // Default is to sort by best match. 42 Sort string `url:"sort,omitempty"` 43 44 // Sort order if sort parameter is provided. Possible values are: asc, 45 // desc. Default is desc. 46 Order string `url:"order,omitempty"` 47 48 // Whether to retrieve text match metadata with a query 49 TextMatch bool `url:"-"` 50 51 ListOptions 52} 53 54// Common search parameters. 55type searchParameters struct { 56 Query string 57 RepositoryID *int64 // Sent if non-nil. 58} 59 60// RepositoriesSearchResult represents the result of a repositories search. 61type RepositoriesSearchResult struct { 62 Total *int `json:"total_count,omitempty"` 63 IncompleteResults *bool `json:"incomplete_results,omitempty"` 64 Repositories []Repository `json:"items,omitempty"` 65} 66 67// Repositories searches repositories via various criteria. 68// 69// GitHub API docs: https://developer.github.com/v3/search/#search-repositories 70func (s *SearchService) Repositories(ctx context.Context, query string, opt *SearchOptions) (*RepositoriesSearchResult, *Response, error) { 71 result := new(RepositoriesSearchResult) 72 resp, err := s.search(ctx, "repositories", &searchParameters{Query: query}, opt, result) 73 return result, resp, err 74} 75 76// CommitsSearchResult represents the result of a commits search. 77type CommitsSearchResult struct { 78 Total *int `json:"total_count,omitempty"` 79 IncompleteResults *bool `json:"incomplete_results,omitempty"` 80 Commits []*CommitResult `json:"items,omitempty"` 81} 82 83// CommitResult represents a commit object as returned in commit search endpoint response. 84type CommitResult struct { 85 SHA *string `json:"sha,omitempty"` 86 Commit *Commit `json:"commit,omitempty"` 87 Author *User `json:"author,omitempty"` 88 Committer *User `json:"committer,omitempty"` 89 Parents []*Commit `json:"parents,omitempty"` 90 HTMLURL *string `json:"html_url,omitempty"` 91 URL *string `json:"url,omitempty"` 92 CommentsURL *string `json:"comments_url,omitempty"` 93 94 Repository *Repository `json:"repository,omitempty"` 95 Score *float64 `json:"score,omitempty"` 96} 97 98// Commits searches commits via various criteria. 99// 100// GitHub API docs: https://developer.github.com/v3/search/#search-commits 101func (s *SearchService) Commits(ctx context.Context, query string, opt *SearchOptions) (*CommitsSearchResult, *Response, error) { 102 result := new(CommitsSearchResult) 103 resp, err := s.search(ctx, "commits", &searchParameters{Query: query}, opt, result) 104 return result, resp, err 105} 106 107// IssuesSearchResult represents the result of an issues search. 108type IssuesSearchResult struct { 109 Total *int `json:"total_count,omitempty"` 110 IncompleteResults *bool `json:"incomplete_results,omitempty"` 111 Issues []Issue `json:"items,omitempty"` 112} 113 114// Issues searches issues via various criteria. 115// 116// GitHub API docs: https://developer.github.com/v3/search/#search-issues 117func (s *SearchService) Issues(ctx context.Context, query string, opt *SearchOptions) (*IssuesSearchResult, *Response, error) { 118 result := new(IssuesSearchResult) 119 resp, err := s.search(ctx, "issues", &searchParameters{Query: query}, opt, result) 120 return result, resp, err 121} 122 123// UsersSearchResult represents the result of a users search. 124type UsersSearchResult struct { 125 Total *int `json:"total_count,omitempty"` 126 IncompleteResults *bool `json:"incomplete_results,omitempty"` 127 Users []User `json:"items,omitempty"` 128} 129 130// Users searches users via various criteria. 131// 132// GitHub API docs: https://developer.github.com/v3/search/#search-users 133func (s *SearchService) Users(ctx context.Context, query string, opt *SearchOptions) (*UsersSearchResult, *Response, error) { 134 result := new(UsersSearchResult) 135 resp, err := s.search(ctx, "users", &searchParameters{Query: query}, opt, result) 136 return result, resp, err 137} 138 139// Match represents a single text match. 140type Match struct { 141 Text *string `json:"text,omitempty"` 142 Indices []int `json:"indices,omitempty"` 143} 144 145// TextMatch represents a text match for a SearchResult 146type TextMatch struct { 147 ObjectURL *string `json:"object_url,omitempty"` 148 ObjectType *string `json:"object_type,omitempty"` 149 Property *string `json:"property,omitempty"` 150 Fragment *string `json:"fragment,omitempty"` 151 Matches []Match `json:"matches,omitempty"` 152} 153 154func (tm TextMatch) String() string { 155 return Stringify(tm) 156} 157 158// CodeSearchResult represents the result of a code search. 159type CodeSearchResult struct { 160 Total *int `json:"total_count,omitempty"` 161 IncompleteResults *bool `json:"incomplete_results,omitempty"` 162 CodeResults []CodeResult `json:"items,omitempty"` 163} 164 165// CodeResult represents a single search result. 166type CodeResult struct { 167 Name *string `json:"name,omitempty"` 168 Path *string `json:"path,omitempty"` 169 SHA *string `json:"sha,omitempty"` 170 HTMLURL *string `json:"html_url,omitempty"` 171 Repository *Repository `json:"repository,omitempty"` 172 TextMatches []TextMatch `json:"text_matches,omitempty"` 173} 174 175func (c CodeResult) String() string { 176 return Stringify(c) 177} 178 179// Code searches code via various criteria. 180// 181// GitHub API docs: https://developer.github.com/v3/search/#search-code 182func (s *SearchService) Code(ctx context.Context, query string, opt *SearchOptions) (*CodeSearchResult, *Response, error) { 183 result := new(CodeSearchResult) 184 resp, err := s.search(ctx, "code", &searchParameters{Query: query}, opt, result) 185 return result, resp, err 186} 187 188// LabelsSearchResult represents the result of a code search. 189type LabelsSearchResult struct { 190 Total *int `json:"total_count,omitempty"` 191 IncompleteResults *bool `json:"incomplete_results,omitempty"` 192 Labels []*LabelResult `json:"items,omitempty"` 193} 194 195// LabelResult represents a single search result. 196type LabelResult struct { 197 ID *int64 `json:"id,omitempty"` 198 URL *string `json:"url,omitempty"` 199 Name *string `json:"name,omitempty"` 200 Color *string `json:"color,omitempty"` 201 Default *bool `json:"default,omitempty"` 202 Description *string `json:"description,omitempty"` 203 Score *float64 `json:"score,omitempty"` 204} 205 206func (l LabelResult) String() string { 207 return Stringify(l) 208} 209 210// Labels searches labels in the repository with ID repoID via various criteria. 211// 212// GitHub API docs: https://developer.github.com/v3/search/#search-labels 213func (s *SearchService) Labels(ctx context.Context, repoID int64, query string, opt *SearchOptions) (*LabelsSearchResult, *Response, error) { 214 result := new(LabelsSearchResult) 215 resp, err := s.search(ctx, "labels", &searchParameters{RepositoryID: &repoID, Query: query}, opt, result) 216 return result, resp, err 217} 218 219// Helper function that executes search queries against different 220// GitHub search types (repositories, commits, code, issues, users, labels) 221func (s *SearchService) search(ctx context.Context, searchType string, parameters *searchParameters, opt *SearchOptions, result interface{}) (*Response, error) { 222 params, err := qs.Values(opt) 223 if err != nil { 224 return nil, err 225 } 226 q := strings.Replace(parameters.Query, " ", "+", -1) 227 if parameters.RepositoryID != nil { 228 params.Set("repository_id", strconv.FormatInt(*parameters.RepositoryID, 10)) 229 } 230 query := "q=" + url.PathEscape(q) 231 if v := params.Encode(); v != "" { 232 query = query + "&" + v 233 } 234 u := fmt.Sprintf("search/%s?%s", searchType, query) 235 236 req, err := s.client.NewRequest("GET", u, nil) 237 if err != nil { 238 return nil, err 239 } 240 241 switch { 242 case searchType == "commits": 243 // Accept header for search commits preview endpoint 244 // TODO: remove custom Accept header when this API fully launches. 245 req.Header.Set("Accept", mediaTypeCommitSearchPreview) 246 case searchType == "repositories": 247 // Accept header for search repositories based on topics preview endpoint 248 // TODO: remove custom Accept header when this API fully launches. 249 req.Header.Set("Accept", mediaTypeTopicsPreview) 250 case searchType == "labels": 251 // Accept header for search labels based on label description preview endpoint. 252 // TODO: remove custom Accept header when this API fully launches. 253 req.Header.Set("Accept", mediaTypeLabelDescriptionSearchPreview) 254 case opt != nil && opt.TextMatch: 255 // Accept header defaults to "application/vnd.github.v3+json" 256 // We change it here to fetch back text-match metadata 257 req.Header.Set("Accept", "application/vnd.github.v3.text-match+json") 258 } 259 260 return s.client.Do(ctx, req, result) 261} 262