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