Reduce project item response size
Return compact project item content and field values from project item tools to avoid verbose issue and pull request payloads. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -2,7 +2,9 @@ package github
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-github/v87/github"
|
||||
@@ -207,6 +209,68 @@ type MinimalProject struct {
|
||||
OwnerType string `json:"owner_type,omitempty"`
|
||||
}
|
||||
|
||||
type MinimalProjectItem struct {
|
||||
ID int64 `json:"id"`
|
||||
NodeID string `json:"node_id,omitempty"`
|
||||
ContentType string `json:"content_type,omitempty"`
|
||||
Content *MinimalProjectItemContent `json:"content,omitempty"`
|
||||
Fields []MinimalProjectItemFieldValue `json:"fields,omitempty"`
|
||||
ArchivedAt string `json:"archived_at,omitempty"`
|
||||
CreatedAt string `json:"created_at,omitempty"`
|
||||
UpdatedAt string `json:"updated_at,omitempty"`
|
||||
Creator string `json:"creator,omitempty"`
|
||||
}
|
||||
|
||||
type MinimalProjectItemContent struct {
|
||||
ID int64 `json:"id,omitempty"`
|
||||
NodeID string `json:"node_id,omitempty"`
|
||||
Number int `json:"number,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
StateReason string `json:"state_reason,omitempty"`
|
||||
HTMLURL string `json:"html_url,omitempty"`
|
||||
Repository string `json:"repository,omitempty"`
|
||||
Author string `json:"author,omitempty"`
|
||||
Assignees []string `json:"assignees,omitempty"`
|
||||
Labels []string `json:"labels,omitempty"`
|
||||
Milestone string `json:"milestone,omitempty"`
|
||||
Comments int `json:"comments,omitempty"`
|
||||
Draft bool `json:"draft,omitempty"`
|
||||
Merged bool `json:"merged,omitempty"`
|
||||
CreatedAt string `json:"created_at,omitempty"`
|
||||
UpdatedAt string `json:"updated_at,omitempty"`
|
||||
ClosedAt string `json:"closed_at,omitempty"`
|
||||
MergedAt string `json:"merged_at,omitempty"`
|
||||
}
|
||||
|
||||
type MinimalProjectItemFieldValue struct {
|
||||
ID int64 `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
DataType string `json:"data_type,omitempty"`
|
||||
Value any `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
type minimalProjectOptionValue struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Color string `json:"color,omitempty"`
|
||||
}
|
||||
|
||||
type minimalProjectIterationValue struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
StartDate string `json:"start_date,omitempty"`
|
||||
Duration int `json:"duration,omitempty"`
|
||||
}
|
||||
|
||||
type minimalProjectPullRequestRef struct {
|
||||
Number int `json:"number,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
HTMLURL string `json:"html_url,omitempty"`
|
||||
Repository string `json:"repository,omitempty"`
|
||||
}
|
||||
|
||||
// MinimalReactions is the trimmed output type for reaction summaries, dropping the API URL.
|
||||
type MinimalReactions struct {
|
||||
TotalCount int `json:"total_count"`
|
||||
@@ -788,6 +852,534 @@ func convertToMinimalProject(fullProject *github.ProjectV2) *MinimalProject {
|
||||
}
|
||||
}
|
||||
|
||||
func convertToMinimalProjectItem(item *github.ProjectV2Item) MinimalProjectItem {
|
||||
if item == nil {
|
||||
return MinimalProjectItem{}
|
||||
}
|
||||
|
||||
contentType := ""
|
||||
if item.ContentType != nil {
|
||||
contentType = string(*item.ContentType)
|
||||
}
|
||||
|
||||
creator := ""
|
||||
if item.Creator != nil {
|
||||
creator = item.Creator.GetLogin()
|
||||
}
|
||||
|
||||
return MinimalProjectItem{
|
||||
ID: item.GetID(),
|
||||
NodeID: item.GetNodeID(),
|
||||
ContentType: contentType,
|
||||
Content: convertToMinimalProjectItemContent(item.GetContent()),
|
||||
Fields: convertToMinimalProjectItemFields(item.GetFields()),
|
||||
ArchivedAt: formatProjectTimestamp(item.ArchivedAt),
|
||||
CreatedAt: formatProjectTimestamp(item.CreatedAt),
|
||||
UpdatedAt: formatProjectTimestamp(item.UpdatedAt),
|
||||
Creator: creator,
|
||||
}
|
||||
}
|
||||
|
||||
func convertToMinimalProjectItemContent(content *github.ProjectV2ItemContent) *MinimalProjectItemContent {
|
||||
if content == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if issue := content.GetIssue(); issue != nil {
|
||||
return convertIssueToMinimalProjectItemContent(issue)
|
||||
}
|
||||
if pr := content.GetPullRequest(); pr != nil {
|
||||
return convertPullRequestToMinimalProjectItemContent(pr)
|
||||
}
|
||||
if draftIssue := content.GetDraftIssue(); draftIssue != nil {
|
||||
return convertDraftIssueToMinimalProjectItemContent(draftIssue)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertIssueToMinimalProjectItemContent(issue *github.Issue) *MinimalProjectItemContent {
|
||||
m := &MinimalProjectItemContent{
|
||||
ID: issue.GetID(),
|
||||
NodeID: issue.GetNodeID(),
|
||||
Number: issue.GetNumber(),
|
||||
Title: issue.GetTitle(),
|
||||
State: issue.GetState(),
|
||||
StateReason: issue.GetStateReason(),
|
||||
HTMLURL: issue.GetHTMLURL(),
|
||||
Repository: issueRepositoryFullName(issue),
|
||||
Comments: issue.GetComments(),
|
||||
Draft: issue.GetDraft(),
|
||||
CreatedAt: formatProjectTimestamp(issue.CreatedAt),
|
||||
UpdatedAt: formatProjectTimestamp(issue.UpdatedAt),
|
||||
ClosedAt: formatProjectTimestamp(issue.ClosedAt),
|
||||
}
|
||||
|
||||
if user := issue.GetUser(); user != nil {
|
||||
m.Author = user.GetLogin()
|
||||
}
|
||||
for _, assignee := range issue.Assignees {
|
||||
if assignee != nil {
|
||||
m.Assignees = append(m.Assignees, assignee.GetLogin())
|
||||
}
|
||||
}
|
||||
for _, label := range issue.Labels {
|
||||
if label != nil {
|
||||
m.Labels = append(m.Labels, label.GetName())
|
||||
}
|
||||
}
|
||||
if milestone := issue.GetMilestone(); milestone != nil {
|
||||
m.Milestone = milestone.GetTitle()
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func convertPullRequestToMinimalProjectItemContent(pr *github.PullRequest) *MinimalProjectItemContent {
|
||||
m := &MinimalProjectItemContent{
|
||||
ID: pr.GetID(),
|
||||
NodeID: pr.GetNodeID(),
|
||||
Number: pr.GetNumber(),
|
||||
Title: pr.GetTitle(),
|
||||
State: pr.GetState(),
|
||||
HTMLURL: pr.GetHTMLURL(),
|
||||
Repository: pullRequestRepositoryFullName(pr),
|
||||
Comments: pr.GetComments(),
|
||||
Draft: pr.GetDraft(),
|
||||
Merged: pr.GetMerged(),
|
||||
CreatedAt: formatProjectTimestamp(pr.CreatedAt),
|
||||
UpdatedAt: formatProjectTimestamp(pr.UpdatedAt),
|
||||
ClosedAt: formatProjectTimestamp(pr.ClosedAt),
|
||||
MergedAt: formatProjectTimestamp(pr.MergedAt),
|
||||
}
|
||||
|
||||
if user := pr.GetUser(); user != nil {
|
||||
m.Author = user.GetLogin()
|
||||
}
|
||||
for _, assignee := range pr.Assignees {
|
||||
if assignee != nil {
|
||||
m.Assignees = append(m.Assignees, assignee.GetLogin())
|
||||
}
|
||||
}
|
||||
for _, label := range pr.Labels {
|
||||
if label != nil {
|
||||
m.Labels = append(m.Labels, label.GetName())
|
||||
}
|
||||
}
|
||||
if milestone := pr.GetMilestone(); milestone != nil {
|
||||
m.Milestone = milestone.GetTitle()
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func convertDraftIssueToMinimalProjectItemContent(draftIssue *github.ProjectV2DraftIssue) *MinimalProjectItemContent {
|
||||
m := &MinimalProjectItemContent{
|
||||
ID: draftIssue.GetID(),
|
||||
NodeID: draftIssue.GetNodeID(),
|
||||
Title: draftIssue.GetTitle(),
|
||||
CreatedAt: formatProjectTimestamp(draftIssue.CreatedAt),
|
||||
UpdatedAt: formatProjectTimestamp(draftIssue.UpdatedAt),
|
||||
}
|
||||
|
||||
if user := draftIssue.GetUser(); user != nil {
|
||||
m.Author = user.GetLogin()
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func convertToMinimalProjectItemFields(fields []*github.ProjectV2ItemFieldValue) []MinimalProjectItemFieldValue {
|
||||
minimalFields := make([]MinimalProjectItemFieldValue, 0, len(fields))
|
||||
for _, field := range fields {
|
||||
if field == nil {
|
||||
continue
|
||||
}
|
||||
minimalFields = append(minimalFields, MinimalProjectItemFieldValue{
|
||||
ID: field.GetID(),
|
||||
Name: field.GetName(),
|
||||
DataType: field.GetDataType(),
|
||||
Value: minimalProjectFieldValue(field.GetValue()),
|
||||
})
|
||||
}
|
||||
return minimalFields
|
||||
}
|
||||
|
||||
func minimalProjectFieldValue(value any) any {
|
||||
switch v := value.(type) {
|
||||
case nil:
|
||||
return nil
|
||||
case string, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
|
||||
return v
|
||||
case []string:
|
||||
return v
|
||||
case map[string]any:
|
||||
return minimalProjectMapValue(v)
|
||||
case []any:
|
||||
return minimalProjectArrayValue(v)
|
||||
case *github.User:
|
||||
return v.GetLogin()
|
||||
case *github.Label:
|
||||
return v.GetName()
|
||||
case *github.Repository:
|
||||
return v.GetFullName()
|
||||
case *github.Milestone:
|
||||
return v.GetTitle()
|
||||
case *github.PullRequest:
|
||||
return minimalProjectPullRequestRefFromPullRequest(v)
|
||||
case *github.ProjectV2FieldOption:
|
||||
return minimalProjectOptionValue{
|
||||
ID: v.GetID(),
|
||||
Name: projectTextContentString(v.GetName()),
|
||||
Color: v.GetColor(),
|
||||
}
|
||||
case *github.ProjectV2FieldIteration:
|
||||
return minimalProjectIterationValue{
|
||||
ID: v.GetID(),
|
||||
Title: projectTextContentString(v.GetTitle()),
|
||||
StartDate: v.GetStartDate(),
|
||||
Duration: v.GetDuration(),
|
||||
}
|
||||
case []*github.User:
|
||||
logins := make([]string, 0, len(v))
|
||||
for _, user := range v {
|
||||
if user != nil {
|
||||
logins = append(logins, user.GetLogin())
|
||||
}
|
||||
}
|
||||
return logins
|
||||
case []*github.Label:
|
||||
names := make([]string, 0, len(v))
|
||||
for _, label := range v {
|
||||
if label != nil {
|
||||
names = append(names, label.GetName())
|
||||
}
|
||||
}
|
||||
return names
|
||||
case []*github.PullRequest:
|
||||
refs := make([]minimalProjectPullRequestRef, 0, len(v))
|
||||
for _, pr := range v {
|
||||
if pr != nil {
|
||||
refs = append(refs, minimalProjectPullRequestRefFromPullRequest(pr))
|
||||
}
|
||||
}
|
||||
return refs
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func minimalProjectMapValue(value map[string]any) any {
|
||||
if text := minimalProjectTextValue(value); text != "" {
|
||||
return text
|
||||
}
|
||||
if repo := fullNameFromMap(value); repo != "" {
|
||||
return repo
|
||||
}
|
||||
if login := stringFromMap(value, "login"); login != "" {
|
||||
return login
|
||||
}
|
||||
if isPullRequestMap(value) {
|
||||
return minimalProjectPullRequestRefFromMap(value)
|
||||
}
|
||||
if option, ok := minimalProjectOptionFromMap(value); ok {
|
||||
return option
|
||||
}
|
||||
if iteration, ok := minimalProjectIterationFromMap(value); ok {
|
||||
return iteration
|
||||
}
|
||||
if title := stringFromMap(value, "title"); title != "" {
|
||||
return title
|
||||
}
|
||||
if name := stringFromMap(value, "name"); name != "" {
|
||||
return name
|
||||
}
|
||||
|
||||
compact := make(map[string]any)
|
||||
for key, nestedValue := range value {
|
||||
minimalValue := minimalProjectFieldValue(nestedValue)
|
||||
if shouldKeepMinimalProjectValue(minimalValue) {
|
||||
compact[key] = minimalValue
|
||||
}
|
||||
}
|
||||
if len(compact) == 0 {
|
||||
return nil
|
||||
}
|
||||
return compact
|
||||
}
|
||||
|
||||
func minimalProjectArrayValue(values []any) any {
|
||||
if refs, ok := minimalProjectPullRequestRefsFromArray(values); ok {
|
||||
return refs
|
||||
}
|
||||
if strings, ok := minimalProjectStringsFromArray(values, "login"); ok {
|
||||
return strings
|
||||
}
|
||||
if strings, ok := minimalProjectStringsFromArray(values, "name"); ok {
|
||||
return strings
|
||||
}
|
||||
|
||||
compact := make([]any, 0, len(values))
|
||||
for _, value := range values {
|
||||
minimalValue := minimalProjectFieldValue(value)
|
||||
if shouldKeepMinimalProjectValue(minimalValue) {
|
||||
compact = append(compact, minimalValue)
|
||||
}
|
||||
}
|
||||
if len(compact) == 0 {
|
||||
return nil
|
||||
}
|
||||
return compact
|
||||
}
|
||||
|
||||
func minimalProjectTextValue(value map[string]any) string {
|
||||
if raw := stringFromMap(value, "raw"); raw != "" {
|
||||
return raw
|
||||
}
|
||||
if html := stringFromMap(value, "html"); html != "" {
|
||||
return html
|
||||
}
|
||||
return stringFromMap(value, "text")
|
||||
}
|
||||
|
||||
func minimalProjectOptionFromMap(value map[string]any) (minimalProjectOptionValue, bool) {
|
||||
name := stringFromMap(value, "name")
|
||||
color := stringFromMap(value, "color")
|
||||
if name == "" && color == "" {
|
||||
return minimalProjectOptionValue{}, false
|
||||
}
|
||||
return minimalProjectOptionValue{
|
||||
ID: stringFromMap(value, "id"),
|
||||
Name: name,
|
||||
Color: color,
|
||||
}, true
|
||||
}
|
||||
|
||||
func minimalProjectIterationFromMap(value map[string]any) (minimalProjectIterationValue, bool) {
|
||||
startDate := stringFromMap(value, "start_date")
|
||||
duration := intFromAny(value["duration"])
|
||||
if startDate == "" && duration == 0 {
|
||||
return minimalProjectIterationValue{}, false
|
||||
}
|
||||
return minimalProjectIterationValue{
|
||||
ID: stringFromMap(value, "id"),
|
||||
Title: stringFromMap(value, "title"),
|
||||
StartDate: startDate,
|
||||
Duration: duration,
|
||||
}, true
|
||||
}
|
||||
|
||||
func minimalProjectPullRequestRefsFromArray(values []any) ([]minimalProjectPullRequestRef, bool) {
|
||||
refs := make([]minimalProjectPullRequestRef, 0, len(values))
|
||||
for _, value := range values {
|
||||
switch pr := value.(type) {
|
||||
case map[string]any:
|
||||
if !isPullRequestMap(pr) {
|
||||
return nil, false
|
||||
}
|
||||
refs = append(refs, minimalProjectPullRequestRefFromMap(pr))
|
||||
case *github.PullRequest:
|
||||
if pr == nil {
|
||||
continue
|
||||
}
|
||||
refs = append(refs, minimalProjectPullRequestRefFromPullRequest(pr))
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
return refs, len(refs) > 0
|
||||
}
|
||||
|
||||
func minimalProjectStringsFromArray(values []any, key string) ([]string, bool) {
|
||||
strings := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
switch v := value.(type) {
|
||||
case map[string]any:
|
||||
stringValue := stringFromMap(v, key)
|
||||
if stringValue == "" {
|
||||
return nil, false
|
||||
}
|
||||
strings = append(strings, stringValue)
|
||||
case *github.User:
|
||||
if key != "login" || v == nil {
|
||||
return nil, false
|
||||
}
|
||||
strings = append(strings, v.GetLogin())
|
||||
case *github.Label:
|
||||
if key != "name" || v == nil {
|
||||
return nil, false
|
||||
}
|
||||
strings = append(strings, v.GetName())
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
return strings, len(strings) > 0
|
||||
}
|
||||
|
||||
func minimalProjectPullRequestRefFromPullRequest(pr *github.PullRequest) minimalProjectPullRequestRef {
|
||||
if pr == nil {
|
||||
return minimalProjectPullRequestRef{}
|
||||
}
|
||||
return minimalProjectPullRequestRef{
|
||||
Number: pr.GetNumber(),
|
||||
Title: pr.GetTitle(),
|
||||
State: pr.GetState(),
|
||||
HTMLURL: pr.GetHTMLURL(),
|
||||
Repository: pullRequestRepositoryFullName(pr),
|
||||
}
|
||||
}
|
||||
|
||||
func minimalProjectPullRequestRefFromMap(value map[string]any) minimalProjectPullRequestRef {
|
||||
htmlURL := stringFromMap(value, "html_url")
|
||||
repository := fullNameFromMapValue(value["repository"])
|
||||
if repository == "" {
|
||||
repository = branchRepositoryFullNameFromMap(value, "base")
|
||||
}
|
||||
if repository == "" {
|
||||
repository = branchRepositoryFullNameFromMap(value, "head")
|
||||
}
|
||||
if repository == "" {
|
||||
repository = repositoryFromHTMLURL(htmlURL)
|
||||
}
|
||||
|
||||
return minimalProjectPullRequestRef{
|
||||
Number: intFromAny(value["number"]),
|
||||
Title: stringFromMap(value, "title"),
|
||||
State: stringFromMap(value, "state"),
|
||||
HTMLURL: htmlURL,
|
||||
Repository: repository,
|
||||
}
|
||||
}
|
||||
|
||||
func isPullRequestMap(value map[string]any) bool {
|
||||
return intFromAny(value["number"]) != 0 && (stringFromMap(value, "html_url") != "" || stringFromMap(value, "state") != "")
|
||||
}
|
||||
|
||||
func branchRepositoryFullNameFromMap(value map[string]any, branchKey string) string {
|
||||
branch, ok := value[branchKey].(map[string]any)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return fullNameFromMapValue(branch["repo"])
|
||||
}
|
||||
|
||||
func shouldKeepMinimalProjectValue(value any) bool {
|
||||
switch v := value.(type) {
|
||||
case nil:
|
||||
return false
|
||||
case string:
|
||||
return v != ""
|
||||
case []any:
|
||||
return len(v) > 0
|
||||
case []string:
|
||||
return len(v) > 0
|
||||
case []minimalProjectPullRequestRef:
|
||||
return len(v) > 0
|
||||
case map[string]any:
|
||||
return len(v) > 0
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func issueRepositoryFullName(issue *github.Issue) string {
|
||||
if repo := issue.GetRepository(); repo != nil {
|
||||
return repo.GetFullName()
|
||||
}
|
||||
return repositoryFromHTMLURL(issue.GetHTMLURL())
|
||||
}
|
||||
|
||||
func pullRequestRepositoryFullName(pr *github.PullRequest) string {
|
||||
if base := pr.GetBase(); base != nil {
|
||||
if repo := base.GetRepo(); repo != nil && repo.GetFullName() != "" {
|
||||
return repo.GetFullName()
|
||||
}
|
||||
}
|
||||
if head := pr.GetHead(); head != nil {
|
||||
if repo := head.GetRepo(); repo != nil && repo.GetFullName() != "" {
|
||||
return repo.GetFullName()
|
||||
}
|
||||
}
|
||||
return repositoryFromHTMLURL(pr.GetHTMLURL())
|
||||
}
|
||||
|
||||
func fullNameFromMapValue(value any) string {
|
||||
repo, ok := value.(map[string]any)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return fullNameFromMap(repo)
|
||||
}
|
||||
|
||||
func fullNameFromMap(value map[string]any) string {
|
||||
return stringFromMap(value, "full_name")
|
||||
}
|
||||
|
||||
func repositoryFromHTMLURL(rawURL string) string {
|
||||
if rawURL == "" {
|
||||
return ""
|
||||
}
|
||||
parsedURL, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
parts := strings.Split(strings.Trim(parsedURL.Path, "/"), "/")
|
||||
if len(parts) < 2 || parts[0] == "" || parts[1] == "" {
|
||||
return ""
|
||||
}
|
||||
return parts[0] + "/" + parts[1]
|
||||
}
|
||||
|
||||
func projectTextContentString(content *github.ProjectV2TextContent) string {
|
||||
if content == nil {
|
||||
return ""
|
||||
}
|
||||
if raw := content.GetRaw(); raw != "" {
|
||||
return raw
|
||||
}
|
||||
return content.GetHTML()
|
||||
}
|
||||
|
||||
func formatProjectTimestamp(timestamp *github.Timestamp) string {
|
||||
if timestamp == nil || timestamp.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return timestamp.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
func stringFromMap(value map[string]any, key string) string {
|
||||
return stringFromAny(value[key])
|
||||
}
|
||||
|
||||
func stringFromAny(value any) string {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
return v
|
||||
case fmt.Stringer:
|
||||
return v.String()
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func intFromAny(value any) int {
|
||||
switch v := value.(type) {
|
||||
case int:
|
||||
return v
|
||||
case float64:
|
||||
return int(v)
|
||||
case string:
|
||||
i, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return i
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func convertToMinimalUser(user *github.User) *MinimalUser {
|
||||
if user == nil {
|
||||
return nil
|
||||
|
||||
@@ -820,8 +820,13 @@ func listProjectItems(ctx context.Context, client *github.Client, args map[strin
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
minimalItems := make([]MinimalProjectItem, 0, len(projectItems))
|
||||
for _, item := range projectItems {
|
||||
minimalItems = append(minimalItems, convertToMinimalProjectItem(item))
|
||||
}
|
||||
|
||||
response := map[string]any{
|
||||
"items": projectItems,
|
||||
"items": minimalItems,
|
||||
"pageInfo": buildPageInfo(resp),
|
||||
}
|
||||
|
||||
@@ -939,7 +944,7 @@ func getProjectItem(ctx context.Context, client *github.Client, owner, ownerType
|
||||
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get project item", resp, body), nil, nil
|
||||
}
|
||||
|
||||
r, err := json.Marshal(projectItem)
|
||||
r, err := json.Marshal(convertToMinimalProjectItem(projectItem))
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
|
||||
}
|
||||
@@ -978,7 +983,7 @@ func updateProjectItem(ctx context.Context, client *github.Client, owner, ownerT
|
||||
}
|
||||
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, ProjectUpdateFailedError, resp, body), nil, nil
|
||||
}
|
||||
r, err := json.Marshal(updatedItem)
|
||||
r, err := json.Marshal(convertToMinimalProjectItem(updatedItem))
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
|
||||
}
|
||||
|
||||
+252
-5
@@ -185,10 +185,152 @@ func Test_ProjectsList_ListProjectFields(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func verbosePullRequestProjectItemFixture() map[string]any {
|
||||
return map[string]any{
|
||||
"id": 1001,
|
||||
"node_id": "PVTI_1",
|
||||
"content_type": "PullRequest",
|
||||
"item_url": "https://api.github.com/projectsV2/items/1001",
|
||||
"project_url": "https://api.github.com/orgs/octo-org/projectsV2/1",
|
||||
"creator": map[string]any{
|
||||
"login": "creator",
|
||||
"id": 999,
|
||||
"followers_url": "https://api.github.com/users/creator/followers",
|
||||
},
|
||||
"content": map[string]any{
|
||||
"id": 2002,
|
||||
"node_id": "PR_1",
|
||||
"number": 42,
|
||||
"title": "Reduce project item output",
|
||||
"body": "Long pull request body that should not be returned from project item tools.",
|
||||
"state": "closed",
|
||||
"html_url": "https://github.com/cli/cli/pull/42",
|
||||
"url": "https://api.github.com/repos/cli/cli/pulls/42",
|
||||
"diff_url": "https://github.com/cli/cli/pull/42.diff",
|
||||
"patch_url": "https://github.com/cli/cli/pull/42.patch",
|
||||
"draft": false,
|
||||
"merged": true,
|
||||
"created_at": "2026-05-07T18:41:21Z",
|
||||
"updated_at": "2026-05-07T21:21:57Z",
|
||||
"closed_at": "2026-05-07T21:21:55Z",
|
||||
"merged_at": "2026-05-07T21:21:55Z",
|
||||
"user": map[string]any{
|
||||
"login": "octocat",
|
||||
"id": 123,
|
||||
"followers_url": "https://api.github.com/users/octocat/followers",
|
||||
},
|
||||
"assignees": []map[string]any{
|
||||
{
|
||||
"login": "hubot",
|
||||
"events_url": "https://api.github.com/users/hubot/events{/privacy}",
|
||||
},
|
||||
},
|
||||
"labels": []map[string]any{
|
||||
{
|
||||
"name": "bug",
|
||||
"url": "https://api.github.com/repos/cli/cli/labels/bug",
|
||||
},
|
||||
},
|
||||
"milestone": map[string]any{
|
||||
"title": "v1.0",
|
||||
"description": "Verbose milestone description",
|
||||
},
|
||||
"head": map[string]any{
|
||||
"ref": "feature",
|
||||
"repo": map[string]any{
|
||||
"full_name": "fork/cli",
|
||||
"archive_url": "https://api.github.com/repos/fork/cli/{archive_format}{/ref}",
|
||||
},
|
||||
},
|
||||
"base": map[string]any{
|
||||
"ref": "trunk",
|
||||
"repo": map[string]any{
|
||||
"full_name": "cli/cli",
|
||||
"archive_url": "https://api.github.com/repos/cli/cli/{archive_format}{/ref}",
|
||||
},
|
||||
},
|
||||
"_links": map[string]any{
|
||||
"self": map[string]any{
|
||||
"href": "https://api.github.com/repos/cli/cli/pulls/42",
|
||||
},
|
||||
},
|
||||
"statuses_url": "https://api.github.com/repos/cli/cli/statuses/abc123",
|
||||
},
|
||||
"fields": []map[string]any{
|
||||
{
|
||||
"id": 301,
|
||||
"name": "Status",
|
||||
"data_type": "single_select",
|
||||
"value": map[string]any{
|
||||
"id": "opt1",
|
||||
"name": "Done",
|
||||
"color": "GREEN",
|
||||
"description": "Verbose option description",
|
||||
},
|
||||
},
|
||||
},
|
||||
"created_at": "2026-05-28T07:39:37Z",
|
||||
"updated_at": "2026-05-28T07:40:15Z",
|
||||
}
|
||||
}
|
||||
|
||||
func assertMinimalPullRequestProjectItem(t *testing.T, rawJSON string, item map[string]any) {
|
||||
t.Helper()
|
||||
|
||||
assert.Equal(t, float64(1001), item["id"])
|
||||
assert.Equal(t, "PVTI_1", item["node_id"])
|
||||
assert.Equal(t, "PullRequest", item["content_type"])
|
||||
assert.Equal(t, "creator", item["creator"])
|
||||
assert.Equal(t, "2026-05-28T07:39:37Z", item["created_at"])
|
||||
assert.Equal(t, "2026-05-28T07:40:15Z", item["updated_at"])
|
||||
|
||||
content, ok := item["content"].(map[string]any)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, float64(42), content["number"])
|
||||
assert.Equal(t, "Reduce project item output", content["title"])
|
||||
assert.Equal(t, "closed", content["state"])
|
||||
assert.Equal(t, "https://github.com/cli/cli/pull/42", content["html_url"])
|
||||
assert.Equal(t, "cli/cli", content["repository"])
|
||||
assert.Equal(t, "octocat", content["author"])
|
||||
assert.Equal(t, true, content["merged"])
|
||||
assert.Equal(t, "2026-05-07T18:41:21Z", content["created_at"])
|
||||
assert.Equal(t, "2026-05-07T21:21:57Z", content["updated_at"])
|
||||
assert.Equal(t, "2026-05-07T21:21:55Z", content["closed_at"])
|
||||
assert.Equal(t, "2026-05-07T21:21:55Z", content["merged_at"])
|
||||
assert.Equal(t, []any{"hubot"}, content["assignees"])
|
||||
assert.Equal(t, []any{"bug"}, content["labels"])
|
||||
assert.Equal(t, "v1.0", content["milestone"])
|
||||
|
||||
fields, ok := item["fields"].([]any)
|
||||
require.True(t, ok)
|
||||
require.Len(t, fields, 1)
|
||||
field, ok := fields[0].(map[string]any)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, float64(301), field["id"])
|
||||
assert.Equal(t, "Status", field["name"])
|
||||
assert.Equal(t, "single_select", field["data_type"])
|
||||
value, ok := field["value"].(map[string]any)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "opt1", value["id"])
|
||||
assert.Equal(t, "Done", value["name"])
|
||||
assert.Equal(t, "GREEN", value["color"])
|
||||
|
||||
assert.NotContains(t, rawJSON, `"body"`)
|
||||
assert.NotContains(t, rawJSON, `"archive_url"`)
|
||||
assert.NotContains(t, rawJSON, `"followers_url"`)
|
||||
assert.NotContains(t, rawJSON, `"events_url"`)
|
||||
assert.NotContains(t, rawJSON, `"_links"`)
|
||||
assert.NotContains(t, rawJSON, `"head"`)
|
||||
assert.NotContains(t, rawJSON, `"base"`)
|
||||
assert.NotContains(t, rawJSON, `"url":`)
|
||||
assert.NotContains(t, rawJSON, `"statuses_url"`)
|
||||
assert.NotContains(t, rawJSON, `"diff_url"`)
|
||||
}
|
||||
|
||||
func Test_ProjectsList_ListProjectItems(t *testing.T) {
|
||||
toolDef := ProjectsList(translations.NullTranslationHelper)
|
||||
|
||||
items := []map[string]any{{"id": 1001, "archived_at": nil, "content": map[string]any{"title": "Issue 1"}}}
|
||||
items := []map[string]any{verbosePullRequestProjectItemFixture()}
|
||||
|
||||
t.Run("success organization", func(t *testing.T) {
|
||||
mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
|
||||
@@ -218,6 +360,9 @@ func Test_ProjectsList_ListProjectItems(t *testing.T) {
|
||||
itemsList, ok := response["items"].([]any)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, 1, len(itemsList))
|
||||
item, ok := itemsList[0].(map[string]any)
|
||||
require.True(t, ok)
|
||||
assertMinimalPullRequestProjectItem(t, textContent.Text, item)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -352,7 +497,7 @@ func Test_ProjectsGet_GetProjectField(t *testing.T) {
|
||||
func Test_ProjectsGet_GetProjectItem(t *testing.T) {
|
||||
toolDef := ProjectsGet(translations.NullTranslationHelper)
|
||||
|
||||
item := map[string]any{"id": 1001, "archived_at": nil, "content": map[string]any{"title": "Issue 1"}}
|
||||
item := verbosePullRequestProjectItemFixture()
|
||||
|
||||
t.Run("success organization", func(t *testing.T) {
|
||||
mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
|
||||
@@ -380,7 +525,7 @@ func Test_ProjectsGet_GetProjectItem(t *testing.T) {
|
||||
var response map[string]any
|
||||
err = json.Unmarshal([]byte(textContent.Text), &response)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, response["id"])
|
||||
assertMinimalPullRequestProjectItem(t, textContent.Text, response)
|
||||
})
|
||||
|
||||
t.Run("missing item_id", func(t *testing.T) {
|
||||
@@ -703,7 +848,7 @@ func Test_ProjectsWrite_AddProjectItem(t *testing.T) {
|
||||
func Test_ProjectsWrite_UpdateProjectItem(t *testing.T) {
|
||||
toolDef := ProjectsWrite(translations.NullTranslationHelper)
|
||||
|
||||
updatedItem := map[string]any{"id": 1001, "archived_at": nil}
|
||||
updatedItem := verbosePullRequestProjectItemFixture()
|
||||
|
||||
t.Run("success organization", func(t *testing.T) {
|
||||
mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
|
||||
@@ -735,7 +880,7 @@ func Test_ProjectsWrite_UpdateProjectItem(t *testing.T) {
|
||||
var response map[string]any
|
||||
err = json.Unmarshal([]byte(textContent.Text), &response)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, response["id"])
|
||||
assertMinimalPullRequestProjectItem(t, textContent.Text, response)
|
||||
})
|
||||
|
||||
t.Run("missing updated_field", func(t *testing.T) {
|
||||
@@ -814,6 +959,108 @@ func Test_ProjectsWrite_DeleteProjectItem(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestMinimalProjectFieldValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value any
|
||||
want any
|
||||
}{
|
||||
{
|
||||
name: "select option",
|
||||
value: map[string]any{
|
||||
"id": "opt1",
|
||||
"name": "Done",
|
||||
"color": "GREEN",
|
||||
"description": "verbose",
|
||||
},
|
||||
want: minimalProjectOptionValue{
|
||||
ID: "opt1",
|
||||
Name: "Done",
|
||||
Color: "GREEN",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "iteration",
|
||||
value: map[string]any{
|
||||
"id": "iter1",
|
||||
"title": "Sprint 1",
|
||||
"start_date": "2026-05-01",
|
||||
"duration": float64(14),
|
||||
},
|
||||
want: minimalProjectIterationValue{
|
||||
ID: "iter1",
|
||||
Title: "Sprint 1",
|
||||
StartDate: "2026-05-01",
|
||||
Duration: 14,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "assignees",
|
||||
value: []any{
|
||||
map[string]any{"login": "octocat", "followers_url": "https://api.github.com/users/octocat/followers"},
|
||||
map[string]any{"login": "hubot", "followers_url": "https://api.github.com/users/hubot/followers"},
|
||||
},
|
||||
want: []string{"octocat", "hubot"},
|
||||
},
|
||||
{
|
||||
name: "labels",
|
||||
value: []any{
|
||||
map[string]any{"name": "bug", "url": "https://api.github.com/repos/cli/cli/labels/bug"},
|
||||
map[string]any{"name": "help wanted", "url": "https://api.github.com/repos/cli/cli/labels/help%20wanted"},
|
||||
},
|
||||
want: []string{"bug", "help wanted"},
|
||||
},
|
||||
{
|
||||
name: "repository",
|
||||
value: map[string]any{
|
||||
"full_name": "cli/cli",
|
||||
"archive_url": "https://api.github.com/repos/cli/cli/{archive_format}{/ref}",
|
||||
},
|
||||
want: "cli/cli",
|
||||
},
|
||||
{
|
||||
name: "linked pull requests",
|
||||
value: []any{
|
||||
map[string]any{
|
||||
"number": float64(42),
|
||||
"title": "Reduce output",
|
||||
"state": "open",
|
||||
"html_url": "https://github.com/cli/cli/pull/42",
|
||||
"base": map[string]any{
|
||||
"repo": map[string]any{
|
||||
"full_name": "cli/cli",
|
||||
"archive_url": "https://api.github.com/repos/cli/cli/{archive_format}{/ref}",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []minimalProjectPullRequestRef{
|
||||
{
|
||||
Number: 42,
|
||||
Title: "Reduce output",
|
||||
State: "open",
|
||||
HTMLURL: "https://github.com/cli/cli/pull/42",
|
||||
Repository: "cli/cli",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "raw text content",
|
||||
value: map[string]any{
|
||||
"raw": "plain text",
|
||||
"html": "<p>plain text</p>",
|
||||
},
|
||||
want: "plain text",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert.Equal(t, tc.want, minimalProjectFieldValue(tc.value))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ProjectsList_ListProjectStatusUpdates(t *testing.T) {
|
||||
toolDef := ProjectsList(translations.NullTranslationHelper)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user