package search import ( "encoding/json" "reflect" "strings" "time" "github.com/cli/cli/v2/pkg/cmdutil" ) var CodeFields = []string{ "path", "repository", "sha", "textMatches", "url", } var textMatchFields = []string{ "fragment", "matches", "type", "property", } var CommitFields = []string{ "author", "commit", "committer", "sha", "id", "parents", "repository", "url", } var RepositoryFields = []string{ "createdAt", "defaultBranch", "description", "forksCount", "fullName", "hasDownloads", "hasIssues", "hasPages", "hasProjects", "hasWiki", "homepage", "id", "isArchived", "isDisabled", "isFork", "isPrivate", "language", "license", "name", "openIssuesCount", "owner", "pushedAt", "size", "stargazersCount", "updatedAt", "url", "visibility", "watchersCount", } var IssueFields = []string{ "assignees", "author", "authorAssociation", "body", "closedAt", "commentsCount", "createdAt", "id", "isLocked", "isPullRequest", "labels", "number", "repository", "state", "title", "updatedAt", "url", } var PullRequestFields = append(IssueFields, "isDraft", ) type CodeResult struct { IncompleteResults bool `json:"incomplete_results"` Items []Code `json:"items"` // Number of code search results matching the query on the server. Ignoring limit. Total int `json:"total_count"` } type CommitsResult struct { IncompleteResults bool `json:"incomplete_results"` Items []Commit `json:"items"` // Number of commits matching the query on the server. Ignoring limit. Total int `json:"total_count"` } type RepositoriesResult struct { IncompleteResults bool `json:"incomplete_results"` Items []Repository `json:"items"` // Number of repositories matching the query on the server. Ignoring limit. Total int `json:"total_count"` } type IssuesResult struct { IncompleteResults bool `json:"incomplete_results"` Items []Issue `json:"items"` // Number of isssues matching the query on the server. Ignoring limit. Total int `json:"total_count"` } type Code struct { Name string `json:"name"` Path string `json:"path"` Repository Repository `json:"repository"` Sha string `json:"sha"` TextMatches []TextMatch `json:"text_matches"` URL string `json:"html_url"` } type TextMatch struct { Fragment string `json:"fragment"` Matches []Match `json:"matches"` Type string `json:"object_type"` Property string `json:"property"` } type Match struct { Indices []int `json:"indices"` Text string `json:"text"` } 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"` Description string `json:"description"` ForksCount int `json:"forks_count"` FullName string `json:"full_name"` HasDownloads bool `json:"has_downloads"` HasIssues bool `json:"has_issues"` HasPages bool `json:"has_pages"` HasProjects bool `json:"has_projects"` HasWiki bool `json:"has_wiki"` Homepage string `json:"homepage"` ID string `json:"node_id"` IsArchived bool `json:"archived"` IsDisabled bool `json:"disabled"` IsFork bool `json:"fork"` IsPrivate bool `json:"private"` Language string `json:"language"` License License `json:"license"` MasterBranch string `json:"master_branch"` Name string `json:"name"` OpenIssuesCount int `json:"open_issues_count"` Owner User `json:"owner"` PushedAt time.Time `json:"pushed_at"` Size int `json:"size"` StargazersCount int `json:"stargazers_count"` URL string `json:"html_url"` UpdatedAt time.Time `json:"updated_at"` Visibility string `json:"visibility"` WatchersCount int `json:"watchers_count"` } type License struct { Key string `json:"key"` Name string `json:"name"` URL string `json:"url"` } type User struct { GravatarID string `json:"gravatar_id"` ID string `json:"node_id"` Login string `json:"login"` SiteAdmin bool `json:"site_admin"` Type string `json:"type"` URL string `json:"html_url"` } type Issue struct { Assignees []User `json:"assignees"` Author User `json:"user"` AuthorAssociation string `json:"author_association"` Body string `json:"body"` ClosedAt time.Time `json:"closed_at"` CommentsCount int `json:"comments"` CreatedAt time.Time `json:"created_at"` ID string `json:"node_id"` Labels []Label `json:"labels"` // This is a PullRequest field which does not appear in issue results, // but lives outside the PullRequest object. IsDraft *bool `json:"draft,omitempty"` IsLocked bool `json:"locked"` Number int `json:"number"` PullRequest PullRequest `json:"pull_request"` RepositoryURL string `json:"repository_url"` // StateInternal should not be used directly. Use State() instead. StateInternal string `json:"state"` StateReason string `json:"state_reason"` Title string `json:"title"` URL string `json:"html_url"` UpdatedAt time.Time `json:"updated_at"` } type PullRequest struct { URL string `json:"html_url"` MergedAt time.Time `json:"merged_at"` } type Label struct { Color string `json:"color"` Description string `json:"description"` ID string `json:"node_id"` 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 (code Code) ExportData(fields []string) map[string]interface{} { v := reflect.ValueOf(code) data := map[string]interface{}{} for _, f := range fields { switch f { case "textMatches": matches := make([]interface{}, 0, len(code.TextMatches)) for _, match := range code.TextMatches { matches = append(matches, match.ExportData(textMatchFields)) } data[f] = matches default: sf := fieldByName(v, f) data[f] = sf.Interface() } } return data } func (textMatch TextMatch) ExportData(fields []string) map[string]interface{} { return cmdutil.StructExportData(textMatch, fields) } 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{}{} for _, f := range fields { switch f { case "license": data[f] = map[string]interface{}{ "key": repo.License.Key, "name": repo.License.Name, "url": repo.License.URL, } case "owner": data[f] = repo.Owner.ExportData() default: sf := fieldByName(v, f) data[f] = sf.Interface() } } return data } func (repo Repository) MarshalJSON() ([]byte, error) { return json.Marshal(map[string]interface{}{ "id": repo.ID, "nameWithOwner": repo.FullName, "url": repo.URL, "isPrivate": repo.IsPrivate, "isFork": repo.IsFork, }) } // 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 != "" } func (issue Issue) ExportData(fields []string) map[string]interface{} { v := reflect.ValueOf(issue) data := map[string]interface{}{} for _, f := range fields { switch f { case "assignees": assignees := make([]interface{}, 0, len(issue.Assignees)) for _, assignee := range issue.Assignees { assignees = append(assignees, assignee.ExportData()) } data[f] = assignees case "author": data[f] = issue.Author.ExportData() case "isPullRequest": data[f] = issue.IsPullRequest() case "labels": labels := make([]interface{}, 0, len(issue.Labels)) for _, label := range issue.Labels { labels = append(labels, map[string]interface{}{ "color": label.Color, "description": label.Description, "id": label.ID, "name": label.Name, }) } data[f] = labels case "repository": comp := strings.Split(issue.RepositoryURL, "/") name := comp[len(comp)-1] nameWithOwner := strings.Join(comp[len(comp)-2:], "/") data[f] = map[string]interface{}{ "name": name, "nameWithOwner": nameWithOwner, } case "state": data[f] = issue.State() default: sf := fieldByName(v, f) data[f] = sf.Interface() } } return data } func fieldByName(v reflect.Value, field string) reflect.Value { return v.FieldByNameFunc(func(s string) bool { return strings.EqualFold(field, s) }) }