cli/api/export_pr.go
Kynan Ware a78bedb772 Wrap relationship JSON output in {nodes, totalCount}, fetch full pages
The subIssues, blockedBy, and blocking JSON output is currently shaped
as a flat array, which silently truncates when there are more entries
than the GraphQL fragment fetches. That's misleading once a user
crosses the page size, so this switches each connection to a
{nodes, totalCount} object so consumers can see when there's more.

While confirming page sizes, the GitHub limits turn out to be:

- sub-issues: up to 100 per parent
  https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/adding-sub-issues
- blocked-by / blocking: up to 50 per relationship type
  https://github.blog/changelog/2025-08-21-dependencies-on-issues/

So subIssues moves to first:100 to fetch the full set; blockedBy and
blocking stay at first:50, which already covers their cap.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-06 16:05:07 -06:00

256 lines
7.1 KiB
Go

package api
import (
"reflect"
"strings"
)
func (issue *Issue) ExportData(fields []string) map[string]interface{} {
v := reflect.ValueOf(issue).Elem()
data := map[string]interface{}{}
for _, f := range fields {
switch f {
case "comments":
data[f] = issue.Comments.Nodes
case "assignees":
data[f] = issue.Assignees.Nodes
case "labels":
data[f] = issue.Labels.Nodes
case "projectCards":
data[f] = issue.ProjectCards.Nodes
case "projectItems":
items := make([]map[string]interface{}, 0, len(issue.ProjectItems.Nodes))
for _, n := range issue.ProjectItems.Nodes {
items = append(items, map[string]interface{}{
"status": n.Status,
"title": n.Project.Title,
})
}
data[f] = items
case "closedByPullRequestsReferences":
items := make([]map[string]interface{}, 0, len(issue.ClosedByPullRequestsReferences.Nodes))
for _, n := range issue.ClosedByPullRequestsReferences.Nodes {
items = append(items, map[string]interface{}{
"id": n.ID,
"number": n.Number,
"url": n.URL,
"repository": map[string]interface{}{
"id": n.Repository.ID,
"name": n.Repository.Name,
"owner": map[string]interface{}{
"id": n.Repository.Owner.ID,
"login": n.Repository.Owner.Login,
},
},
})
}
data[f] = items
case "issueType":
data[f] = issue.IssueType
case "parent":
if issue.Parent != nil {
data[f] = map[string]interface{}{
"id": issue.Parent.ID,
"number": issue.Parent.Number,
"title": issue.Parent.Title,
"url": issue.Parent.URL,
"state": issue.Parent.State,
}
} else {
data[f] = nil
}
case "subIssues":
items := make([]map[string]interface{}, 0, len(issue.SubIssues.Nodes))
for _, n := range issue.SubIssues.Nodes {
items = append(items, map[string]interface{}{
"id": n.ID,
"number": n.Number,
"title": n.Title,
"url": n.URL,
"state": n.State,
})
}
data[f] = map[string]interface{}{
"nodes": items,
"totalCount": issue.SubIssues.TotalCount,
}
case "subIssuesSummary":
data[f] = map[string]interface{}{
"total": issue.SubIssuesSummary.Total,
"completed": issue.SubIssuesSummary.Completed,
"percentCompleted": issue.SubIssuesSummary.PercentCompleted,
}
case "blockedBy":
items := make([]map[string]interface{}, 0, len(issue.BlockedBy.Nodes))
for _, n := range issue.BlockedBy.Nodes {
items = append(items, map[string]interface{}{
"id": n.ID,
"number": n.Number,
"title": n.Title,
"url": n.URL,
"state": n.State,
})
}
data[f] = map[string]interface{}{
"nodes": items,
"totalCount": issue.BlockedBy.TotalCount,
}
case "blocking":
items := make([]map[string]interface{}, 0, len(issue.Blocking.Nodes))
for _, n := range issue.Blocking.Nodes {
items = append(items, map[string]interface{}{
"id": n.ID,
"number": n.Number,
"title": n.Title,
"url": n.URL,
"state": n.State,
})
}
data[f] = map[string]interface{}{
"nodes": items,
"totalCount": issue.Blocking.TotalCount,
}
default:
sf := fieldByName(v, f)
data[f] = sf.Interface()
}
}
return data
}
func (pr *PullRequest) ExportData(fields []string) map[string]interface{} {
v := reflect.ValueOf(pr).Elem()
data := map[string]interface{}{}
for _, f := range fields {
switch f {
case "headRepository":
data[f] = pr.HeadRepository
case "statusCheckRollup":
if n := pr.StatusCheckRollup.Nodes; len(n) > 0 {
checks := make([]interface{}, 0, len(n[0].Commit.StatusCheckRollup.Contexts.Nodes))
for _, c := range n[0].Commit.StatusCheckRollup.Contexts.Nodes {
if c.TypeName == "CheckRun" {
checks = append(checks, map[string]interface{}{
"__typename": c.TypeName,
"name": c.Name,
"workflowName": c.CheckSuite.WorkflowRun.Workflow.Name,
"status": c.Status,
"conclusion": c.Conclusion,
"startedAt": c.StartedAt,
"completedAt": c.CompletedAt,
"detailsUrl": c.DetailsURL,
})
} else {
checks = append(checks, map[string]interface{}{
"__typename": c.TypeName,
"context": c.Context,
"state": c.State,
"targetUrl": c.TargetURL,
"startedAt": c.CreatedAt,
})
}
}
data[f] = checks
} else {
data[f] = nil
}
case "commits":
commits := make([]interface{}, 0, len(pr.Commits.Nodes))
for _, c := range pr.Commits.Nodes {
commit := c.Commit
authors := make([]interface{}, 0, len(commit.Authors.Nodes))
for _, author := range commit.Authors.Nodes {
authors = append(authors, map[string]interface{}{
"name": author.Name,
"email": author.Email,
"id": author.User.ID,
"login": author.User.Login,
})
}
commits = append(commits, map[string]interface{}{
"oid": commit.OID,
"messageHeadline": commit.MessageHeadline,
"messageBody": commit.MessageBody,
"committedDate": commit.CommittedDate,
"authoredDate": commit.AuthoredDate,
"authors": authors,
})
}
data[f] = commits
case "comments":
data[f] = pr.Comments.Nodes
case "assignees":
data[f] = pr.Assignees.Nodes
case "labels":
data[f] = pr.Labels.Nodes
case "projectCards":
data[f] = pr.ProjectCards.Nodes
case "projectItems":
items := make([]map[string]interface{}, 0, len(pr.ProjectItems.Nodes))
for _, n := range pr.ProjectItems.Nodes {
items = append(items, map[string]interface{}{
"status": n.Status,
"title": n.Project.Title,
})
}
data[f] = items
case "reviews":
data[f] = pr.Reviews.Nodes
case "latestReviews":
data[f] = pr.LatestReviews.Nodes
case "files":
data[f] = pr.Files.Nodes
case "reviewRequests":
requests := make([]interface{}, 0, len(pr.ReviewRequests.Nodes))
for _, req := range pr.ReviewRequests.Nodes {
r := req.RequestedReviewer
switch r.TypeName {
case "User":
requests = append(requests, map[string]string{
"__typename": r.TypeName,
"login": r.Login,
})
case "Team":
requests = append(requests, map[string]string{
"__typename": r.TypeName,
"name": r.Name,
"slug": r.LoginOrSlug(),
})
}
}
data[f] = &requests
case "closingIssuesReferences":
items := make([]map[string]interface{}, 0, len(pr.ClosingIssuesReferences.Nodes))
for _, n := range pr.ClosingIssuesReferences.Nodes {
items = append(items, map[string]interface{}{
"id": n.ID,
"number": n.Number,
"url": n.URL,
"repository": map[string]interface{}{
"id": n.Repository.ID,
"name": n.Repository.Name,
"owner": map[string]interface{}{
"id": n.Repository.Owner.ID,
"login": n.Repository.Owner.Login,
},
},
})
}
data[f] = items
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)
})
}