diff --git a/github/event_types.go b/github/event_types.go index eb1fd57a9a3..2a085d47b60 100644 --- a/github/event_types.go +++ b/github/event_types.go @@ -1148,44 +1148,6 @@ type FieldValue struct { To json.RawMessage `json:"to,omitempty"` } -// ProjectV2ItemFieldValue represents a field value of a project item. -type ProjectV2ItemFieldValue struct { - ID *int64 `json:"id,omitempty"` - Name string `json:"name,omitempty"` - DataType string `json:"data_type,omitempty"` - // Value set for the field. The type depends on the field type: - // - text: string - // - number: float64 - // - date: string (ISO 8601 date format, e.g. "2023-06-23") or null - // - single_select: object with "id", "name", "color", "description" fields or null - // - iteration: object with "id", "title", "start_date", "duration" fields or null - // - title: object with "text" field (read-only, reflects the item's title) or null - // - assignees: array of user objects with "login", "id", etc. or null - // - labels: array of label objects with "id", "name", "color", etc. or null - // - linked_pull_requests: array of pull request objects or null - // - milestone: milestone object with "id", "title", "description", etc. or null - // - repository: repository object with "id", "name", "full_name", etc. or null - // - reviewers: array of user objects or null - // - status: object with "id", "name", "color", "description" fields (same structure as single_select) or null - Value any `json:"value,omitempty"` -} - -// ProjectV2Item represents an item belonging to a project. -type ProjectV2Item struct { - ID *int64 `json:"id,omitempty"` - NodeID *string `json:"node_id,omitempty"` - ProjectNodeID *string `json:"project_node_id,omitempty"` - ContentNodeID *string `json:"content_node_id,omitempty"` - ProjectURL *string `json:"project_url,omitempty"` - ContentType *string `json:"content_type,omitempty"` - Creator *User `json:"creator,omitempty"` - CreatedAt *Timestamp `json:"created_at,omitempty"` - UpdatedAt *Timestamp `json:"updated_at,omitempty"` - ArchivedAt *Timestamp `json:"archived_at,omitempty"` - ItemURL *string `json:"item_url,omitempty"` - Fields []*ProjectV2ItemFieldValue `json:"fields,omitempty"` -} - // PublicEvent is triggered when a private repository is open sourced. // According to GitHub: "Without a doubt: the best GitHub event." // The Webhook event name is "public". diff --git a/github/event_types_test.go b/github/event_types_test.go index 15881ab6cec..955de1b09e0 100644 --- a/github/event_types_test.go +++ b/github/event_types_test.go @@ -15866,7 +15866,7 @@ func TestProjectV2ItemEvent_Marshal(t *testing.T) { NodeID: Ptr("nid"), ProjectNodeID: Ptr("pnid"), ContentNodeID: Ptr("cnid"), - ContentType: Ptr("ct"), + ContentType: Ptr(ProjectV2ItemContentType("ct")), Creator: &User{ Login: Ptr("l"), ID: Ptr(int64(1)), diff --git a/github/github-accessors.go b/github/github-accessors.go index 43d0e2e3d5c..b907c2af56a 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -358,6 +358,22 @@ func (a *ActorLocation) GetCountryCode() string { return *a.CountryCode } +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (a *AddProjectItemOptions) GetID() int64 { + if a == nil || a.ID == nil { + return 0 + } + return *a.ID +} + +// GetType returns the Type field. +func (a *AddProjectItemOptions) GetType() *ProjectV2ItemContentType { + if a == nil { + return nil + } + return a.Type +} + // GetMessage returns the Message field if it's non-nil, zero value otherwise. func (a *AddResourcesToCostCenterResponse) GetMessage() string { if a == nil || a.Message == nil { @@ -19526,6 +19542,22 @@ func (p *ProjectV2) GetID() int64 { return *p.ID } +// GetIsTemplate returns the IsTemplate field if it's non-nil, zero value otherwise. +func (p *ProjectV2) GetIsTemplate() bool { + if p == nil || p.IsTemplate == nil { + return false + } + return *p.IsTemplate +} + +// GetLatestStatusUpdate returns the LatestStatusUpdate field. +func (p *ProjectV2) GetLatestStatusUpdate() *ProjectV2StatusUpdate { + if p == nil { + return nil + } + return p.LatestStatusUpdate +} + // GetName returns the Name field if it's non-nil, zero value otherwise. func (p *ProjectV2) GetName() string { if p == nil || p.Name == nil { @@ -19630,6 +19662,62 @@ func (p *ProjectV2) GetURL() string { return *p.URL } +// GetBody returns the Body field if it's non-nil, zero value otherwise. +func (p *ProjectV2DraftIssue) GetBody() string { + if p == nil || p.Body == nil { + return "" + } + return *p.Body +} + +// GetCreatedAt returns the CreatedAt field if it's non-nil, zero value otherwise. +func (p *ProjectV2DraftIssue) GetCreatedAt() Timestamp { + if p == nil || p.CreatedAt == nil { + return Timestamp{} + } + return *p.CreatedAt +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (p *ProjectV2DraftIssue) GetID() int64 { + if p == nil || p.ID == nil { + return 0 + } + return *p.ID +} + +// GetNodeID returns the NodeID field if it's non-nil, zero value otherwise. +func (p *ProjectV2DraftIssue) GetNodeID() string { + if p == nil || p.NodeID == nil { + return "" + } + return *p.NodeID +} + +// GetTitle returns the Title field if it's non-nil, zero value otherwise. +func (p *ProjectV2DraftIssue) GetTitle() string { + if p == nil || p.Title == nil { + return "" + } + return *p.Title +} + +// GetUpdatedAt returns the UpdatedAt field if it's non-nil, zero value otherwise. +func (p *ProjectV2DraftIssue) GetUpdatedAt() Timestamp { + if p == nil || p.UpdatedAt == nil { + return Timestamp{} + } + return *p.UpdatedAt +} + +// GetUser returns the User field. +func (p *ProjectV2DraftIssue) GetUser() *User { + if p == nil { + return nil + } + return p.User +} + // GetAction returns the Action field if it's non-nil, zero value otherwise. func (p *ProjectV2Event) GetAction() string { if p == nil || p.Action == nil { @@ -19822,6 +19910,14 @@ func (p *ProjectV2Item) GetArchivedAt() Timestamp { return *p.ArchivedAt } +// GetContent returns the Content field. +func (p *ProjectV2Item) GetContent() *ProjectV2ItemContent { + if p == nil { + return nil + } + return p.Content +} + // GetContentNodeID returns the ContentNodeID field if it's non-nil, zero value otherwise. func (p *ProjectV2Item) GetContentNodeID() string { if p == nil || p.ContentNodeID == nil { @@ -19830,12 +19926,12 @@ func (p *ProjectV2Item) GetContentNodeID() string { return *p.ContentNodeID } -// GetContentType returns the ContentType field if it's non-nil, zero value otherwise. -func (p *ProjectV2Item) GetContentType() string { - if p == nil || p.ContentType == nil { - return "" +// GetContentType returns the ContentType field. +func (p *ProjectV2Item) GetContentType() *ProjectV2ItemContentType { + if p == nil { + return nil } - return *p.ContentType + return p.ContentType } // GetCreatedAt returns the CreatedAt field if it's non-nil, zero value otherwise. @@ -19918,6 +20014,30 @@ func (p *ProjectV2ItemChange) GetFieldValue() *FieldValue { return p.FieldValue } +// GetDraftIssue returns the DraftIssue field. +func (p *ProjectV2ItemContent) GetDraftIssue() *ProjectV2DraftIssue { + if p == nil { + return nil + } + return p.DraftIssue +} + +// GetIssue returns the Issue field. +func (p *ProjectV2ItemContent) GetIssue() *Issue { + if p == nil { + return nil + } + return p.Issue +} + +// GetPullRequest returns the PullRequest field. +func (p *ProjectV2ItemContent) GetPullRequest() *PullRequest { + if p == nil { + return nil + } + return p.PullRequest +} + // GetAction returns the Action field if it's non-nil, zero value otherwise. func (p *ProjectV2ItemEvent) GetAction() string { if p == nil || p.Action == nil { @@ -19966,6 +20086,14 @@ func (p *ProjectV2ItemEvent) GetSender() *User { return p.Sender } +// GetDataType returns the DataType field if it's non-nil, zero value otherwise. +func (p *ProjectV2ItemFieldValue) GetDataType() string { + if p == nil || p.DataType == nil { + return "" + } + return *p.DataType +} + // GetID returns the ID field if it's non-nil, zero value otherwise. func (p *ProjectV2ItemFieldValue) GetID() int64 { if p == nil || p.ID == nil { @@ -19974,6 +20102,94 @@ func (p *ProjectV2ItemFieldValue) GetID() int64 { return *p.ID } +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (p *ProjectV2ItemFieldValue) GetName() string { + if p == nil || p.Name == nil { + return "" + } + return *p.Name +} + +// GetBody returns the Body field if it's non-nil, zero value otherwise. +func (p *ProjectV2StatusUpdate) GetBody() string { + if p == nil || p.Body == nil { + return "" + } + return *p.Body +} + +// GetCreatedAt returns the CreatedAt field if it's non-nil, zero value otherwise. +func (p *ProjectV2StatusUpdate) GetCreatedAt() Timestamp { + if p == nil || p.CreatedAt == nil { + return Timestamp{} + } + return *p.CreatedAt +} + +// GetCreator returns the Creator field. +func (p *ProjectV2StatusUpdate) GetCreator() *User { + if p == nil { + return nil + } + return p.Creator +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (p *ProjectV2StatusUpdate) GetID() int64 { + if p == nil || p.ID == nil { + return 0 + } + return *p.ID +} + +// GetNodeID returns the NodeID field if it's non-nil, zero value otherwise. +func (p *ProjectV2StatusUpdate) GetNodeID() string { + if p == nil || p.NodeID == nil { + return "" + } + return *p.NodeID +} + +// GetProjectNodeID returns the ProjectNodeID field if it's non-nil, zero value otherwise. +func (p *ProjectV2StatusUpdate) GetProjectNodeID() string { + if p == nil || p.ProjectNodeID == nil { + return "" + } + return *p.ProjectNodeID +} + +// GetStartDate returns the StartDate field if it's non-nil, zero value otherwise. +func (p *ProjectV2StatusUpdate) GetStartDate() string { + if p == nil || p.StartDate == nil { + return "" + } + return *p.StartDate +} + +// GetStatus returns the Status field if it's non-nil, zero value otherwise. +func (p *ProjectV2StatusUpdate) GetStatus() string { + if p == nil || p.Status == nil { + return "" + } + return *p.Status +} + +// GetTargetDate returns the TargetDate field if it's non-nil, zero value otherwise. +func (p *ProjectV2StatusUpdate) GetTargetDate() string { + if p == nil || p.TargetDate == nil { + return "" + } + return *p.TargetDate +} + +// GetUpdatedAt returns the UpdatedAt field if it's non-nil, zero value otherwise. +func (p *ProjectV2StatusUpdate) GetUpdatedAt() Timestamp { + if p == nil || p.UpdatedAt == nil { + return Timestamp{} + } + return *p.UpdatedAt +} + // GetHTML returns the HTML field if it's non-nil, zero value otherwise. func (p *ProjectV2TextContent) GetHTML() string { if p == nil || p.HTML == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index 78249314e47..b12c405d2bf 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -479,6 +479,25 @@ func TestActorLocation_GetCountryCode(tt *testing.T) { a.GetCountryCode() } +func TestAddProjectItemOptions_GetID(tt *testing.T) { + tt.Parallel() + var zeroValue int64 + a := &AddProjectItemOptions{ID: &zeroValue} + a.GetID() + a = &AddProjectItemOptions{} + a.GetID() + a = nil + a.GetID() +} + +func TestAddProjectItemOptions_GetType(tt *testing.T) { + tt.Parallel() + a := &AddProjectItemOptions{} + a.GetType() + a = nil + a.GetType() +} + func TestAddResourcesToCostCenterResponse_GetMessage(tt *testing.T) { tt.Parallel() var zeroValue string @@ -25386,6 +25405,25 @@ func TestProjectV2_GetID(tt *testing.T) { p.GetID() } +func TestProjectV2_GetIsTemplate(tt *testing.T) { + tt.Parallel() + var zeroValue bool + p := &ProjectV2{IsTemplate: &zeroValue} + p.GetIsTemplate() + p = &ProjectV2{} + p.GetIsTemplate() + p = nil + p.GetIsTemplate() +} + +func TestProjectV2_GetLatestStatusUpdate(tt *testing.T) { + tt.Parallel() + p := &ProjectV2{} + p.GetLatestStatusUpdate() + p = nil + p.GetLatestStatusUpdate() +} + func TestProjectV2_GetName(tt *testing.T) { tt.Parallel() var zeroValue string @@ -25526,6 +25564,80 @@ func TestProjectV2_GetURL(tt *testing.T) { p.GetURL() } +func TestProjectV2DraftIssue_GetBody(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2DraftIssue{Body: &zeroValue} + p.GetBody() + p = &ProjectV2DraftIssue{} + p.GetBody() + p = nil + p.GetBody() +} + +func TestProjectV2DraftIssue_GetCreatedAt(tt *testing.T) { + tt.Parallel() + var zeroValue Timestamp + p := &ProjectV2DraftIssue{CreatedAt: &zeroValue} + p.GetCreatedAt() + p = &ProjectV2DraftIssue{} + p.GetCreatedAt() + p = nil + p.GetCreatedAt() +} + +func TestProjectV2DraftIssue_GetID(tt *testing.T) { + tt.Parallel() + var zeroValue int64 + p := &ProjectV2DraftIssue{ID: &zeroValue} + p.GetID() + p = &ProjectV2DraftIssue{} + p.GetID() + p = nil + p.GetID() +} + +func TestProjectV2DraftIssue_GetNodeID(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2DraftIssue{NodeID: &zeroValue} + p.GetNodeID() + p = &ProjectV2DraftIssue{} + p.GetNodeID() + p = nil + p.GetNodeID() +} + +func TestProjectV2DraftIssue_GetTitle(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2DraftIssue{Title: &zeroValue} + p.GetTitle() + p = &ProjectV2DraftIssue{} + p.GetTitle() + p = nil + p.GetTitle() +} + +func TestProjectV2DraftIssue_GetUpdatedAt(tt *testing.T) { + tt.Parallel() + var zeroValue Timestamp + p := &ProjectV2DraftIssue{UpdatedAt: &zeroValue} + p.GetUpdatedAt() + p = &ProjectV2DraftIssue{} + p.GetUpdatedAt() + p = nil + p.GetUpdatedAt() +} + +func TestProjectV2DraftIssue_GetUser(tt *testing.T) { + tt.Parallel() + p := &ProjectV2DraftIssue{} + p.GetUser() + p = nil + p.GetUser() +} + func TestProjectV2Event_GetAction(tt *testing.T) { tt.Parallel() var zeroValue string @@ -25766,6 +25878,14 @@ func TestProjectV2Item_GetArchivedAt(tt *testing.T) { p.GetArchivedAt() } +func TestProjectV2Item_GetContent(tt *testing.T) { + tt.Parallel() + p := &ProjectV2Item{} + p.GetContent() + p = nil + p.GetContent() +} + func TestProjectV2Item_GetContentNodeID(tt *testing.T) { tt.Parallel() var zeroValue string @@ -25779,10 +25899,7 @@ func TestProjectV2Item_GetContentNodeID(tt *testing.T) { func TestProjectV2Item_GetContentType(tt *testing.T) { tt.Parallel() - var zeroValue string - p := &ProjectV2Item{ContentType: &zeroValue} - p.GetContentType() - p = &ProjectV2Item{} + p := &ProjectV2Item{} p.GetContentType() p = nil p.GetContentType() @@ -25889,6 +26006,30 @@ func TestProjectV2ItemChange_GetFieldValue(tt *testing.T) { p.GetFieldValue() } +func TestProjectV2ItemContent_GetDraftIssue(tt *testing.T) { + tt.Parallel() + p := &ProjectV2ItemContent{} + p.GetDraftIssue() + p = nil + p.GetDraftIssue() +} + +func TestProjectV2ItemContent_GetIssue(tt *testing.T) { + tt.Parallel() + p := &ProjectV2ItemContent{} + p.GetIssue() + p = nil + p.GetIssue() +} + +func TestProjectV2ItemContent_GetPullRequest(tt *testing.T) { + tt.Parallel() + p := &ProjectV2ItemContent{} + p.GetPullRequest() + p = nil + p.GetPullRequest() +} + func TestProjectV2ItemEvent_GetAction(tt *testing.T) { tt.Parallel() var zeroValue string @@ -25940,6 +26081,17 @@ func TestProjectV2ItemEvent_GetSender(tt *testing.T) { p.GetSender() } +func TestProjectV2ItemFieldValue_GetDataType(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2ItemFieldValue{DataType: &zeroValue} + p.GetDataType() + p = &ProjectV2ItemFieldValue{} + p.GetDataType() + p = nil + p.GetDataType() +} + func TestProjectV2ItemFieldValue_GetID(tt *testing.T) { tt.Parallel() var zeroValue int64 @@ -25951,6 +26103,124 @@ func TestProjectV2ItemFieldValue_GetID(tt *testing.T) { p.GetID() } +func TestProjectV2ItemFieldValue_GetName(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2ItemFieldValue{Name: &zeroValue} + p.GetName() + p = &ProjectV2ItemFieldValue{} + p.GetName() + p = nil + p.GetName() +} + +func TestProjectV2StatusUpdate_GetBody(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2StatusUpdate{Body: &zeroValue} + p.GetBody() + p = &ProjectV2StatusUpdate{} + p.GetBody() + p = nil + p.GetBody() +} + +func TestProjectV2StatusUpdate_GetCreatedAt(tt *testing.T) { + tt.Parallel() + var zeroValue Timestamp + p := &ProjectV2StatusUpdate{CreatedAt: &zeroValue} + p.GetCreatedAt() + p = &ProjectV2StatusUpdate{} + p.GetCreatedAt() + p = nil + p.GetCreatedAt() +} + +func TestProjectV2StatusUpdate_GetCreator(tt *testing.T) { + tt.Parallel() + p := &ProjectV2StatusUpdate{} + p.GetCreator() + p = nil + p.GetCreator() +} + +func TestProjectV2StatusUpdate_GetID(tt *testing.T) { + tt.Parallel() + var zeroValue int64 + p := &ProjectV2StatusUpdate{ID: &zeroValue} + p.GetID() + p = &ProjectV2StatusUpdate{} + p.GetID() + p = nil + p.GetID() +} + +func TestProjectV2StatusUpdate_GetNodeID(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2StatusUpdate{NodeID: &zeroValue} + p.GetNodeID() + p = &ProjectV2StatusUpdate{} + p.GetNodeID() + p = nil + p.GetNodeID() +} + +func TestProjectV2StatusUpdate_GetProjectNodeID(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2StatusUpdate{ProjectNodeID: &zeroValue} + p.GetProjectNodeID() + p = &ProjectV2StatusUpdate{} + p.GetProjectNodeID() + p = nil + p.GetProjectNodeID() +} + +func TestProjectV2StatusUpdate_GetStartDate(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2StatusUpdate{StartDate: &zeroValue} + p.GetStartDate() + p = &ProjectV2StatusUpdate{} + p.GetStartDate() + p = nil + p.GetStartDate() +} + +func TestProjectV2StatusUpdate_GetStatus(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2StatusUpdate{Status: &zeroValue} + p.GetStatus() + p = &ProjectV2StatusUpdate{} + p.GetStatus() + p = nil + p.GetStatus() +} + +func TestProjectV2StatusUpdate_GetTargetDate(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2StatusUpdate{TargetDate: &zeroValue} + p.GetTargetDate() + p = &ProjectV2StatusUpdate{} + p.GetTargetDate() + p = nil + p.GetTargetDate() +} + +func TestProjectV2StatusUpdate_GetUpdatedAt(tt *testing.T) { + tt.Parallel() + var zeroValue Timestamp + p := &ProjectV2StatusUpdate{UpdatedAt: &zeroValue} + p.GetUpdatedAt() + p = &ProjectV2StatusUpdate{} + p.GetUpdatedAt() + p = nil + p.GetUpdatedAt() +} + func TestProjectV2TextContent_GetHTML(tt *testing.T) { tt.Parallel() var zeroValue string diff --git a/github/github-stringify_test.go b/github/github-stringify_test.go index 5bf246a265d..ebfe5fa1664 100644 --- a/github/github-stringify_test.go +++ b/github/github-stringify_test.go @@ -1560,17 +1560,19 @@ func TestProjectV2_String(t *testing.T) { Number: Ptr(0), ShortDescription: Ptr(""), DeletedBy: &User{}, + State: Ptr(""), + LatestStatusUpdate: &ProjectV2StatusUpdate{}, + IsTemplate: Ptr(false), URL: Ptr(""), HTMLURL: Ptr(""), ColumnsURL: Ptr(""), OwnerURL: Ptr(""), Name: Ptr(""), Body: Ptr(""), - State: Ptr(""), OrganizationPermission: Ptr(""), Private: Ptr(false), } - want := `github.ProjectV2{ID:0, NodeID:"", Owner:github.User{}, Creator:github.User{}, Title:"", Description:"", Public:false, ClosedAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, CreatedAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, UpdatedAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, DeletedAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, Number:0, ShortDescription:"", DeletedBy:github.User{}, URL:"", HTMLURL:"", ColumnsURL:"", OwnerURL:"", Name:"", Body:"", State:"", OrganizationPermission:"", Private:false}` + want := `github.ProjectV2{ID:0, NodeID:"", Owner:github.User{}, Creator:github.User{}, Title:"", Description:"", Public:false, ClosedAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, CreatedAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, UpdatedAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, DeletedAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, Number:0, ShortDescription:"", DeletedBy:github.User{}, State:"", LatestStatusUpdate:github.ProjectV2StatusUpdate{}, IsTemplate:false, URL:"", HTMLURL:"", ColumnsURL:"", OwnerURL:"", Name:"", Body:"", OrganizationPermission:"", Private:false}` if got := v.String(); got != want { t.Errorf("ProjectV2.String = %v, want %v", got, want) } diff --git a/github/projects.go b/github/projects.go index af602bc5657..d2c52f37944 100644 --- a/github/projects.go +++ b/github/projects.go @@ -7,6 +7,7 @@ package github import ( "context" + "encoding/json" "fmt" ) @@ -16,22 +17,61 @@ import ( // GitHub API docs: https://docs.github.com/rest/projects/projects type ProjectsService service +// ProjectV2ItemContentType represents the type of content in a ProjectV2Item. +type ProjectV2ItemContentType string + +// This is the set of possible content types for a ProjectV2Item. +const ( + ProjectV2ItemContentTypeDraftIssue ProjectV2ItemContentType = "DraftIssue" + ProjectV2ItemContentTypeIssue ProjectV2ItemContentType = "Issue" + ProjectV2ItemContentTypePullRequest ProjectV2ItemContentType = "PullRequest" +) + +// ProjectV2StatusUpdate represents a status update for a project. +type ProjectV2StatusUpdate struct { + ID *int64 `json:"id,omitempty"` + NodeID *string `json:"node_id,omitempty"` + ProjectNodeID *string `json:"project_node_id,omitempty"` + Creator *User `json:"creator,omitempty"` + CreatedAt *Timestamp `json:"created_at,omitempty"` + UpdatedAt *Timestamp `json:"updated_at,omitempty"` + // Status can be one of: "INACTIVE", "ON_TRACK", "AT_RISK", "OFF_TRACK", "COMPLETE". + Status *string `json:"status,omitempty"` + StartDate *string `json:"start_date,omitempty"` + TargetDate *string `json:"target_date,omitempty"` + Body *string `json:"body,omitempty"` +} + +// ProjectV2DraftIssue represents a draft issue in a project. +type ProjectV2DraftIssue struct { + ID *int64 `json:"id,omitempty"` + NodeID *string `json:"node_id,omitempty"` + Title *string `json:"title,omitempty"` + Body *string `json:"body,omitempty"` + User *User `json:"user,omitempty"` + CreatedAt *Timestamp `json:"created_at,omitempty"` + UpdatedAt *Timestamp `json:"updated_at,omitempty"` +} + // ProjectV2 represents a v2 project. type ProjectV2 struct { - ID *int64 `json:"id,omitempty"` - NodeID *string `json:"node_id,omitempty"` - Owner *User `json:"owner,omitempty"` - Creator *User `json:"creator,omitempty"` - Title *string `json:"title,omitempty"` - Description *string `json:"description,omitempty"` - Public *bool `json:"public,omitempty"` - ClosedAt *Timestamp `json:"closed_at,omitempty"` - CreatedAt *Timestamp `json:"created_at,omitempty"` - UpdatedAt *Timestamp `json:"updated_at,omitempty"` - DeletedAt *Timestamp `json:"deleted_at,omitempty"` - Number *int `json:"number,omitempty"` - ShortDescription *string `json:"short_description,omitempty"` - DeletedBy *User `json:"deleted_by,omitempty"` + ID *int64 `json:"id,omitempty"` + NodeID *string `json:"node_id,omitempty"` + Owner *User `json:"owner,omitempty"` + Creator *User `json:"creator,omitempty"` + Title *string `json:"title,omitempty"` + Description *string `json:"description,omitempty"` + Public *bool `json:"public,omitempty"` + ClosedAt *Timestamp `json:"closed_at,omitempty"` + CreatedAt *Timestamp `json:"created_at,omitempty"` + UpdatedAt *Timestamp `json:"updated_at,omitempty"` + DeletedAt *Timestamp `json:"deleted_at,omitempty"` + Number *int `json:"number,omitempty"` + ShortDescription *string `json:"short_description,omitempty"` + DeletedBy *User `json:"deleted_by,omitempty"` + State *string `json:"state,omitempty"` + LatestStatusUpdate *ProjectV2StatusUpdate `json:"latest_status_update,omitempty"` + IsTemplate *bool `json:"is_template,omitempty"` // Fields migrated from the Project (classic) struct: URL *string `json:"url,omitempty"` @@ -40,7 +80,6 @@ type ProjectV2 struct { OwnerURL *string `json:"owner_url,omitempty"` Name *string `json:"name,omitempty"` Body *string `json:"body,omitempty"` - State *string `json:"state,omitempty"` OrganizationPermission *string `json:"organization_permission,omitempty"` Private *bool `json:"private,omitempty"` } @@ -115,6 +154,87 @@ type ProjectV2FieldConfiguration struct { Iterations []*ProjectV2FieldIteration `json:"iterations,omitempty"` // The list of iterations associated with the configuration. } +// ProjectV2ItemContent is a union type that holds the content of a ProjectV2Item. +// The actual type depends on the ContentType field of the parent ProjectV2Item. +// Only one of the fields will be populated after unmarshaling. +type ProjectV2ItemContent struct { + Issue *Issue `json:"-"` + PullRequest *PullRequest `json:"-"` + DraftIssue *ProjectV2DraftIssue `json:"-"` +} + +// MarshalJSON implements custom marshaling for ProjectV2ItemContent. +func (c *ProjectV2ItemContent) MarshalJSON() ([]byte, error) { + if c.Issue != nil { + return json.Marshal(c.Issue) + } + if c.PullRequest != nil { + return json.Marshal(c.PullRequest) + } + if c.DraftIssue != nil { + return json.Marshal(c.DraftIssue) + } + return []byte("null"), nil +} + +// ProjectV2Item represents a full project item with field values. +// This type is used by Get, List, and Update operations which return field values. +// The Content field is automatically unmarshaled into the appropriate type based on ContentType. +type ProjectV2Item struct { + ArchivedAt *Timestamp `json:"archived_at,omitempty"` + Content *ProjectV2ItemContent `json:"content,omitempty"` + ContentType *ProjectV2ItemContentType `json:"content_type,omitempty"` + CreatedAt *Timestamp `json:"created_at,omitempty"` + Creator *User `json:"creator,omitempty"` + Fields []*ProjectV2ItemFieldValue `json:"fields,omitempty"` + ID *int64 `json:"id,omitempty"` + ItemURL *string `json:"item_url,omitempty"` + NodeID *string `json:"node_id,omitempty"` + ProjectURL *string `json:"project_url,omitempty"` + UpdatedAt *Timestamp `json:"updated_at,omitempty"` + + // ProjectNodeID and ContentNodeID are used in ProjectsV2Item Webhook payloads. + // They may not be populated in all API responses, but are included here for completeness. + // See: https://docs.github.com/en/webhooks/webhook-events-and-payloads#projects_v2_item + ProjectNodeID *string `json:"project_node_id,omitempty"` + ContentNodeID *string `json:"content_node_id,omitempty"` +} + +// UnmarshalJSON implements custom unmarshaling for ProjectV2Item. +// It uses the ContentType field to determine how to unmarshal the Content field. +func (p *ProjectV2Item) UnmarshalJSON(data []byte) error { + type contentAlias ProjectV2Item + + aux := &struct { + Content json.RawMessage `json:"content,omitempty"` + *contentAlias + }{ + contentAlias: (*contentAlias)(p), + } + + if err := json.Unmarshal(data, aux); err != nil { + return err + } + + // Now unmarshal the content based on ContentType + if len(aux.Content) > 0 && string(aux.Content) != "null" && p.ContentType != nil { + p.Content = &ProjectV2ItemContent{} + switch *p.ContentType { + case ProjectV2ItemContentTypeIssue: + p.Content.Issue = &Issue{} + return json.Unmarshal(aux.Content, p.Content.Issue) + case ProjectV2ItemContentTypePullRequest: + p.Content.PullRequest = &PullRequest{} + return json.Unmarshal(aux.Content, p.Content.PullRequest) + case ProjectV2ItemContentTypeDraftIssue: + p.Content.DraftIssue = &ProjectV2DraftIssue{} + return json.Unmarshal(aux.Content, p.Content.DraftIssue) + } + } + + return nil +} + // ProjectV2Field represents a field in a GitHub Projects V2 project. // Fields define the structure and data types for project items. // @@ -131,6 +251,28 @@ type ProjectV2Field struct { UpdatedAt *Timestamp `json:"updated_at,omitempty"` } +// ProjectV2ItemFieldValue represents a field value of a project item. +type ProjectV2ItemFieldValue struct { + ID *int64 `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + DataType *string `json:"data_type,omitempty"` + // Value set for the field. The type depends on the field type: + // - text: string + // - number: float64 + // - date: string (ISO 8601 date format, e.g. "2023-06-23") or null + // - single_select: object with "id", "name", "color", "description" fields or null + // - iteration: object with "id", "title", "start_date", "duration" fields or null + // - title: object with "text" field (read-only, reflects the item's title) or null + // - assignees: array of user objects with "login", "id", etc. or null + // - labels: array of label objects with "id", "name", "color", etc. or null + // - linked_pull_requests: array of pull request objects or null + // - milestone: milestone object with "id", "title", "description", etc. or null + // - repository: repository object with "id", "name", "full_name", etc. or null + // - reviewers: array of user objects or null + // - status: object with "id", "name", "color", "description" fields (same structure as single_select) or null + Value any `json:"value,omitempty"` +} + // ListOrganizationProjects lists Projects V2 for an organization. // // GitHub API docs: https://docs.github.com/rest/projects/projects#list-projects-for-organization @@ -330,8 +472,8 @@ type GetProjectItemOptions struct { // to a project. The Type must be either "Issue" or "PullRequest" (as per API docs) and // ID is the numerical ID of that issue or pull request. type AddProjectItemOptions struct { - Type string `json:"type,omitempty"` - ID int64 `json:"id,omitempty"` + Type *ProjectV2ItemContentType `json:"type,omitempty"` + ID *int64 `json:"id,omitempty"` } // UpdateProjectV2Field represents a field update for a project item. diff --git a/github/projects_test.go b/github/projects_test.go index 4b30b3d4ecb..ddfa2bd6a4e 100644 --- a/github/projects_test.go +++ b/github/projects_test.go @@ -7,6 +7,7 @@ package github import ( "context" + "encoding/json" "fmt" "io" "net/http" @@ -809,7 +810,7 @@ func TestProjectsService_AddOrganizationProjectItem(t *testing.T) { }) ctx := t.Context() - item, _, err := client.Projects.AddOrganizationProjectItem(ctx, "o", 1, &AddProjectItemOptions{Type: "Issue", ID: 99}) + item, _, err := client.Projects.AddOrganizationProjectItem(ctx, "o", 1, &AddProjectItemOptions{Type: Ptr(ProjectV2ItemContentType("Issue")), ID: Ptr(int64(99))}) if err != nil { t.Fatalf("Projects.AddOrganizationProjectItem returned error: %v", err) } @@ -829,7 +830,7 @@ func TestProjectsService_AddProjectItemForOrg_error(t *testing.T) { ctx := t.Context() const methodName = "AddOrganizationProjectItem" testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { - got, resp, err := client.Projects.AddOrganizationProjectItem(ctx, "o", 1, &AddProjectItemOptions{Type: "Issue", ID: 1}) + got, resp, err := client.Projects.AddOrganizationProjectItem(ctx, "o", 1, &AddProjectItemOptions{Type: Ptr(ProjectV2ItemContentType("Issue")), ID: Ptr(int64(1))}) if got != nil { t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) } @@ -1081,7 +1082,7 @@ func TestProjectsService_AddUserProjectItem(t *testing.T) { fmt.Fprint(w, `{"id":123,"node_id":"PVTI_new_user"}`) }) ctx := t.Context() - item, _, err := client.Projects.AddUserProjectItem(ctx, "u", 2, &AddProjectItemOptions{Type: "PullRequest", ID: 123}) + item, _, err := client.Projects.AddUserProjectItem(ctx, "u", 2, &AddProjectItemOptions{Type: Ptr(ProjectV2ItemContentType("PullRequest")), ID: Ptr(int64(123))}) if err != nil { t.Fatalf("AddUserProjectItem error: %v", err) } @@ -1100,7 +1101,7 @@ func TestProjectsService_AddUserProjectItem_error(t *testing.T) { ctx := t.Context() const methodName = "AddUserProjectItem" testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { - got, resp, err := client.Projects.AddUserProjectItem(ctx, "u", 2, &AddProjectItemOptions{Type: "Issue", ID: 5}) + got, resp, err := client.Projects.AddUserProjectItem(ctx, "u", 2, &AddProjectItemOptions{Type: Ptr(ProjectV2ItemContentType("Issue")), ID: Ptr(int64(5))}) if got != nil { t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) } @@ -1298,3 +1299,366 @@ func TestProjectsService_DeleteUserProjectItem_error(t *testing.T) { return client.Projects.DeleteUserProjectItem(ctx, "u", 2, 55) }) } + +func TestProjectV2Item_UnmarshalJSON_Issue(t *testing.T) { + t.Parallel() + + // Test unmarshaling an issue + jsonData := `{ + "id": 123, + "node_id": "PVTI_test", + "content_type": "Issue", + "content": { + "id": 456, + "number": 10, + "title": "Test Issue", + "state": "open", + "body": "Issue body", + "repository": { + "id": 789, + "name": "test-repo" + } + }, + "created_at": "2023-01-01T00:00:00Z" + }` + + var item ProjectV2Item + if err := json.Unmarshal([]byte(jsonData), &item); err != nil { + t.Fatalf("json.Unmarshal failed: %v", err) + } + + // Verify basic fields + if item.GetID() != 123 { + t.Errorf("ID = %v, want 123", item.GetID()) + } + if item.GetNodeID() != "PVTI_test" { + t.Errorf("NodeID = %v, want PVTI_test", item.GetNodeID()) + } + if item.ContentType == nil || *item.ContentType != ProjectV2ItemContentTypeIssue { + t.Errorf("ContentType = %v, want Issue", item.ContentType) + } + + // Verify content is unmarshaled as Issue + if item.Content == nil { + t.Fatal("Content is nil") + } + if item.GetContent().GetIssue() == nil { + t.Fatal("Content.Issue is nil") + } + if item.GetContent().GetIssue().GetNumber() != 10 { + t.Errorf("Issue.Number = %v, want 10", item.GetContent().GetIssue().GetNumber()) + } + if item.GetContent().GetIssue().GetTitle() != "Test Issue" { + t.Errorf("Issue.Title = %v, want Test Issue", item.GetContent().GetIssue().GetTitle()) + } + if item.GetContent().GetIssue().GetState() != "open" { + t.Errorf("Issue.State = %v, want open", item.GetContent().GetIssue().GetState()) + } + + // Verify other content types are nil + if item.GetContent().GetPullRequest() != nil { + t.Error("Content.PullRequest should be nil for Issue content") + } + if item.GetContent().GetDraftIssue() != nil { + t.Error("Content.DraftIssue should be nil for Issue content") + } +} + +func TestProjectV2Item_UnmarshalJSON_PullRequest(t *testing.T) { + t.Parallel() + + // Test unmarshaling a pull request + jsonData := `{ + "id": 124, + "node_id": "PVTI_pr", + "content_type": "PullRequest", + "content": { + "id": 457, + "number": 20, + "title": "Test PR", + "state": "closed", + "merged": true, + "merge_commit_sha": "abc123", + "head": { + "ref": "feature-branch", + "sha": "def456" + }, + "base": { + "ref": "main", + "sha": "ghi789" + } + }, + "created_at": "2023-01-02T00:00:00Z" + }` + + var item ProjectV2Item + if err := json.Unmarshal([]byte(jsonData), &item); err != nil { + t.Fatalf("json.Unmarshal failed: %v", err) + } + + // Verify basic fields + if item.GetID() != 124 { + t.Errorf("ID = %v, want 124", item.GetID()) + } + if item.ContentType == nil || *item.ContentType != ProjectV2ItemContentTypePullRequest { + t.Errorf("ContentType = %v, want PullRequest", item.ContentType) + } + + // Verify content is unmarshaled as PullRequest + if item.Content == nil { + t.Fatal("Content is nil") + } + if item.GetContent().GetPullRequest() == nil { + t.Fatal("Content.PullRequest is nil") + } + if item.GetContent().GetPullRequest().GetNumber() != 20 { + t.Errorf("PullRequest.Number = %v, want 20", item.GetContent().GetPullRequest().GetNumber()) + } + if item.GetContent().GetPullRequest().GetTitle() != "Test PR" { + t.Errorf("PullRequest.Title = %v, want Test PR", item.GetContent().GetPullRequest().GetTitle()) + } + if !item.GetContent().GetPullRequest().GetMerged() { + t.Errorf("PullRequest.Merged = %t, want true", item.GetContent().GetPullRequest().GetMerged()) + } + if item.GetContent().GetPullRequest().GetMergeCommitSHA() != "abc123" { + t.Errorf("PullRequest.MergeCommitSHA = %v, want abc123", item.GetContent().GetPullRequest().GetMergeCommitSHA()) + } + + // Verify other content types are nil + if item.GetContent().GetIssue() != nil { + t.Error("Content.Issue should be nil for PullRequest content") + } + if item.GetContent().GetDraftIssue() != nil { + t.Error("Content.DraftIssue should be nil for PullRequest content") + } +} + +func TestProjectV2Item_UnmarshalJSON_DraftIssue(t *testing.T) { + t.Parallel() + + // Test unmarshaling a draft issue + jsonData := `{ + "id": 125, + "node_id": "PVTI_draft", + "content_type": "DraftIssue", + "content": { + "id": 458, + "node_id": "DI_test", + "title": "Draft Issue Title", + "body": "Draft issue body content" + }, + "created_at": "2023-01-03T00:00:00Z" + }` + + var item ProjectV2Item + if err := json.Unmarshal([]byte(jsonData), &item); err != nil { + t.Fatalf("json.Unmarshal failed: %v", err) + } + + // Verify basic fields + if item.GetID() != 125 { + t.Errorf("ID = %v, want 125", item.GetID()) + } + if item.ContentType == nil || *item.ContentType != ProjectV2ItemContentTypeDraftIssue { + t.Errorf("ContentType = %v, want DraftIssue", item.ContentType) + } + + // Verify content is unmarshaled as DraftIssue + if item.Content == nil { + t.Fatal("Content is nil") + } + if item.GetContent().GetDraftIssue() == nil { + t.Fatal("Content.DraftIssue is nil") + } + if item.GetContent().GetDraftIssue().GetID() != 458 { + t.Errorf("DraftIssue.ID = %v, want 458", item.GetContent().GetDraftIssue().GetID()) + } + if item.GetContent().GetDraftIssue().GetTitle() != "Draft Issue Title" { + t.Errorf("DraftIssue.Title = %v, want Draft Issue Title", item.GetContent().GetDraftIssue().GetTitle()) + } + if item.GetContent().GetDraftIssue().GetBody() != "Draft issue body content" { + t.Errorf("DraftIssue.Body = %v, want Draft issue body content", item.GetContent().GetDraftIssue().GetBody()) + } + + // Verify other content types are nil + if item.GetContent().GetIssue() != nil { + t.Error("Content.Issue should be nil for DraftIssue content") + } + if item.GetContent().GetPullRequest() != nil { + t.Error("Content.PullRequest should be nil for DraftIssue content") + } +} + +func TestProjectV2Item_UnmarshalJSON_NullContent(t *testing.T) { + t.Parallel() + + // Test with null content + jsonData := `{ + "id": 126, + "node_id": "PVTI_null", + "content_type": "Issue", + "content": null + }` + + var item ProjectV2Item + if err := json.Unmarshal([]byte(jsonData), &item); err != nil { + t.Fatalf("json.Unmarshal failed: %v", err) + } + + // Content should be nil + if item.Content != nil { + t.Error("Content should be nil when content is null in JSON") + } +} + +func TestProjectV2Item_UnmarshalJSON_MissingContentType(t *testing.T) { + t.Parallel() + + // Test without content_type field + jsonData := `{ + "id": 127, + "node_id": "PVTI_no_type", + "content": { + "id": 459, + "title": "Some content" + } + }` + + var item ProjectV2Item + if err := json.Unmarshal([]byte(jsonData), &item); err != nil { + t.Fatalf("json.Unmarshal failed: %v", err) + } + + // Should handle missing ContentType gracefully - content should be nil + // since we can't determine the type + if item.Content != nil { + t.Error("Content should be nil when ContentType is missing") + } +} + +func TestProjectV2Item_UnmarshalJSON_EmptyJSON(t *testing.T) { + t.Parallel() + + // Test with null JSON + var item ProjectV2Item + if err := json.Unmarshal([]byte("null"), &item); err != nil { + t.Fatalf("json.Unmarshal failed with null: %v", err) + } + + // Verify item is in zero state after unmarshaling null + if item.Content != nil { + t.Error("Content should be nil after unmarshaling null") + } +} + +func TestProjectV2Item_UnmarshalJSON_InvalidJSON(t *testing.T) { + t.Parallel() + + // Test with invalid JSON + var item ProjectV2Item + if err := json.Unmarshal([]byte("~~~"), &item); err == nil { + t.Error("expected error for invalid JSON, got nil") + } +} + +func TestProjectV2Item_Marshal_Issue(t *testing.T) { + t.Parallel() + testJSONMarshal(t, &ProjectV2Item{}, "{}") + + item := &ProjectV2Item{ + ContentType: Ptr(ProjectV2ItemContentTypeIssue), + Content: &ProjectV2ItemContent{ + Issue: &Issue{ + Number: Ptr(42), + Title: Ptr("Bug report"), + State: Ptr("open"), + }, + }, + ID: Ptr(int64(123)), + } + + want := `{ + "content_type":"Issue", + "content":{ + "number":42, + "state":"open", + "title":"Bug report" + }, + "id":123 + }` + + testJSONMarshal(t, item, want) +} + +func TestProjectV2Item_Marshal_PullRequest(t *testing.T) { + t.Parallel() + testJSONMarshal(t, &ProjectV2Item{}, "{}") + + item := &ProjectV2Item{ + ContentType: Ptr(ProjectV2ItemContentTypePullRequest), + Content: &ProjectV2ItemContent{ + PullRequest: &PullRequest{ + Number: Ptr(99), + Title: Ptr("Feature addition"), + State: Ptr("closed"), + }, + }, + ID: Ptr(int64(456)), + } + + want := `{ + "content_type":"PullRequest", + "content":{ + "number":99, + "state":"closed", + "title":"Feature addition" + }, + "id":456 + }` + + testJSONMarshal(t, item, want) +} + +func TestProjectV2Item_Marshal_DraftIssue(t *testing.T) { + t.Parallel() + testJSONMarshal(t, &ProjectV2Item{}, "{}") + + item := &ProjectV2Item{ + ContentType: Ptr(ProjectV2ItemContentTypeDraftIssue), + Content: &ProjectV2ItemContent{ + DraftIssue: &ProjectV2DraftIssue{ + Title: Ptr("Draft task"), + Body: Ptr("Work in progress"), + }, + }, + ID: Ptr(int64(789)), + } + + want := `{ + "content_type":"DraftIssue", + "content":{ + "body":"Work in progress", + "title":"Draft task" + }, + "id":789 + }` + + testJSONMarshal(t, item, want) +} + +func TestProjectV2Item_Marshal_MissingContent(t *testing.T) { + t.Parallel() + + item := &ProjectV2Item{ + ContentType: Ptr(ProjectV2ItemContentTypeIssue), + Content: nil, + ID: Ptr(int64(789)), + } + + want := `{ + "content_type":"Issue", + "id":789 + }` + + testJSONMarshal(t, item, want) +}