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