Add search commits command (#6817)
This commit is contained in:
parent
ba27e5bfb8
commit
9a1056fc87
12 changed files with 914 additions and 53 deletions
|
|
@ -24,11 +24,12 @@ func (t *TablePrinter) HeaderRow(columns ...string) {
|
|||
t.EndRow()
|
||||
}
|
||||
|
||||
func (tp *TablePrinter) AddTimeField(t time.Time, c func(string) string) {
|
||||
// In tty mode display the fuzzy time difference between now and t.
|
||||
// In nontty mode just display t with the time.RFC3339 format.
|
||||
func (tp *TablePrinter) AddTimeField(now, t time.Time, c func(string) string) {
|
||||
tf := t.Format(time.RFC3339)
|
||||
if tp.isTTY {
|
||||
// TODO: use a static time.Now
|
||||
tf = text.FuzzyAgo(time.Now(), t)
|
||||
tf = text.FuzzyAgo(now, t)
|
||||
}
|
||||
tp.AddField(tf, tableprinter.WithColor(c))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package list
|
|||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/tableprinter"
|
||||
|
|
@ -107,7 +108,7 @@ func listRun(opts *ListOptions) error {
|
|||
if rel.PublishedAt.IsZero() {
|
||||
pubDate = rel.CreatedAt
|
||||
}
|
||||
table.AddTimeField(pubDate, iofmt.Gray)
|
||||
table.AddTimeField(time.Now(), pubDate, iofmt.Gray)
|
||||
table.EndRow()
|
||||
}
|
||||
err = table.Render()
|
||||
|
|
|
|||
172
pkg/cmd/search/commits/commits.go
Normal file
172
pkg/cmd/search/commits/commits.go
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
package commits
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/internal/browser"
|
||||
"github.com/cli/cli/v2/internal/tableprinter"
|
||||
"github.com/cli/cli/v2/internal/text"
|
||||
"github.com/cli/cli/v2/pkg/cmd/search/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/cli/cli/v2/pkg/search"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type CommitsOptions struct {
|
||||
Browser browser.Browser
|
||||
Exporter cmdutil.Exporter
|
||||
IO *iostreams.IOStreams
|
||||
Now time.Time
|
||||
Query search.Query
|
||||
Searcher search.Searcher
|
||||
WebMode bool
|
||||
}
|
||||
|
||||
func NewCmdCommits(f *cmdutil.Factory, runF func(*CommitsOptions) error) *cobra.Command {
|
||||
var order string
|
||||
var sort string
|
||||
opts := &CommitsOptions{
|
||||
Browser: f.Browser,
|
||||
IO: f.IOStreams,
|
||||
Query: search.Query{Kind: search.KindCommits},
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "commits [<query>]",
|
||||
Short: "Search for commits",
|
||||
Long: heredoc.Doc(`
|
||||
Search for commits on GitHub.
|
||||
|
||||
The command supports constructing queries using the GitHub search syntax,
|
||||
using the parameter and qualifier flags, or a combination of the two.
|
||||
|
||||
GitHub search syntax is documented at:
|
||||
<https://docs.github.com/search-github/searching-on-github/searching-commits>
|
||||
`),
|
||||
Example: heredoc.Doc(`
|
||||
# search commits matching set of keywords "readme" and "typo"
|
||||
$ gh search commits readme typo
|
||||
|
||||
# search commits matching phrase "bug fix"
|
||||
$ gh search commits "bug fix"
|
||||
|
||||
# search commits committed by user "monalisa"
|
||||
$ gh search commits --committer=monalisa
|
||||
|
||||
# search commits authored by users with name "Jane Doe"
|
||||
$ gh search commits --author-name="Jane Doe"
|
||||
|
||||
# search commits matching hash "8dd03144ffdc6c0d486d6b705f9c7fba871ee7c3"
|
||||
$ gh search commits --hash=8dd03144ffdc6c0d486d6b705f9c7fba871ee7c3
|
||||
|
||||
# search commits authored before February 1st, 2022
|
||||
$ gh search commits --author-date="<2022-02-01"
|
||||
`),
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
if len(args) == 0 && c.Flags().NFlag() == 0 {
|
||||
return cmdutil.FlagErrorf("specify search keywords or flags")
|
||||
}
|
||||
if opts.Query.Limit < 1 || opts.Query.Limit > shared.SearchMaxResults {
|
||||
return cmdutil.FlagErrorf("`--limit` must be between 1 and 1000")
|
||||
}
|
||||
if c.Flags().Changed("order") {
|
||||
opts.Query.Order = order
|
||||
}
|
||||
if c.Flags().Changed("sort") {
|
||||
opts.Query.Sort = sort
|
||||
}
|
||||
opts.Query.Keywords = args
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
}
|
||||
var err error
|
||||
opts.Searcher, err = shared.Searcher(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return commitsRun(opts)
|
||||
},
|
||||
}
|
||||
|
||||
// Output flags
|
||||
cmdutil.AddJSONFlags(cmd, &opts.Exporter, search.CommitFields)
|
||||
cmd.Flags().BoolVarP(&opts.WebMode, "web", "w", false, "Open the search query in the web browser")
|
||||
|
||||
// Query parameter flags
|
||||
cmd.Flags().IntVarP(&opts.Query.Limit, "limit", "L", 30, "Maximum number of commits to fetch")
|
||||
cmdutil.StringEnumFlag(cmd, &order, "order", "", "desc", []string{"asc", "desc"}, "Order of commits returned, ignored unless '--sort' flag is specified")
|
||||
cmdutil.StringEnumFlag(cmd, &sort, "sort", "", "best-match", []string{"author-date", "committer-date"}, "Sort fetched commits")
|
||||
|
||||
// Query qualifier flags
|
||||
cmd.Flags().StringVar(&opts.Query.Qualifiers.Author, "author", "", "Filter by author")
|
||||
cmd.Flags().StringVar(&opts.Query.Qualifiers.AuthorDate, "author-date", "", "Filter based on authored `date`")
|
||||
cmd.Flags().StringVar(&opts.Query.Qualifiers.AuthorEmail, "author-email", "", "Filter on author email")
|
||||
cmd.Flags().StringVar(&opts.Query.Qualifiers.AuthorName, "author-name", "", "Filter on author name")
|
||||
cmd.Flags().StringVar(&opts.Query.Qualifiers.Committer, "committer", "", "Filter by committer")
|
||||
cmd.Flags().StringVar(&opts.Query.Qualifiers.CommitterDate, "committer-date", "", "Filter based on committed `date`")
|
||||
cmd.Flags().StringVar(&opts.Query.Qualifiers.CommitterEmail, "committer-email", "", "Filter on committer email")
|
||||
cmd.Flags().StringVar(&opts.Query.Qualifiers.CommitterName, "committer-name", "", "Filter on committer name")
|
||||
cmd.Flags().StringVar(&opts.Query.Qualifiers.Hash, "hash", "", "Filter by commit hash")
|
||||
cmdutil.NilBoolFlag(cmd, &opts.Query.Qualifiers.Merge, "merge", "", "Filter on merge commits")
|
||||
cmd.Flags().StringVar(&opts.Query.Qualifiers.Parent, "parent", "", "Filter by parent hash")
|
||||
cmd.Flags().StringSliceVar(&opts.Query.Qualifiers.Repo, "repo", nil, "Filter on repository")
|
||||
cmd.Flags().StringVar(&opts.Query.Qualifiers.Tree, "tree", "", "Filter by tree hash")
|
||||
cmd.Flags().StringVar(&opts.Query.Qualifiers.User, "owner", "", "Filter on repository owner")
|
||||
cmdutil.StringSliceEnumFlag(cmd, &opts.Query.Qualifiers.Is, "visibility", "", nil, []string{"public", "private", "internal"}, "Filter based on repository visibility")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func commitsRun(opts *CommitsOptions) error {
|
||||
io := opts.IO
|
||||
if opts.WebMode {
|
||||
url := opts.Searcher.URL(opts.Query)
|
||||
if io.IsStdoutTTY() {
|
||||
fmt.Fprintf(io.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(url))
|
||||
}
|
||||
return opts.Browser.Browse(url)
|
||||
}
|
||||
io.StartProgressIndicator()
|
||||
result, err := opts.Searcher.Commits(opts.Query)
|
||||
io.StopProgressIndicator()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(result.Items) == 0 && opts.Exporter == nil {
|
||||
return cmdutil.NewNoResultsError("no commits matched your search")
|
||||
}
|
||||
if err := io.StartPager(); err == nil {
|
||||
defer io.StopPager()
|
||||
} else {
|
||||
fmt.Fprintf(io.ErrOut, "failed to start pager: %v\n", err)
|
||||
}
|
||||
if opts.Exporter != nil {
|
||||
return opts.Exporter.Write(io, result.Items)
|
||||
}
|
||||
|
||||
return displayResults(io, opts.Now, result)
|
||||
}
|
||||
|
||||
func displayResults(io *iostreams.IOStreams, now time.Time, results search.CommitsResult) error {
|
||||
if now.IsZero() {
|
||||
now = time.Now()
|
||||
}
|
||||
cs := io.ColorScheme()
|
||||
tp := tableprinter.New(io)
|
||||
for _, commit := range results.Items {
|
||||
tp.AddField(commit.Repo.FullName)
|
||||
tp.AddField(commit.Sha)
|
||||
tp.AddField(text.RemoveExcessiveWhitespace(commit.Info.Message))
|
||||
tp.AddField(commit.Author.Login)
|
||||
tp.AddTimeField(now, commit.Info.Author.Date, cs.Gray)
|
||||
tp.EndRow()
|
||||
}
|
||||
if io.IsStdoutTTY() {
|
||||
header := fmt.Sprintf("Showing %d of %d commits\n\n", len(results.Items), results.Total)
|
||||
fmt.Fprintf(io.Out, "\n%s", header)
|
||||
}
|
||||
return tp.Render()
|
||||
}
|
||||
311
pkg/cmd/search/commits/commits_test.go
Normal file
311
pkg/cmd/search/commits/commits_test.go
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
package commits
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/v2/internal/browser"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/cli/cli/v2/pkg/search"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewCmdCommits(t *testing.T) {
|
||||
var trueBool = true
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
output CommitsOptions
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "no arguments",
|
||||
input: "",
|
||||
wantErr: true,
|
||||
errMsg: "specify search keywords or flags",
|
||||
},
|
||||
{
|
||||
name: "keyword arguments",
|
||||
input: "some search terms",
|
||||
output: CommitsOptions{
|
||||
Query: search.Query{Keywords: []string{"some", "search", "terms"}, Kind: "commits", Limit: 30},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "web flag",
|
||||
input: "--web",
|
||||
output: CommitsOptions{
|
||||
Query: search.Query{Keywords: []string{}, Kind: "commits", Limit: 30},
|
||||
WebMode: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "limit flag",
|
||||
input: "--limit 10",
|
||||
output: CommitsOptions{Query: search.Query{Keywords: []string{}, Kind: "commits", Limit: 10}},
|
||||
},
|
||||
{
|
||||
name: "invalid limit flag",
|
||||
input: "--limit 1001",
|
||||
wantErr: true,
|
||||
errMsg: "`--limit` must be between 1 and 1000",
|
||||
},
|
||||
{
|
||||
name: "order flag",
|
||||
input: "--order asc",
|
||||
output: CommitsOptions{
|
||||
Query: search.Query{Keywords: []string{}, Kind: "commits", Limit: 30, Order: "asc"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid order flag",
|
||||
input: "--order invalid",
|
||||
wantErr: true,
|
||||
errMsg: "invalid argument \"invalid\" for \"--order\" flag: valid values are {asc|desc}",
|
||||
},
|
||||
{
|
||||
name: "qualifier flags",
|
||||
input: `
|
||||
--author=foo
|
||||
--author-date=01-01-2000
|
||||
--author-email=foo@example.com
|
||||
--author-name=Foo
|
||||
--committer=bar
|
||||
--committer-date=01-02-2000
|
||||
--committer-email=bar@example.com
|
||||
--committer-name=Bar
|
||||
--hash=aaa
|
||||
--merge
|
||||
--parent=bbb
|
||||
--repo=owner/repo
|
||||
--tree=ccc
|
||||
--owner=owner
|
||||
--visibility=public
|
||||
`,
|
||||
output: CommitsOptions{
|
||||
Query: search.Query{
|
||||
Keywords: []string{},
|
||||
Kind: "commits",
|
||||
Limit: 30,
|
||||
Qualifiers: search.Qualifiers{
|
||||
Author: "foo",
|
||||
AuthorDate: "01-01-2000",
|
||||
AuthorEmail: "foo@example.com",
|
||||
AuthorName: "Foo",
|
||||
Committer: "bar",
|
||||
CommitterDate: "01-02-2000",
|
||||
CommitterEmail: "bar@example.com",
|
||||
CommitterName: "Bar",
|
||||
Hash: "aaa",
|
||||
Merge: &trueBool,
|
||||
Parent: "bbb",
|
||||
Repo: []string{"owner/repo"},
|
||||
Tree: "ccc",
|
||||
User: "owner",
|
||||
Is: []string{"public"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: ios,
|
||||
}
|
||||
argv, err := shlex.Split(tt.input)
|
||||
assert.NoError(t, err)
|
||||
var gotOpts *CommitsOptions
|
||||
cmd := NewCmdCommits(f, func(opts *CommitsOptions) error {
|
||||
gotOpts = opts
|
||||
return nil
|
||||
})
|
||||
cmd.SetArgs(argv)
|
||||
cmd.SetIn(&bytes.Buffer{})
|
||||
cmd.SetOut(&bytes.Buffer{})
|
||||
cmd.SetErr(&bytes.Buffer{})
|
||||
|
||||
_, err = cmd.ExecuteC()
|
||||
if tt.wantErr {
|
||||
assert.EqualError(t, err, tt.errMsg)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.output.Query, gotOpts.Query)
|
||||
assert.Equal(t, tt.output.WebMode, gotOpts.WebMode)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommitsRun(t *testing.T) {
|
||||
var now = time.Date(2023, 1, 17, 12, 30, 0, 0, time.UTC)
|
||||
var author = search.CommitUser{Date: time.Date(2022, 12, 27, 11, 30, 0, 0, time.UTC)}
|
||||
var committer = search.CommitUser{Date: time.Date(2022, 12, 28, 12, 30, 0, 0, time.UTC)}
|
||||
var query = search.Query{
|
||||
Keywords: []string{"cli"},
|
||||
Kind: "commits",
|
||||
Limit: 30,
|
||||
Qualifiers: search.Qualifiers{},
|
||||
}
|
||||
tests := []struct {
|
||||
errMsg string
|
||||
name string
|
||||
opts *CommitsOptions
|
||||
tty bool
|
||||
wantErr bool
|
||||
wantStderr string
|
||||
wantStdout string
|
||||
}{
|
||||
{
|
||||
name: "displays results tty",
|
||||
opts: &CommitsOptions{
|
||||
Query: query,
|
||||
Searcher: &search.SearcherMock{
|
||||
CommitsFunc: func(query search.Query) (search.CommitsResult, error) {
|
||||
return search.CommitsResult{
|
||||
IncompleteResults: false,
|
||||
Items: []search.Commit{
|
||||
{
|
||||
Author: search.User{Login: "monalisa"},
|
||||
Info: search.CommitInfo{Author: author, Committer: committer, Message: "hello"},
|
||||
Repo: search.Repository{FullName: "test/cli"},
|
||||
Sha: "aaaaaaaa",
|
||||
},
|
||||
{
|
||||
Author: search.User{Login: "johnnytest"},
|
||||
Info: search.CommitInfo{Author: author, Committer: committer, Message: "hi"},
|
||||
Repo: search.Repository{FullName: "test/cliing", IsPrivate: true},
|
||||
Sha: "bbbbbbbb",
|
||||
},
|
||||
{
|
||||
Author: search.User{Login: "hubot"},
|
||||
Info: search.CommitInfo{Author: author, Committer: committer, Message: "greetings"},
|
||||
Repo: search.Repository{FullName: "cli/cli"},
|
||||
Sha: "cccccccc",
|
||||
},
|
||||
},
|
||||
Total: 300,
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
tty: true,
|
||||
wantStdout: "\nShowing 3 of 300 commits\n\ntest/cli aaaaaaaa hello monalisa about 21 days ago\ntest/cliing bbbbbbbb hi johnnytest about 21 days ago\ncli/cli cccccccc greetings hubot about 21 days ago\n",
|
||||
},
|
||||
{
|
||||
name: "displays results notty",
|
||||
opts: &CommitsOptions{
|
||||
Query: query,
|
||||
Searcher: &search.SearcherMock{
|
||||
CommitsFunc: func(query search.Query) (search.CommitsResult, error) {
|
||||
return search.CommitsResult{
|
||||
IncompleteResults: false,
|
||||
Items: []search.Commit{
|
||||
{
|
||||
Author: search.User{Login: "monalisa"},
|
||||
Info: search.CommitInfo{Author: author, Committer: committer, Message: "hello"},
|
||||
Repo: search.Repository{FullName: "test/cli"},
|
||||
Sha: "aaaaaaaa",
|
||||
},
|
||||
{
|
||||
Author: search.User{Login: "johnnytest"},
|
||||
Info: search.CommitInfo{Author: author, Committer: committer, Message: "hi"},
|
||||
Repo: search.Repository{FullName: "test/cliing", IsPrivate: true},
|
||||
Sha: "bbbbbbbb",
|
||||
},
|
||||
{
|
||||
Author: search.User{Login: "hubot"},
|
||||
Info: search.CommitInfo{Author: author, Committer: committer, Message: "greetings"},
|
||||
Repo: search.Repository{FullName: "cli/cli"},
|
||||
Sha: "cccccccc",
|
||||
},
|
||||
},
|
||||
Total: 300,
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
wantStdout: "test/cli\taaaaaaaa\thello\tmonalisa\t2022-12-27T11:30:00Z\ntest/cliing\tbbbbbbbb\thi\tjohnnytest\t2022-12-27T11:30:00Z\ncli/cli\tcccccccc\tgreetings\thubot\t2022-12-27T11:30:00Z\n",
|
||||
},
|
||||
{
|
||||
name: "displays no results",
|
||||
opts: &CommitsOptions{
|
||||
Query: query,
|
||||
Searcher: &search.SearcherMock{
|
||||
CommitsFunc: func(query search.Query) (search.CommitsResult, error) {
|
||||
return search.CommitsResult{}, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "no commits matched your search",
|
||||
},
|
||||
{
|
||||
name: "displays search error",
|
||||
opts: &CommitsOptions{
|
||||
Query: query,
|
||||
Searcher: &search.SearcherMock{
|
||||
CommitsFunc: func(query search.Query) (search.CommitsResult, error) {
|
||||
return search.CommitsResult{}, fmt.Errorf("error with query")
|
||||
},
|
||||
},
|
||||
},
|
||||
errMsg: "error with query",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "opens browser for web mode tty",
|
||||
opts: &CommitsOptions{
|
||||
Browser: &browser.Stub{},
|
||||
Query: query,
|
||||
Searcher: &search.SearcherMock{
|
||||
URLFunc: func(query search.Query) string {
|
||||
return "https://github.com/search?type=commits&q=cli"
|
||||
},
|
||||
},
|
||||
WebMode: true,
|
||||
},
|
||||
tty: true,
|
||||
wantStderr: "Opening github.com/search in your browser.\n",
|
||||
},
|
||||
{
|
||||
name: "opens browser for web mode notty",
|
||||
opts: &CommitsOptions{
|
||||
Browser: &browser.Stub{},
|
||||
Query: query,
|
||||
Searcher: &search.SearcherMock{
|
||||
URLFunc: func(query search.Query) string {
|
||||
return "https://github.com/search?type=commits&q=cli"
|
||||
},
|
||||
},
|
||||
WebMode: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
ios.SetStdinTTY(tt.tty)
|
||||
ios.SetStdoutTTY(tt.tty)
|
||||
ios.SetStderrTTY(tt.tty)
|
||||
tt.opts.IO = ios
|
||||
tt.opts.Now = now
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := commitsRun(tt.opts)
|
||||
if tt.wantErr {
|
||||
assert.EqualError(t, err, tt.errMsg)
|
||||
return
|
||||
} else if err != nil {
|
||||
t.Fatalf("commitsRun unexpected error: %v", err)
|
||||
}
|
||||
assert.Equal(t, tt.wantStdout, stdout.String())
|
||||
assert.Equal(t, tt.wantStderr, stderr.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
searchCommitsCmd "github.com/cli/cli/v2/pkg/cmd/search/commits"
|
||||
searchIssuesCmd "github.com/cli/cli/v2/pkg/cmd/search/issues"
|
||||
searchPrsCmd "github.com/cli/cli/v2/pkg/cmd/search/prs"
|
||||
searchReposCmd "github.com/cli/cli/v2/pkg/cmd/search/repos"
|
||||
|
|
@ -16,6 +17,7 @@ func NewCmdSearch(f *cmdutil.Factory) *cobra.Command {
|
|||
Long: "Search across all of GitHub.",
|
||||
}
|
||||
|
||||
cmd.AddCommand(searchCommitsCmd.NewCmdCommits(f, nil))
|
||||
cmd.AddCommand(searchIssuesCmd.NewCmdIssues(f, nil))
|
||||
cmd.AddCommand(searchPrsCmd.NewCmdPrs(f, nil))
|
||||
cmd.AddCommand(searchReposCmd.NewCmdRepos(f, nil))
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
const (
|
||||
KindRepositories = "repositories"
|
||||
KindIssues = "issues"
|
||||
KindCommits = "commits"
|
||||
)
|
||||
|
||||
type Query struct {
|
||||
|
|
@ -27,16 +28,24 @@ type Qualifiers struct {
|
|||
Archived *bool
|
||||
Assignee string
|
||||
Author string
|
||||
AuthorDate string
|
||||
AuthorEmail string
|
||||
AuthorName string
|
||||
Base string
|
||||
Closed string
|
||||
Commenter string
|
||||
Comments string
|
||||
Committer string
|
||||
CommitterDate string
|
||||
CommitterEmail string
|
||||
CommitterName string
|
||||
Created string
|
||||
Draft *bool
|
||||
Followers string
|
||||
Fork string
|
||||
Forks string
|
||||
GoodFirstIssues string
|
||||
Hash string
|
||||
Head string
|
||||
HelpWantedIssues string
|
||||
In []string
|
||||
|
|
@ -47,9 +56,11 @@ type Qualifiers struct {
|
|||
Language string
|
||||
License []string
|
||||
Mentions string
|
||||
Merge *bool
|
||||
Merged string
|
||||
Milestone string
|
||||
No []string
|
||||
Parent string
|
||||
Project string
|
||||
Pushed string
|
||||
Reactions string
|
||||
|
|
@ -65,6 +76,7 @@ type Qualifiers struct {
|
|||
TeamReviewRequested string
|
||||
Topic []string
|
||||
Topics string
|
||||
Tree string
|
||||
Type string
|
||||
Updated string
|
||||
User string
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ func TestQueryString(t *testing.T) {
|
|||
Keywords: []string{"some", "keywords"},
|
||||
Qualifiers: Qualifiers{
|
||||
Archived: &trueBool,
|
||||
AuthorEmail: "foo@example.com",
|
||||
CommitterDate: "2021-02-28",
|
||||
Created: "created",
|
||||
Followers: "1",
|
||||
Fork: "true",
|
||||
|
|
@ -38,7 +40,7 @@ func TestQueryString(t *testing.T) {
|
|||
Is: []string{"public"},
|
||||
},
|
||||
},
|
||||
out: "some keywords archived:true created:created followers:1 fork:true forks:2 good-first-issues:3 help-wanted-issues:4 in:description in:readme is:public language:language license:license pushed:updated size:5 stars:6 topic:topic topics:7 user:user",
|
||||
out: "some keywords archived:true author-email:foo@example.com committer-date:2021-02-28 created:created followers:1 fork:true forks:2 good-first-issues:3 help-wanted-issues:4 in:description in:readme is:public language:language license:license pushed:updated size:5 stars:6 topic:topic topics:7 user:user",
|
||||
},
|
||||
{
|
||||
name: "quotes keywords",
|
||||
|
|
@ -74,6 +76,8 @@ func TestQualifiersMap(t *testing.T) {
|
|||
name: "changes qualifiers to map",
|
||||
qualifiers: Qualifiers{
|
||||
Archived: &trueBool,
|
||||
AuthorEmail: "foo@example.com",
|
||||
CommitterDate: "2021-02-28",
|
||||
Created: "created",
|
||||
Followers: "1",
|
||||
Fork: "true",
|
||||
|
|
@ -93,6 +97,8 @@ func TestQualifiersMap(t *testing.T) {
|
|||
},
|
||||
out: map[string][]string{
|
||||
"archived": {"true"},
|
||||
"author-email": {"foo@example.com"},
|
||||
"committer-date": {"2021-02-28"},
|
||||
"created": {"created"},
|
||||
"followers": {"1"},
|
||||
"fork": {"true"},
|
||||
|
|
|
|||
|
|
@ -6,6 +6,17 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
var CommitFields = []string{
|
||||
"author",
|
||||
"commit",
|
||||
"committer",
|
||||
"sha",
|
||||
"id",
|
||||
"parents",
|
||||
"repository",
|
||||
"url",
|
||||
}
|
||||
|
||||
var RepositoryFields = []string{
|
||||
"createdAt",
|
||||
"defaultBranch",
|
||||
|
|
@ -61,6 +72,12 @@ var PullRequestFields = append(IssueFields,
|
|||
"isDraft",
|
||||
)
|
||||
|
||||
type CommitsResult struct {
|
||||
IncompleteResults bool `json:"incomplete_results"`
|
||||
Items []Commit `json:"items"`
|
||||
Total int `json:"total_count"`
|
||||
}
|
||||
|
||||
type RepositoriesResult struct {
|
||||
IncompleteResults bool `json:"incomplete_results"`
|
||||
Items []Repository `json:"items"`
|
||||
|
|
@ -73,6 +90,40 @@ type IssuesResult struct {
|
|||
Total int `json:"total_count"`
|
||||
}
|
||||
|
||||
type Commit struct {
|
||||
Author User `json:"author"`
|
||||
Committer User `json:"committer"`
|
||||
ID string `json:"node_id"`
|
||||
Info CommitInfo `json:"commit"`
|
||||
Parents []Parent `json:"parents"`
|
||||
Repo Repository `json:"repository"`
|
||||
Sha string `json:"sha"`
|
||||
URL string `json:"html_url"`
|
||||
}
|
||||
|
||||
type CommitInfo struct {
|
||||
Author CommitUser `json:"author"`
|
||||
CommentCount int `json:"comment_count"`
|
||||
Committer CommitUser `json:"committer"`
|
||||
Message string `json:"message"`
|
||||
Tree Tree `json:"tree"`
|
||||
}
|
||||
|
||||
type CommitUser struct {
|
||||
Date time.Time `json:"date"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type Tree struct {
|
||||
Sha string `json:"sha"`
|
||||
}
|
||||
|
||||
type Parent struct {
|
||||
Sha string `json:"sha"`
|
||||
URL string `json:"html_url"`
|
||||
}
|
||||
|
||||
type Repository struct {
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
DefaultBranch string `json:"default_branch"`
|
||||
|
|
@ -120,13 +171,6 @@ type User struct {
|
|||
URL string `json:"html_url"`
|
||||
}
|
||||
|
||||
func (u *User) IsBot() bool {
|
||||
// copied from api/queries_issue.go
|
||||
// would ideally be shared, but it would require coordinating a "user"
|
||||
// abstraction in a bunch of places.
|
||||
return u.ID == ""
|
||||
}
|
||||
|
||||
type Issue struct {
|
||||
Assignees []User `json:"assignees"`
|
||||
Author User `json:"user"`
|
||||
|
|
@ -157,18 +201,6 @@ type PullRequest struct {
|
|||
MergedAt time.Time `json:"merged_at"`
|
||||
}
|
||||
|
||||
// the state of an issue or a pull request,
|
||||
// may be either open or closed.
|
||||
// for a pull request, the "merged" state is
|
||||
// inferred from a value for merged_at and
|
||||
// which we take return instead of the "closed" state.
|
||||
func (issue Issue) State() string {
|
||||
if !issue.PullRequest.MergedAt.IsZero() {
|
||||
return "merged"
|
||||
}
|
||||
return issue.StateInternal
|
||||
}
|
||||
|
||||
type Label struct {
|
||||
Color string `json:"color"`
|
||||
Description string `json:"description"`
|
||||
|
|
@ -176,6 +208,83 @@ type Label struct {
|
|||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (u User) IsBot() bool {
|
||||
// copied from api/queries_issue.go
|
||||
// would ideally be shared, but it would require coordinating a "user"
|
||||
// abstraction in a bunch of places.
|
||||
return u.ID == ""
|
||||
}
|
||||
|
||||
func (u User) ExportData() map[string]interface{} {
|
||||
isBot := u.IsBot()
|
||||
login := u.Login
|
||||
if isBot {
|
||||
login = "app/" + login
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"id": u.ID,
|
||||
"login": login,
|
||||
"type": u.Type,
|
||||
"url": u.URL,
|
||||
"is_bot": isBot,
|
||||
}
|
||||
}
|
||||
|
||||
func (commit Commit) ExportData(fields []string) map[string]interface{} {
|
||||
v := reflect.ValueOf(commit)
|
||||
data := map[string]interface{}{}
|
||||
for _, f := range fields {
|
||||
switch f {
|
||||
case "author":
|
||||
data[f] = commit.Author.ExportData()
|
||||
case "commit":
|
||||
info := commit.Info
|
||||
data[f] = map[string]interface{}{
|
||||
"author": map[string]interface{}{
|
||||
"date": info.Author.Date,
|
||||
"email": info.Author.Email,
|
||||
"name": info.Author.Name,
|
||||
},
|
||||
"committer": map[string]interface{}{
|
||||
"date": info.Committer.Date,
|
||||
"email": info.Committer.Email,
|
||||
"name": info.Committer.Name,
|
||||
},
|
||||
"comment_count": info.CommentCount,
|
||||
"message": info.Message,
|
||||
"tree": map[string]interface{}{"sha": info.Tree.Sha},
|
||||
}
|
||||
case "committer":
|
||||
data[f] = commit.Committer.ExportData()
|
||||
case "parents":
|
||||
parents := make([]interface{}, 0, len(commit.Parents))
|
||||
for _, parent := range commit.Parents {
|
||||
parents = append(parents, map[string]interface{}{
|
||||
"sha": parent.Sha,
|
||||
"url": parent.URL,
|
||||
})
|
||||
}
|
||||
data[f] = parents
|
||||
case "repository":
|
||||
repo := commit.Repo
|
||||
data[f] = map[string]interface{}{
|
||||
"description": repo.Description,
|
||||
"fullName": repo.FullName,
|
||||
"name": repo.Name,
|
||||
"id": repo.ID,
|
||||
"isFork": repo.IsFork,
|
||||
"isPrivate": repo.IsPrivate,
|
||||
"owner": repo.Owner.ExportData(),
|
||||
"url": repo.URL,
|
||||
}
|
||||
default:
|
||||
sf := fieldByName(v, f)
|
||||
data[f] = sf.Interface()
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func (repo Repository) ExportData(fields []string) map[string]interface{} {
|
||||
v := reflect.ValueOf(repo)
|
||||
data := map[string]interface{}{}
|
||||
|
|
@ -188,12 +297,7 @@ func (repo Repository) ExportData(fields []string) map[string]interface{} {
|
|||
"url": repo.License.URL,
|
||||
}
|
||||
case "owner":
|
||||
data[f] = map[string]interface{}{
|
||||
"id": repo.Owner.ID,
|
||||
"login": repo.Owner.Login,
|
||||
"type": repo.Owner.Type,
|
||||
"url": repo.Owner.URL,
|
||||
}
|
||||
data[f] = repo.Owner.ExportData()
|
||||
default:
|
||||
sf := fieldByName(v, f)
|
||||
data[f] = sf.Interface()
|
||||
|
|
@ -202,6 +306,16 @@ func (repo Repository) ExportData(fields []string) map[string]interface{} {
|
|||
return data
|
||||
}
|
||||
|
||||
// The state of an issue or a pull request, may be either open or closed.
|
||||
// For a pull request, the "merged" state is inferred from a value for merged_at and
|
||||
// which we take return instead of the "closed" state.
|
||||
func (issue Issue) State() string {
|
||||
if !issue.PullRequest.MergedAt.IsZero() {
|
||||
return "merged"
|
||||
}
|
||||
return issue.StateInternal
|
||||
}
|
||||
|
||||
func (issue Issue) IsPullRequest() bool {
|
||||
return issue.PullRequest.URL != ""
|
||||
}
|
||||
|
|
@ -214,31 +328,11 @@ func (issue Issue) ExportData(fields []string) map[string]interface{} {
|
|||
case "assignees":
|
||||
assignees := make([]interface{}, 0, len(issue.Assignees))
|
||||
for _, assignee := range issue.Assignees {
|
||||
isBot := assignee.IsBot()
|
||||
login := assignee.Login
|
||||
if isBot {
|
||||
login = "app/" + login
|
||||
}
|
||||
assignees = append(assignees, map[string]interface{}{
|
||||
"id": assignee.ID,
|
||||
"login": login,
|
||||
"type": assignee.Type,
|
||||
"is_bot": isBot,
|
||||
})
|
||||
assignees = append(assignees, assignee.ExportData())
|
||||
}
|
||||
data[f] = assignees
|
||||
case "author":
|
||||
isBot := issue.Author.IsBot()
|
||||
login := issue.Author.Login
|
||||
if isBot {
|
||||
login = "app/" + login
|
||||
}
|
||||
data[f] = map[string]interface{}{
|
||||
"id": issue.Author.ID,
|
||||
"login": login,
|
||||
"type": issue.Author.Type,
|
||||
"is_bot": isBot,
|
||||
}
|
||||
data[f] = issue.Author.ExportData()
|
||||
case "isPullRequest":
|
||||
data[f] = issue.IsPullRequest()
|
||||
case "labels":
|
||||
|
|
|
|||
|
|
@ -11,6 +11,42 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCommitExportData(t *testing.T) {
|
||||
var authoredAt = time.Date(2021, 2, 27, 11, 30, 0, 0, time.UTC)
|
||||
var committedAt = time.Date(2021, 2, 28, 12, 30, 0, 0, time.UTC)
|
||||
tests := []struct {
|
||||
name string
|
||||
fields []string
|
||||
commit Commit
|
||||
output string
|
||||
}{
|
||||
{
|
||||
name: "exports requested fields",
|
||||
fields: []string{"author", "commit", "committer", "sha"},
|
||||
commit: Commit{
|
||||
Author: User{Login: "foo"},
|
||||
Committer: User{Login: "bar", ID: "123"},
|
||||
Info: CommitInfo{
|
||||
Author: CommitUser{Date: authoredAt, Name: "Foo"},
|
||||
Committer: CommitUser{Date: committedAt, Name: "Bar"},
|
||||
Message: "test message",
|
||||
},
|
||||
Sha: "8dd03144ffdc6c0d",
|
||||
},
|
||||
output: `{"author":{"id":"","is_bot":true,"login":"app/foo","type":"","url":""},"commit":{"author":{"date":"2021-02-27T11:30:00Z","email":"","name":"Foo"},"comment_count":0,"committer":{"date":"2021-02-28T12:30:00Z","email":"","name":"Bar"},"message":"test message","tree":{"sha":""}},"committer":{"id":"123","is_bot":false,"login":"bar","type":"","url":""},"sha":"8dd03144ffdc6c0d"}`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
exported := tt.commit.ExportData(tt.fields)
|
||||
buf := bytes.Buffer{}
|
||||
enc := json.NewEncoder(&buf)
|
||||
require.NoError(t, enc.Encode(exported))
|
||||
assert.Equal(t, tt.output, strings.TrimSpace(buf.String()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepositoryExportData(t *testing.T) {
|
||||
var createdAt = time.Date(2021, 2, 28, 12, 30, 0, 0, time.UTC)
|
||||
tests := []struct {
|
||||
|
|
@ -67,7 +103,7 @@ func TestIssueExportData(t *testing.T) {
|
|||
Title: "title",
|
||||
UpdatedAt: updatedAt,
|
||||
},
|
||||
output: `{"assignees":[{"id":"123","is_bot":false,"login":"test","type":""},{"id":"","is_bot":true,"login":"app/foo","type":""}],"body":"body","commentsCount":1,"isLocked":true,"labels":[{"color":"","description":"","id":"","name":"label1"},{"color":"","description":"","id":"","name":"label2"}],"repository":{"name":"repo","nameWithOwner":"owner/repo"},"title":"title","updatedAt":"2021-02-28T12:30:00Z"}`,
|
||||
output: `{"assignees":[{"id":"123","is_bot":false,"login":"test","type":"","url":""},{"id":"","is_bot":true,"login":"app/foo","type":"","url":""}],"body":"body","commentsCount":1,"isLocked":true,"labels":[{"color":"","description":"","id":"","name":"label1"},{"color":"","description":"","id":"","name":"label2"}],"repository":{"name":"repo","nameWithOwner":"owner/repo"},"title":"title","updatedAt":"2021-02-28T12:30:00Z"}`,
|
||||
},
|
||||
{
|
||||
name: "state when issue",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ var jsonTypeRE = regexp.MustCompile(`[/+]json($|;)`)
|
|||
|
||||
//go:generate moq -rm -out searcher_mock.go . Searcher
|
||||
type Searcher interface {
|
||||
Commits(Query) (CommitsResult, error)
|
||||
Repositories(Query) (RepositoriesResult, error)
|
||||
Issues(Query) (IssuesResult, error)
|
||||
URL(Query) string
|
||||
|
|
@ -56,6 +57,30 @@ func NewSearcher(client *http.Client, host string) Searcher {
|
|||
}
|
||||
}
|
||||
|
||||
func (s searcher) Commits(query Query) (CommitsResult, error) {
|
||||
result := CommitsResult{}
|
||||
toRetrieve := query.Limit
|
||||
var resp *http.Response
|
||||
var err error
|
||||
for toRetrieve > 0 {
|
||||
query.Limit = min(toRetrieve, maxPerPage)
|
||||
query.Page = nextPage(resp)
|
||||
if query.Page == 0 {
|
||||
break
|
||||
}
|
||||
page := CommitsResult{}
|
||||
resp, err = s.search(query, &page)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
result.IncompleteResults = page.IncompleteResults
|
||||
result.Total = page.Total
|
||||
result.Items = append(result.Items, page.Items...)
|
||||
toRetrieve = toRetrieve - len(page.Items)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s searcher) Repositories(query Query) (RepositoriesResult, error) {
|
||||
result := RepositoriesResult{}
|
||||
toRetrieve := query.Limit
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ var _ Searcher = &SearcherMock{}
|
|||
//
|
||||
// // make and configure a mocked Searcher
|
||||
// mockedSearcher := &SearcherMock{
|
||||
// CommitsFunc: func(query Query) (CommitsResult, error) {
|
||||
// panic("mock out the Commits method")
|
||||
// },
|
||||
// IssuesFunc: func(query Query) (IssuesResult, error) {
|
||||
// panic("mock out the Issues method")
|
||||
// },
|
||||
|
|
@ -33,6 +36,9 @@ var _ Searcher = &SearcherMock{}
|
|||
//
|
||||
// }
|
||||
type SearcherMock struct {
|
||||
// CommitsFunc mocks the Commits method.
|
||||
CommitsFunc func(query Query) (CommitsResult, error)
|
||||
|
||||
// IssuesFunc mocks the Issues method.
|
||||
IssuesFunc func(query Query) (IssuesResult, error)
|
||||
|
||||
|
|
@ -44,6 +50,11 @@ type SearcherMock struct {
|
|||
|
||||
// calls tracks calls to the methods.
|
||||
calls struct {
|
||||
// Commits holds details about calls to the Commits method.
|
||||
Commits []struct {
|
||||
// Query is the query argument value.
|
||||
Query Query
|
||||
}
|
||||
// Issues holds details about calls to the Issues method.
|
||||
Issues []struct {
|
||||
// Query is the query argument value.
|
||||
|
|
@ -60,11 +71,44 @@ type SearcherMock struct {
|
|||
Query Query
|
||||
}
|
||||
}
|
||||
lockCommits sync.RWMutex
|
||||
lockIssues sync.RWMutex
|
||||
lockRepositories sync.RWMutex
|
||||
lockURL sync.RWMutex
|
||||
}
|
||||
|
||||
// Commits calls CommitsFunc.
|
||||
func (mock *SearcherMock) Commits(query Query) (CommitsResult, error) {
|
||||
if mock.CommitsFunc == nil {
|
||||
panic("SearcherMock.CommitsFunc: method is nil but Searcher.Commits was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Query Query
|
||||
}{
|
||||
Query: query,
|
||||
}
|
||||
mock.lockCommits.Lock()
|
||||
mock.calls.Commits = append(mock.calls.Commits, callInfo)
|
||||
mock.lockCommits.Unlock()
|
||||
return mock.CommitsFunc(query)
|
||||
}
|
||||
|
||||
// CommitsCalls gets all the calls that were made to Commits.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedSearcher.CommitsCalls())
|
||||
func (mock *SearcherMock) CommitsCalls() []struct {
|
||||
Query Query
|
||||
} {
|
||||
var calls []struct {
|
||||
Query Query
|
||||
}
|
||||
mock.lockCommits.RLock()
|
||||
calls = mock.calls.Commits
|
||||
mock.lockCommits.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// Issues calls IssuesFunc.
|
||||
func (mock *SearcherMock) Issues(query Query) (IssuesResult, error) {
|
||||
if mock.IssuesFunc == nil {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,163 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSearcherCommits(t *testing.T) {
|
||||
query := Query{
|
||||
Keywords: []string{"keyword"},
|
||||
Kind: "commits",
|
||||
Limit: 30,
|
||||
Order: "desc",
|
||||
Sort: "committer-date",
|
||||
Qualifiers: Qualifiers{
|
||||
Author: "foobar",
|
||||
CommitterDate: ">2021-02-28",
|
||||
},
|
||||
}
|
||||
|
||||
values := url.Values{
|
||||
"page": []string{"1"},
|
||||
"per_page": []string{"30"},
|
||||
"order": []string{"desc"},
|
||||
"sort": []string{"committer-date"},
|
||||
"q": []string{"keyword author:foobar committer-date:>2021-02-28"},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
host string
|
||||
query Query
|
||||
result CommitsResult
|
||||
wantErr bool
|
||||
errMsg string
|
||||
httpStubs func(*httpmock.Registry)
|
||||
}{
|
||||
{
|
||||
name: "searches commits",
|
||||
query: query,
|
||||
result: CommitsResult{
|
||||
IncompleteResults: false,
|
||||
Items: []Commit{{Sha: "abc"}},
|
||||
Total: 1,
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.QueryMatcher("GET", "search/commits", values),
|
||||
httpmock.JSONResponse(CommitsResult{
|
||||
IncompleteResults: false,
|
||||
Items: []Commit{{Sha: "abc"}},
|
||||
Total: 1,
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "searches commits for enterprise host",
|
||||
host: "enterprise.com",
|
||||
query: query,
|
||||
result: CommitsResult{
|
||||
IncompleteResults: false,
|
||||
Items: []Commit{{Sha: "abc"}},
|
||||
Total: 1,
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.QueryMatcher("GET", "api/v3/search/commits", values),
|
||||
httpmock.JSONResponse(CommitsResult{
|
||||
IncompleteResults: false,
|
||||
Items: []Commit{{Sha: "abc"}},
|
||||
Total: 1,
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paginates results",
|
||||
query: query,
|
||||
result: CommitsResult{
|
||||
IncompleteResults: false,
|
||||
Items: []Commit{{Sha: "abc"}, {Sha: "def"}},
|
||||
Total: 2,
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
firstReq := httpmock.QueryMatcher("GET", "search/commits", values)
|
||||
firstRes := httpmock.JSONResponse(CommitsResult{
|
||||
IncompleteResults: false,
|
||||
Items: []Commit{{Sha: "abc"}},
|
||||
Total: 2,
|
||||
},
|
||||
)
|
||||
firstRes = httpmock.WithHeader(firstRes, "Link", `<https://api.github.com/search/commits?page=2&per_page=100&q=org%3Agithub>; rel="next"`)
|
||||
secondReq := httpmock.QueryMatcher("GET", "search/commits", url.Values{
|
||||
"page": []string{"2"},
|
||||
"per_page": []string{"29"},
|
||||
"order": []string{"desc"},
|
||||
"sort": []string{"committer-date"},
|
||||
"q": []string{"keyword author:foobar committer-date:>2021-02-28"},
|
||||
},
|
||||
)
|
||||
secondRes := httpmock.JSONResponse(CommitsResult{
|
||||
IncompleteResults: false,
|
||||
Items: []Commit{{Sha: "def"}},
|
||||
Total: 2,
|
||||
},
|
||||
)
|
||||
reg.Register(firstReq, firstRes)
|
||||
reg.Register(secondReq, secondRes)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "handles search errors",
|
||||
query: query,
|
||||
wantErr: true,
|
||||
errMsg: heredoc.Doc(`
|
||||
Invalid search query "keyword author:foobar committer-date:>2021-02-28".
|
||||
"blah" is not a recognized date/time format. Please provide an ISO 8601 date/time value, such as YYYY-MM-DD.`),
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.QueryMatcher("GET", "search/commits", values),
|
||||
httpmock.WithHeader(
|
||||
httpmock.StatusStringResponse(422,
|
||||
`{
|
||||
"message":"Validation Failed",
|
||||
"errors":[
|
||||
{
|
||||
"message":"\"blah\" is not a recognized date/time format. Please provide an ISO 8601 date/time value, such as YYYY-MM-DD.",
|
||||
"resource":"Search",
|
||||
"field":"q",
|
||||
"code":"invalid"
|
||||
}
|
||||
],
|
||||
"documentation_url":"https://docs.github.com/v3/search/"
|
||||
}`,
|
||||
), "Content-Type", "application/json"),
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.Verify(t)
|
||||
if tt.httpStubs != nil {
|
||||
tt.httpStubs(reg)
|
||||
}
|
||||
client := &http.Client{Transport: reg}
|
||||
if tt.host == "" {
|
||||
tt.host = "github.com"
|
||||
}
|
||||
searcher := NewSearcher(client, tt.host)
|
||||
result, err := searcher.Commits(tt.query)
|
||||
if tt.wantErr {
|
||||
assert.EqualError(t, err, tt.errMsg)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.result, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearcherRepositories(t *testing.T) {
|
||||
query := Query{
|
||||
Keywords: []string{"keyword"},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue