github.com/google/go-github/v66@v66.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  }