Loading issues.go +61 −0 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ import ( "fmt" "net/http" "reflect" "strings" "time" ) Loading Loading @@ -484,6 +485,9 @@ func (s *IssuesService) CreateIssue(pid any, opt *CreateIssueOptions, options .. // UpdateIssueOptions represents the available UpdateIssue() options. // // To reset the due date, epic, milestone, or weight of the issue, set the // ResetDueDate, ResetEpic, ResetMilestone, or ResetWeight field to true. // // GitLab API docs: https://docs.gitlab.com/api/issues/#edit-an-issue type UpdateIssueOptions struct { Title *string `url:"title,omitempty" json:"title,omitempty"` Loading @@ -501,6 +505,63 @@ type UpdateIssueOptions struct { Weight *int `url:"weight,omitempty" json:"weight,omitempty"` DiscussionLocked *bool `url:"discussion_locked,omitempty" json:"discussion_locked,omitempty"` IssueType *string `url:"issue_type,omitempty" json:"issue_type,omitempty"` ResetDueDate bool `url:"-" json:"-"` ResetEpicID bool `url:"-" json:"-"` ResetMilestoneID bool `url:"-" json:"-"` ResetWeight bool `url:"-" json:"-"` } // MarshalJSON implements custom JSON marshaling for UpdateIssueOptions. // This is needed to support emitting a literal `null` when the field needs to be removed. func (o UpdateIssueOptions) MarshalJSON() ([]byte, error) { data := map[string]any{} // Use reflection to copy all fields from o to data val := reflect.ValueOf(o) typ := val.Type() for i := range val.NumField() { field := val.Field(i) fieldName := typ.Field(i).Name if field.IsZero() { continue } name := fieldName if tag := typ.Field(i).Tag.Get("json"); tag != "" { tagFields := strings.Split(tag, ",") name = tagFields[0] } // Skip unexported fields. if name == "-" { continue } data[name] = field.Interface() } // Emit a literal `null` when the field needs to be removed if o.ResetDueDate { data["due_date"] = nil } if o.ResetEpicID { data["epic_id"] = nil } if o.ResetMilestoneID { data["milestone_id"] = nil } if o.ResetWeight { data["weight"] = nil } return json.Marshal(data) } // UpdateIssue updates an existing project issue. This function is also used Loading issues_test.go +87 −1 Original line number Diff line number Diff line Loading @@ -21,8 +21,10 @@ import ( "net/http" "reflect" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestGetIssue(t *testing.T) { Loading Loading @@ -643,7 +645,22 @@ func TestUpdateIssue(t *testing.T) { mux.HandleFunc("/api/v4/projects/1/issues/5", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodPut) fmt.Fprint(w, `{"id":1, "title" : "Title of issue", "description": "This is description of an issue", "author" : {"id" : 1, "name": "snehal"}, "assignees":[{"id":1}]}`) testBodyJSON(t, r, map[string]any{ "title": "Title of issue", "description": "This is description of an issue", }) mustWriteJSONResponse(t, w, map[string]any{ "id": 1, "title": "Title of issue", "description": "This is description of an issue", "author": map[string]any{ "id": 1, "name": "snehal", }, "assignees": []map[string]any{ {"id": 1}, }, }) }) updateIssueOpt := &UpdateIssueOptions{ Loading @@ -668,6 +685,75 @@ func TestUpdateIssue(t *testing.T) { } } func TestUpdateIssue_ResetFields(t *testing.T) { t.Parallel() tests := []struct { name string opts UpdateIssueOptions wantBody map[string]any }{ { name: "set due date", opts: UpdateIssueOptions{DueDate: Ptr(ISOTime(time.Date(2025, time.May, 12, 0, 0, 0, 0, time.UTC)))}, wantBody: map[string]any{"due_date": "2025-05-12"}, }, { name: "unset due date", opts: UpdateIssueOptions{ResetDueDate: true}, wantBody: map[string]any{"due_date": nil}, }, { name: "set epic", opts: UpdateIssueOptions{EpicID: Ptr(42)}, wantBody: map[string]any{"epic_id": float64(42)}, }, { name: "unset epic", opts: UpdateIssueOptions{ResetEpicID: true}, wantBody: map[string]any{"epic_id": nil}, }, { name: "set milestone", opts: UpdateIssueOptions{MilestoneID: Ptr(42)}, wantBody: map[string]any{"milestone_id": float64(42)}, }, { name: "unset milestone", opts: UpdateIssueOptions{ResetMilestoneID: true}, wantBody: map[string]any{"milestone_id": nil}, }, { name: "set weight", opts: UpdateIssueOptions{Weight: Ptr(42)}, wantBody: map[string]any{"weight": float64(42)}, }, { name: "unset weight", opts: UpdateIssueOptions{ResetWeight: true}, wantBody: map[string]any{"weight": nil}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() mux, client := setup(t) mux.HandleFunc("/api/v4/projects/1/issues/5", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodPut) testBodyJSON(t, r, tt.wantBody) mustWriteJSONResponse(t, w, map[string]any{"id": 5}) }) issue, _, err := client.Issues.UpdateIssue(1, 5, &tt.opts) require.NoError(t, err) assert.Equal(t, 5, issue.ID) }) } } func TestSubscribeToIssue(t *testing.T) { t.Parallel() mux, client := setup(t) Loading Loading
issues.go +61 −0 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ import ( "fmt" "net/http" "reflect" "strings" "time" ) Loading Loading @@ -484,6 +485,9 @@ func (s *IssuesService) CreateIssue(pid any, opt *CreateIssueOptions, options .. // UpdateIssueOptions represents the available UpdateIssue() options. // // To reset the due date, epic, milestone, or weight of the issue, set the // ResetDueDate, ResetEpic, ResetMilestone, or ResetWeight field to true. // // GitLab API docs: https://docs.gitlab.com/api/issues/#edit-an-issue type UpdateIssueOptions struct { Title *string `url:"title,omitempty" json:"title,omitempty"` Loading @@ -501,6 +505,63 @@ type UpdateIssueOptions struct { Weight *int `url:"weight,omitempty" json:"weight,omitempty"` DiscussionLocked *bool `url:"discussion_locked,omitempty" json:"discussion_locked,omitempty"` IssueType *string `url:"issue_type,omitempty" json:"issue_type,omitempty"` ResetDueDate bool `url:"-" json:"-"` ResetEpicID bool `url:"-" json:"-"` ResetMilestoneID bool `url:"-" json:"-"` ResetWeight bool `url:"-" json:"-"` } // MarshalJSON implements custom JSON marshaling for UpdateIssueOptions. // This is needed to support emitting a literal `null` when the field needs to be removed. func (o UpdateIssueOptions) MarshalJSON() ([]byte, error) { data := map[string]any{} // Use reflection to copy all fields from o to data val := reflect.ValueOf(o) typ := val.Type() for i := range val.NumField() { field := val.Field(i) fieldName := typ.Field(i).Name if field.IsZero() { continue } name := fieldName if tag := typ.Field(i).Tag.Get("json"); tag != "" { tagFields := strings.Split(tag, ",") name = tagFields[0] } // Skip unexported fields. if name == "-" { continue } data[name] = field.Interface() } // Emit a literal `null` when the field needs to be removed if o.ResetDueDate { data["due_date"] = nil } if o.ResetEpicID { data["epic_id"] = nil } if o.ResetMilestoneID { data["milestone_id"] = nil } if o.ResetWeight { data["weight"] = nil } return json.Marshal(data) } // UpdateIssue updates an existing project issue. This function is also used Loading
issues_test.go +87 −1 Original line number Diff line number Diff line Loading @@ -21,8 +21,10 @@ import ( "net/http" "reflect" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestGetIssue(t *testing.T) { Loading Loading @@ -643,7 +645,22 @@ func TestUpdateIssue(t *testing.T) { mux.HandleFunc("/api/v4/projects/1/issues/5", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodPut) fmt.Fprint(w, `{"id":1, "title" : "Title of issue", "description": "This is description of an issue", "author" : {"id" : 1, "name": "snehal"}, "assignees":[{"id":1}]}`) testBodyJSON(t, r, map[string]any{ "title": "Title of issue", "description": "This is description of an issue", }) mustWriteJSONResponse(t, w, map[string]any{ "id": 1, "title": "Title of issue", "description": "This is description of an issue", "author": map[string]any{ "id": 1, "name": "snehal", }, "assignees": []map[string]any{ {"id": 1}, }, }) }) updateIssueOpt := &UpdateIssueOptions{ Loading @@ -668,6 +685,75 @@ func TestUpdateIssue(t *testing.T) { } } func TestUpdateIssue_ResetFields(t *testing.T) { t.Parallel() tests := []struct { name string opts UpdateIssueOptions wantBody map[string]any }{ { name: "set due date", opts: UpdateIssueOptions{DueDate: Ptr(ISOTime(time.Date(2025, time.May, 12, 0, 0, 0, 0, time.UTC)))}, wantBody: map[string]any{"due_date": "2025-05-12"}, }, { name: "unset due date", opts: UpdateIssueOptions{ResetDueDate: true}, wantBody: map[string]any{"due_date": nil}, }, { name: "set epic", opts: UpdateIssueOptions{EpicID: Ptr(42)}, wantBody: map[string]any{"epic_id": float64(42)}, }, { name: "unset epic", opts: UpdateIssueOptions{ResetEpicID: true}, wantBody: map[string]any{"epic_id": nil}, }, { name: "set milestone", opts: UpdateIssueOptions{MilestoneID: Ptr(42)}, wantBody: map[string]any{"milestone_id": float64(42)}, }, { name: "unset milestone", opts: UpdateIssueOptions{ResetMilestoneID: true}, wantBody: map[string]any{"milestone_id": nil}, }, { name: "set weight", opts: UpdateIssueOptions{Weight: Ptr(42)}, wantBody: map[string]any{"weight": float64(42)}, }, { name: "unset weight", opts: UpdateIssueOptions{ResetWeight: true}, wantBody: map[string]any{"weight": nil}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() mux, client := setup(t) mux.HandleFunc("/api/v4/projects/1/issues/5", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodPut) testBodyJSON(t, r, tt.wantBody) mustWriteJSONResponse(t, w, map[string]any{"id": 5}) }) issue, _, err := client.Issues.UpdateIssue(1, 5, &tt.opts) require.NoError(t, err) assert.Equal(t, 5, issue.ID) }) } } func TestSubscribeToIssue(t *testing.T) { t.Parallel() mux, client := setup(t) Loading