mirror of
https://github.com/lukaszraczylo/git-velocity.git
synced 2026-06-08 22:59:30 +00:00
Use github graphql client as primary, fallback to rest client
This commit is contained in:
@@ -41,6 +41,7 @@ func DefaultRetryConfig() RetryConfig {
|
||||
// Client wraps the GitHub API client with rate limiting and caching
|
||||
type Client struct {
|
||||
gh *github.Client
|
||||
gql *GraphQLClient // GraphQL client for batched queries
|
||||
config *config.Config
|
||||
cache cache.Cache
|
||||
retry RetryConfig
|
||||
@@ -91,8 +92,15 @@ func NewClient(ctx context.Context, cfg *config.Config) (*Client, error) {
|
||||
c = cache.NewNoopCache()
|
||||
}
|
||||
|
||||
// Initialize GraphQL client if using token auth (GraphQL doesn't support GitHub App auth easily)
|
||||
var gql *GraphQLClient
|
||||
if cfg.HasGithubToken() && cfg.Options.UseGraphQL {
|
||||
gql = NewGraphQLClient(cfg.Auth.GithubToken)
|
||||
}
|
||||
|
||||
return &Client{
|
||||
gh: gh,
|
||||
gql: gql,
|
||||
config: cfg,
|
||||
cache: c,
|
||||
retry: DefaultRetryConfig(),
|
||||
@@ -107,6 +115,73 @@ func (c *Client) SetProgressCallback(cb ProgressCallback) {
|
||||
}
|
||||
}
|
||||
|
||||
// HasGraphQL returns true if the GraphQL client is available
|
||||
func (c *Client) HasGraphQL() bool {
|
||||
return c.gql != nil
|
||||
}
|
||||
|
||||
// FetchPRsWithReviewsGraphQL fetches PRs and reviews using GraphQL (much fewer API calls)
|
||||
func (c *Client) FetchPRsWithReviewsGraphQL(ctx context.Context, owner, repo string, since, until *time.Time) ([]models.PullRequest, []models.Review, error) {
|
||||
if c.gql == nil {
|
||||
return nil, nil, fmt.Errorf("GraphQL client not initialized")
|
||||
}
|
||||
|
||||
cacheKey := fmt.Sprintf("gql_prs_reviews:%s/%s:%v:%v", owner, repo, since, until)
|
||||
|
||||
// Check cache
|
||||
type cachedData struct {
|
||||
PRs []models.PullRequest
|
||||
Reviews []models.Review
|
||||
}
|
||||
if cached, ok := c.cache.Get(cacheKey); ok {
|
||||
if data, ok := cached.(cachedData); ok {
|
||||
c.progress(" Using cached PRs and reviews data (GraphQL)")
|
||||
return data.PRs, data.Reviews, nil
|
||||
}
|
||||
}
|
||||
|
||||
prs, reviews, err := c.gql.FetchPRsWithReviews(ctx, owner, repo, since, until)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Cache results
|
||||
c.cache.Set(cacheKey, cachedData{PRs: prs, Reviews: reviews})
|
||||
|
||||
return prs, reviews, nil
|
||||
}
|
||||
|
||||
// FetchIssuesWithCommentsGraphQL fetches issues and comments using GraphQL (much fewer API calls)
|
||||
func (c *Client) FetchIssuesWithCommentsGraphQL(ctx context.Context, owner, repo string, since, until *time.Time) ([]models.Issue, []models.IssueComment, error) {
|
||||
if c.gql == nil {
|
||||
return nil, nil, fmt.Errorf("GraphQL client not initialized")
|
||||
}
|
||||
|
||||
cacheKey := fmt.Sprintf("gql_issues_comments:%s/%s:%v:%v", owner, repo, since, until)
|
||||
|
||||
// Check cache
|
||||
type cachedData struct {
|
||||
Issues []models.Issue
|
||||
Comments []models.IssueComment
|
||||
}
|
||||
if cached, ok := c.cache.Get(cacheKey); ok {
|
||||
if data, ok := cached.(cachedData); ok {
|
||||
c.progress(" Using cached issues and comments data (GraphQL)")
|
||||
return data.Issues, data.Comments, nil
|
||||
}
|
||||
}
|
||||
|
||||
issues, comments, err := c.gql.FetchIssuesWithComments(ctx, owner, repo, since, until)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Cache results
|
||||
c.cache.Set(cacheKey, cachedData{Issues: issues, Comments: comments})
|
||||
|
||||
return issues, comments, nil
|
||||
}
|
||||
|
||||
// SetRetryConfig sets the retry configuration
|
||||
func (c *Client) SetRetryConfig(rc RetryConfig) {
|
||||
c.retry = rc
|
||||
@@ -459,6 +534,7 @@ func (c *Client) fetchPRsForBranch(ctx context.Context, owner, repo, baseBranch
|
||||
ResourceName: "pull requests",
|
||||
EarlyTermination: true,
|
||||
EarlyTerminationThreshold: 2,
|
||||
Quiet: true, // Parent function handles progress
|
||||
}
|
||||
|
||||
return FetchAllPages(ctx, c, "", config, fetcher) // Empty cache key - parent handles caching
|
||||
@@ -489,6 +565,7 @@ func (c *Client) FetchReviews(ctx context.Context, owner, repo string, prNumber
|
||||
|
||||
config := DefaultFetchConfig("reviews")
|
||||
config.EarlyTermination = false // Reviews don't need date-based early termination
|
||||
config.Quiet = true // Suppress per-page progress (called many times in parallel)
|
||||
|
||||
return FetchAllPages(ctx, c, cacheKey, config, fetcher)
|
||||
}
|
||||
|
||||
@@ -52,6 +52,8 @@ type FetchConfig struct {
|
||||
EarlyTermination bool
|
||||
// EarlyTerminationThreshold is the number of consecutive old pages before stopping
|
||||
EarlyTerminationThreshold int
|
||||
// Quiet suppresses per-page progress messages (useful for sub-fetches like reviews)
|
||||
Quiet bool
|
||||
}
|
||||
|
||||
// DefaultFetchConfig returns sensible defaults
|
||||
@@ -91,8 +93,15 @@ func FetchAllPages[T any, R any](
|
||||
return nil, fmt.Errorf("failed to fetch %s: %w", config.ResourceName, err)
|
||||
}
|
||||
|
||||
c.progress(fmt.Sprintf(" Fetching %s page %d (%d %s so far)...",
|
||||
config.ResourceName, page, len(allResults), config.ResourceName))
|
||||
// Safety check for nil response
|
||||
if resp == nil {
|
||||
break
|
||||
}
|
||||
|
||||
if !config.Quiet {
|
||||
c.progress(fmt.Sprintf(" Fetching %s page %d (%d %s so far)...",
|
||||
config.ResourceName, page, len(allResults), config.ResourceName))
|
||||
}
|
||||
|
||||
oldInPage := 0
|
||||
totalEligible := 0
|
||||
@@ -121,8 +130,10 @@ func FetchAllPages[T any, R any](
|
||||
if config.EarlyTermination && totalEligible > 0 && oldInPage == totalEligible {
|
||||
consecutiveOldPages++
|
||||
if consecutiveOldPages >= config.EarlyTerminationThreshold {
|
||||
c.progress(fmt.Sprintf(" Reached %s older than date range, stopping early (page %d)",
|
||||
config.ResourceName, page))
|
||||
if !config.Quiet {
|
||||
c.progress(fmt.Sprintf(" Reached %s older than date range, stopping early (page %d)",
|
||||
config.ResourceName, page))
|
||||
}
|
||||
break
|
||||
}
|
||||
} else {
|
||||
@@ -260,8 +271,15 @@ func FetchAllPagesWithEnrichment[T any, R any](
|
||||
return nil, fmt.Errorf("failed to fetch %s: %w", config.ResourceName, err)
|
||||
}
|
||||
|
||||
c.progress(fmt.Sprintf(" Fetching %s page %d (%d %s so far)...",
|
||||
config.ResourceName, page, len(allResults), config.ResourceName))
|
||||
// Safety check for nil response
|
||||
if resp == nil {
|
||||
break
|
||||
}
|
||||
|
||||
if !config.Quiet {
|
||||
c.progress(fmt.Sprintf(" Fetching %s page %d (%d %s so far)...",
|
||||
config.ResourceName, page, len(allResults), config.ResourceName))
|
||||
}
|
||||
|
||||
itemsInPage := 0
|
||||
for i, item := range items {
|
||||
|
||||
@@ -0,0 +1,516 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/progress"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/lukaszraczylo/git-velocity/internal/domain/models"
|
||||
"github.com/shurcooL/githubv4"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// progressBar handles terminal progress display
|
||||
type progressBar struct {
|
||||
progress progress.Model
|
||||
label string
|
||||
total int
|
||||
current int
|
||||
out io.Writer
|
||||
}
|
||||
|
||||
func newProgressBar(label string, total int) *progressBar {
|
||||
p := progress.New(
|
||||
progress.WithDefaultGradient(),
|
||||
progress.WithWidth(40),
|
||||
)
|
||||
return &progressBar{
|
||||
progress: p,
|
||||
label: label,
|
||||
total: total,
|
||||
current: 0,
|
||||
out: os.Stderr,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *progressBar) update(fetched int) {
|
||||
p.current = fetched
|
||||
percent := float64(p.current) / float64(p.total)
|
||||
if percent > 1.0 {
|
||||
percent = 1.0
|
||||
}
|
||||
|
||||
labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
|
||||
countStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("241"))
|
||||
|
||||
fmt.Fprintf(p.out, "\r%s %s %s",
|
||||
labelStyle.Render(p.label),
|
||||
p.progress.ViewAs(percent),
|
||||
countStyle.Render(fmt.Sprintf("%d/%d", p.current, p.total)),
|
||||
)
|
||||
}
|
||||
|
||||
func (p *progressBar) done() {
|
||||
p.update(p.total)
|
||||
fmt.Fprintln(p.out)
|
||||
}
|
||||
|
||||
// GraphQLClient wraps the githubv4 client for GitHub API
|
||||
type GraphQLClient struct {
|
||||
client *githubv4.Client
|
||||
}
|
||||
|
||||
// NewGraphQLClient creates a new GraphQL client for GitHub
|
||||
func NewGraphQLClient(token string) *GraphQLClient {
|
||||
src := oauth2.StaticTokenSource(
|
||||
&oauth2.Token{AccessToken: token},
|
||||
)
|
||||
httpClient := oauth2.NewClient(context.Background(), src)
|
||||
client := githubv4.NewClient(httpClient)
|
||||
|
||||
return &GraphQLClient{
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
// PageInfo contains pagination info from GraphQL responses
|
||||
type PageInfo struct {
|
||||
HasNextPage bool
|
||||
EndCursor githubv4.String
|
||||
}
|
||||
|
||||
// PageResult represents a page of results from GraphQL
|
||||
type PageResult[T any] struct {
|
||||
TotalCount int
|
||||
PageInfo PageInfo
|
||||
Nodes []T
|
||||
}
|
||||
|
||||
// GQLFetchConfig configures the generic paginated fetcher for GraphQL
|
||||
type GQLFetchConfig[Q any, T any, R any] struct {
|
||||
Label string
|
||||
Query *Q
|
||||
GetPageResult func(q *Q) PageResult[T]
|
||||
// ProcessNode returns items, whether this node is "old" (outside date range),
|
||||
// and whether to hard stop immediately (past cutoff date)
|
||||
ProcessNode func(node T, repo string) (items []R, isOld bool, hardStop bool)
|
||||
// ConsecutiveOldPagesToStop controls early termination (default: 2)
|
||||
ConsecutiveOldPagesToStop int
|
||||
}
|
||||
|
||||
// fetchGQLPaginated is a generic paginated fetcher for GraphQL queries
|
||||
func fetchGQLPaginated[Q any, T any, R any](
|
||||
ctx context.Context,
|
||||
client *githubv4.Client,
|
||||
owner, repo string,
|
||||
config GQLFetchConfig[Q, T, R],
|
||||
) ([]R, error) {
|
||||
var allResults []R
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"owner": githubv4.String(owner),
|
||||
"repo": githubv4.String(repo),
|
||||
"cursor": (*githubv4.String)(nil),
|
||||
}
|
||||
|
||||
var pbar *progressBar
|
||||
fetched := 0
|
||||
repoFullName := fmt.Sprintf("%s/%s", owner, repo)
|
||||
consecutiveOldPages := 0
|
||||
pagesToStop := config.ConsecutiveOldPagesToStop
|
||||
if pagesToStop == 0 {
|
||||
pagesToStop = 2 // default
|
||||
}
|
||||
|
||||
for {
|
||||
if err := client.Query(ctx, config.Query, variables); err != nil {
|
||||
return nil, fmt.Errorf("graphql query failed: %w", err)
|
||||
}
|
||||
|
||||
page := config.GetPageResult(config.Query)
|
||||
|
||||
// Initialize progress bar on first query
|
||||
if pbar == nil && page.TotalCount > 0 {
|
||||
pbar = newProgressBar(config.Label, page.TotalCount)
|
||||
}
|
||||
|
||||
oldInPage := 0
|
||||
totalInPage := 0
|
||||
shouldHardStop := false
|
||||
for _, node := range page.Nodes {
|
||||
fetched++
|
||||
totalInPage++
|
||||
items, isOld, hardStop := config.ProcessNode(node, repoFullName)
|
||||
allResults = append(allResults, items...)
|
||||
if isOld {
|
||||
oldInPage++
|
||||
}
|
||||
if hardStop {
|
||||
shouldHardStop = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if pbar != nil {
|
||||
pbar.update(fetched)
|
||||
}
|
||||
|
||||
// Hard stop takes priority (past cutoff date)
|
||||
if shouldHardStop {
|
||||
if pbar != nil {
|
||||
pbar.done()
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// Track consecutive pages where all items are old
|
||||
if totalInPage > 0 && oldInPage == totalInPage {
|
||||
consecutiveOldPages++
|
||||
} else {
|
||||
consecutiveOldPages = 0
|
||||
}
|
||||
|
||||
// Stop if we've seen enough consecutive old pages or no more pages
|
||||
if consecutiveOldPages >= pagesToStop || !page.PageInfo.HasNextPage {
|
||||
if pbar != nil {
|
||||
pbar.done()
|
||||
}
|
||||
break
|
||||
}
|
||||
variables["cursor"] = githubv4.NewString(page.PageInfo.EndCursor)
|
||||
}
|
||||
|
||||
return allResults, nil
|
||||
}
|
||||
|
||||
// Query structs for PRs with reviews
|
||||
type gqlPRQuery struct {
|
||||
Repository struct {
|
||||
PullRequests struct {
|
||||
TotalCount int
|
||||
PageInfo PageInfo
|
||||
Nodes []gqlPRNode
|
||||
} `graphql:"pullRequests(first: 100, after: $cursor, states: [MERGED], orderBy: {field: UPDATED_AT, direction: DESC})"`
|
||||
} `graphql:"repository(owner: $owner, name: $repo)"`
|
||||
}
|
||||
|
||||
type gqlPRNode struct {
|
||||
Number int
|
||||
Title string
|
||||
State string
|
||||
Merged bool
|
||||
Additions int
|
||||
Deletions int
|
||||
ChangedFiles int
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
MergedAt *time.Time
|
||||
ClosedAt *time.Time
|
||||
BaseRefName string
|
||||
HeadRefName string
|
||||
URL string
|
||||
Commits struct{ TotalCount int }
|
||||
Author gqlActor
|
||||
Reviews struct {
|
||||
TotalCount int
|
||||
Nodes []gqlReviewNode
|
||||
PageInfo PageInfo
|
||||
} `graphql:"reviews(first: 100)"`
|
||||
}
|
||||
|
||||
type gqlActor struct {
|
||||
Login string
|
||||
AvatarURL string `graphql:"avatarUrl"`
|
||||
}
|
||||
|
||||
type gqlReviewNode struct {
|
||||
ID string `graphql:"id"`
|
||||
Author gqlActor
|
||||
State string
|
||||
SubmittedAt *time.Time
|
||||
Body string
|
||||
Comments struct{ TotalCount int } `graphql:"comments"`
|
||||
}
|
||||
|
||||
// Query struct for issues with comments
|
||||
type gqlIssueQuery struct {
|
||||
Repository struct {
|
||||
Issues struct {
|
||||
TotalCount int
|
||||
PageInfo PageInfo
|
||||
Nodes []gqlIssueNode
|
||||
} `graphql:"issues(first: 100, after: $cursor, orderBy: {field: CREATED_AT, direction: DESC})"`
|
||||
} `graphql:"repository(owner: $owner, name: $repo)"`
|
||||
}
|
||||
|
||||
type gqlIssueNode struct {
|
||||
Number int
|
||||
Title string
|
||||
State string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
ClosedAt *time.Time
|
||||
URL string
|
||||
Author gqlActor
|
||||
Labels struct {
|
||||
Nodes []struct{ Name string }
|
||||
} `graphql:"labels(first: 10)"`
|
||||
Comments struct {
|
||||
TotalCount int
|
||||
Nodes []gqlCommentNode
|
||||
PageInfo PageInfo
|
||||
} `graphql:"comments(first: 100)"`
|
||||
}
|
||||
|
||||
type gqlCommentNode struct {
|
||||
ID string `graphql:"id"`
|
||||
Author gqlActor
|
||||
Body string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// prWithReviews bundles a PR with its reviews for the generic fetcher
|
||||
type prWithReviews struct {
|
||||
PR models.PullRequest
|
||||
Reviews []models.Review
|
||||
}
|
||||
|
||||
// FetchPRsWithReviews fetches pull requests with their reviews using GraphQL
|
||||
func (g *GraphQLClient) FetchPRsWithReviews(ctx context.Context, owner, repo string, since, until *time.Time) ([]models.PullRequest, []models.Review, error) {
|
||||
var query gqlPRQuery
|
||||
|
||||
// Hard cutoff: 1 week before start date - stop fetching entirely past this point
|
||||
var hardCutoff *time.Time
|
||||
if since != nil {
|
||||
cutoff := since.AddDate(0, 0, -7)
|
||||
hardCutoff = &cutoff
|
||||
}
|
||||
|
||||
results, err := fetchGQLPaginated(ctx, g.client, owner, repo, GQLFetchConfig[gqlPRQuery, gqlPRNode, prWithReviews]{
|
||||
Label: " Fetching PRs:",
|
||||
Query: &query,
|
||||
ConsecutiveOldPagesToStop: 2,
|
||||
GetPageResult: func(q *gqlPRQuery) PageResult[gqlPRNode] {
|
||||
return PageResult[gqlPRNode]{
|
||||
TotalCount: q.Repository.PullRequests.TotalCount,
|
||||
PageInfo: q.Repository.PullRequests.PageInfo,
|
||||
Nodes: q.Repository.PullRequests.Nodes,
|
||||
}
|
||||
},
|
||||
ProcessNode: func(node gqlPRNode, repoName string) ([]prWithReviews, bool, bool) {
|
||||
// Skip if not merged - not counted as "old"
|
||||
if node.MergedAt == nil {
|
||||
return nil, false, false
|
||||
}
|
||||
|
||||
mergedAt := *node.MergedAt
|
||||
|
||||
// Hard cutoff check - stop entirely if past this date
|
||||
if hardCutoff != nil && mergedAt.Before(*hardCutoff) {
|
||||
return nil, true, true // Hard stop
|
||||
}
|
||||
|
||||
// Check date range - skip if outside range
|
||||
if until != nil && mergedAt.After(*until) {
|
||||
return nil, false, false // Too new, not "old"
|
||||
}
|
||||
if since != nil && mergedAt.Before(*since) {
|
||||
return nil, true, false // Too old - signal for early termination tracking
|
||||
}
|
||||
|
||||
// Convert PR
|
||||
pr := convertPRNode(node, repoName)
|
||||
|
||||
// Convert reviews
|
||||
var reviews []models.Review
|
||||
for _, r := range node.Reviews.Nodes {
|
||||
reviews = append(reviews, convertReviewNode(r, repoName, node.Number))
|
||||
}
|
||||
|
||||
return []prWithReviews{{PR: pr, Reviews: reviews}}, false, false
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Flatten results
|
||||
var prs []models.PullRequest
|
||||
var reviews []models.Review
|
||||
for _, r := range results {
|
||||
prs = append(prs, r.PR)
|
||||
reviews = append(reviews, r.Reviews...)
|
||||
}
|
||||
|
||||
return prs, reviews, nil
|
||||
}
|
||||
|
||||
// issueWithComments bundles an issue with its comments for the generic fetcher
|
||||
type issueWithComments struct {
|
||||
Issue models.Issue
|
||||
Comments []models.IssueComment
|
||||
}
|
||||
|
||||
// FetchIssuesWithComments fetches issues with their comments using GraphQL
|
||||
func (g *GraphQLClient) FetchIssuesWithComments(ctx context.Context, owner, repo string, since, until *time.Time) ([]models.Issue, []models.IssueComment, error) {
|
||||
var query gqlIssueQuery
|
||||
|
||||
// Hard cutoff: 1 week before start date - stop fetching entirely past this point
|
||||
var hardCutoff *time.Time
|
||||
if since != nil {
|
||||
cutoff := since.AddDate(0, 0, -7)
|
||||
hardCutoff = &cutoff
|
||||
}
|
||||
|
||||
results, err := fetchGQLPaginated(ctx, g.client, owner, repo, GQLFetchConfig[gqlIssueQuery, gqlIssueNode, issueWithComments]{
|
||||
Label: " Fetching issues:",
|
||||
Query: &query,
|
||||
ConsecutiveOldPagesToStop: 2,
|
||||
GetPageResult: func(q *gqlIssueQuery) PageResult[gqlIssueNode] {
|
||||
return PageResult[gqlIssueNode]{
|
||||
TotalCount: q.Repository.Issues.TotalCount,
|
||||
PageInfo: q.Repository.Issues.PageInfo,
|
||||
Nodes: q.Repository.Issues.Nodes,
|
||||
}
|
||||
},
|
||||
ProcessNode: func(node gqlIssueNode, repoName string) ([]issueWithComments, bool, bool) {
|
||||
// Hard cutoff check - stop entirely if past this date
|
||||
if hardCutoff != nil && node.CreatedAt.Before(*hardCutoff) {
|
||||
return nil, true, true // Hard stop
|
||||
}
|
||||
|
||||
// Check date range
|
||||
if until != nil && node.CreatedAt.After(*until) {
|
||||
return nil, false, false // Too new, not "old"
|
||||
}
|
||||
if since != nil && node.CreatedAt.Before(*since) {
|
||||
return nil, true, false // Too old - signal for early termination tracking
|
||||
}
|
||||
|
||||
// Convert issue
|
||||
issue := convertIssueNode(node, repoName)
|
||||
|
||||
// Convert comments within date range
|
||||
var comments []models.IssueComment
|
||||
for _, c := range node.Comments.Nodes {
|
||||
if until != nil && c.CreatedAt.After(*until) {
|
||||
continue
|
||||
}
|
||||
if since != nil && c.CreatedAt.Before(*since) {
|
||||
continue
|
||||
}
|
||||
comments = append(comments, convertCommentNode(c, repoName, node.Number))
|
||||
}
|
||||
|
||||
return []issueWithComments{{Issue: issue, Comments: comments}}, false, false
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Flatten results
|
||||
var issues []models.Issue
|
||||
var comments []models.IssueComment
|
||||
for _, r := range results {
|
||||
issues = append(issues, r.Issue)
|
||||
comments = append(comments, r.Comments...)
|
||||
}
|
||||
|
||||
return issues, comments, nil
|
||||
}
|
||||
|
||||
// Conversion helpers
|
||||
|
||||
func convertActor(a gqlActor) models.Author {
|
||||
return models.Author{
|
||||
Login: a.Login,
|
||||
AvatarURL: a.AvatarURL,
|
||||
}
|
||||
}
|
||||
|
||||
func convertPRNode(node gqlPRNode, repoName string) models.PullRequest {
|
||||
state := models.PRStateOpen
|
||||
if node.Merged {
|
||||
state = models.PRStateMerged
|
||||
} else if node.State == "CLOSED" {
|
||||
state = models.PRStateClosed
|
||||
}
|
||||
|
||||
return models.PullRequest{
|
||||
Number: node.Number,
|
||||
Title: node.Title,
|
||||
State: state,
|
||||
Author: convertActor(node.Author),
|
||||
Repository: repoName,
|
||||
BaseBranch: node.BaseRefName,
|
||||
HeadBranch: node.HeadRefName,
|
||||
CreatedAt: node.CreatedAt,
|
||||
UpdatedAt: node.UpdatedAt,
|
||||
MergedAt: node.MergedAt,
|
||||
ClosedAt: node.ClosedAt,
|
||||
Additions: node.Additions,
|
||||
Deletions: node.Deletions,
|
||||
FilesChanged: node.ChangedFiles,
|
||||
CommitCount: node.Commits.TotalCount,
|
||||
Comments: node.Reviews.TotalCount,
|
||||
URL: node.URL,
|
||||
}
|
||||
}
|
||||
|
||||
func convertReviewNode(node gqlReviewNode, repoName string, prNumber int) models.Review {
|
||||
var submittedAt time.Time
|
||||
if node.SubmittedAt != nil {
|
||||
submittedAt = *node.SubmittedAt
|
||||
}
|
||||
|
||||
return models.Review{
|
||||
PullRequest: prNumber,
|
||||
Repository: repoName,
|
||||
Author: convertActor(node.Author),
|
||||
State: models.ReviewState(node.State),
|
||||
SubmittedAt: submittedAt,
|
||||
Body: node.Body,
|
||||
CommentsCount: node.Comments.TotalCount,
|
||||
}
|
||||
}
|
||||
|
||||
func convertIssueNode(node gqlIssueNode, repoName string) models.Issue {
|
||||
state := models.IssueStateOpen
|
||||
if node.State == "CLOSED" {
|
||||
state = models.IssueStateClosed
|
||||
}
|
||||
|
||||
var labels []string
|
||||
for _, l := range node.Labels.Nodes {
|
||||
labels = append(labels, l.Name)
|
||||
}
|
||||
|
||||
return models.Issue{
|
||||
Number: node.Number,
|
||||
Title: node.Title,
|
||||
State: state,
|
||||
Author: convertActor(node.Author),
|
||||
Repository: repoName,
|
||||
CreatedAt: node.CreatedAt,
|
||||
UpdatedAt: node.UpdatedAt,
|
||||
ClosedAt: node.ClosedAt,
|
||||
Comments: node.Comments.TotalCount,
|
||||
Labels: labels,
|
||||
URL: node.URL,
|
||||
}
|
||||
}
|
||||
|
||||
func convertCommentNode(node gqlCommentNode, repoName string, issueNumber int) models.IssueComment {
|
||||
return models.IssueComment{
|
||||
Issue: issueNumber,
|
||||
Repository: repoName,
|
||||
Author: convertActor(node.Author),
|
||||
Body: node.Body,
|
||||
CreatedAt: node.CreatedAt,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user