Repository: xanzy/go-gitlab
Pull request #1905 (2024-03-29 17:50:40)

Add support for "draft" notes
This MR adds supports for draft notes, which are special notes that are part of a review and can be "published."

Documentation for this resource type is here: https://docs.gitlab.com/ee/api/draft_notes.html


Patch:
diff --git a/draft_notes.go b/draft_notes.go
new file mode 100644
index 0000000..376e4d0
--- /dev/null
+++ b/draft_notes.go
@@ -0,0 +1,233 @@
+//
+// Copyright 2021, Sander van Harmelen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package gitlab
+
+import (
+	"fmt"
+	"net/http"
+)
+
+type DraftNote struct {
+	ID                int           `json:"id"`
+	AuthorID          int           `json:"author_id"`
+	MergeRequestID    int           `json:"merge_request_id"`
+	ResolveDiscussion bool          `json:"resolve_discussion"`
+	DiscussionID      string        `json:"discussion_id"`
+	Note              string        `json:"note"`
+	CommitID          string        `json:"commit_id"`
+	LineCode          string        `json:"line_code"`
+	Position          *NotePosition `json:"position"`
+}
+
+// DraftNotesService handles communication with the draft notes related methods
+// of the GitLab API.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/draft_notes.html#list-all-merge-request-draft-notes
+type DraftNotesService struct {
+	client *Client
+}
+
+// ListDraftNotesOptions represents the available ListDraftNotes()
+// options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/draft_notes.html#list-all-merge-request-draft-notes
+type ListDraftNotesOptions struct {
+	ListOptions
+	OrderBy *string `url:"order_by,omitempty" json:"order_by,omitempty"`
+	Sort    *string `url:"sort,omitempty" json:"sort,omitempty"`
+}
+
+// ListDraftNotes gets a list of all draft notes for a merge request.
+//
+// Gitlab API docs:
+// https://docs.gitlab.com/ee/api/draft_notes.html#list-all-merge-request-draft-notes
+func (s *DraftNotesService) ListDraftNotes(pid interface{}, mergeRequest int, opt *ListDraftNotesOptions, options ...RequestOptionFunc) ([]*DraftNote, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("projects/%s/merge_requests/%d/draft_notes", PathEscape(project), mergeRequest)
+
+	req, err := s.client.NewRequest(http.MethodGet, u, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var n []*DraftNote
+	resp, err := s.client.Do(req, &n)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return n, resp, nil
+}
+
+// GetDraftNote gets a single draft note for a merge request.
+//
+// Gitlab API docs:
+// https://docs.gitlab.com/ee/api/draft_notes.html#get-a-single-draft-note
+func (s *DraftNotesService) GetDraftNote(pid interface{}, mergeRequest int, note int, options ...RequestOptionFunc) (*DraftNote, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("projects/%s/merge_requests/%d/draft_notes/%d", PathEscape(project), mergeRequest, note)
+
+	req, err := s.client.NewRequest(http.MethodGet, u, nil, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	n := new(DraftNote)
+	resp, err := s.client.Do(req, &n)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return n, resp, nil
+}
+
+// CreateDraftNoteOptions represents the available CreateDraftNote()
+// options.
+//
+// Gitlab API docs:
+// https://docs.gitlab.com/ee/api/draft_notes.html#create-a-draft-note
+type CreateDraftNoteOptions struct {
+	Note                  *string          `url:"note" json:"note"`
+	CommitID              *string          `url:"commit_id,omitempty" json:"commit_id,omitempty"`
+	InReplyToDiscussionID *string          `url:"in_reply_to_discussion_id,omitempty" json:"in_reply_to_discussion_id,omitempty"`
+	ResolveDiscussion     *bool            `url:"resolve_discussion,omitempty" json:"resolve_discussion,omitempty"`
+	Position              *PositionOptions `url:"position,omitempty" json:"position,omitempty"`
+}
+
+// CreateDraftNote creates a draft note for a merge request.
+//
+// Gitlab API docs:
+// https://docs.gitlab.com/ee/api/draft_notes.html#create-a-draft-note
+func (s *DraftNotesService) CreateDraftNote(pid interface{}, mergeRequest int, opt *CreateDraftNoteOptions, options ...RequestOptionFunc) (*DraftNote, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("projects/%s/merge_requests/%d/draft_notes", PathEscape(project), mergeRequest)
+
+	req, err := s.client.NewRequest(http.MethodPost, u, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	n := new(DraftNote)
+	resp, err := s.client.Do(req, &n)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return n, resp, nil
+}
+
+// UpdateDraftNoteOptions represents the available UpdateDraftNote()
+// options.
+//
+// Gitlab API docs:
+// https://docs.gitlab.com/ee/api/draft_notes.html#create-a-draft-note
+type UpdateDraftNoteOptions struct {
+	Note     *string          `url:"note,omitempty" json:"note,omitempty"`
+	Position *PositionOptions `url:"position,omitempty" json:"position,omitempty"`
+}
+
+// UpdateDraftNote updates a draft note for a merge request.
+//
+// Gitlab API docs: https://docs.gitlab.com/ee/api/draft_notes.html#create-a-draft-note
+func (s *DraftNotesService) UpdateDraftNote(pid interface{}, mergeRequest int, note int, opt *UpdateDraftNoteOptions, options ...RequestOptionFunc) (*DraftNote, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("projects/%s/merge_requests/%d/draft_notes/%d", PathEscape(project), mergeRequest, note)
+
+	req, err := s.client.NewRequest(http.MethodPut, u, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	n := new(DraftNote)
+	resp, err := s.client.Do(req, &n)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return n, resp, nil
+}
+
+// DeleteDraftNote deletes a single draft note for a merge request.
+//
+// Gitlab API docs:
+// https://docs.gitlab.com/ee/api/draft_notes.html#delete-a-draft-note
+func (s *DraftNotesService) DeleteDraftNote(pid interface{}, mergeRequest int, note int, options ...RequestOptionFunc) (*Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, err
+	}
+	u := fmt.Sprintf("projects/%s/merge_requests/%d/draft_notes/%d", PathEscape(project), mergeRequest, note)
+
+	req, err := s.client.NewRequest(http.MethodDelete, u, nil, options)
+	if err != nil {
+		return nil, err
+	}
+
+	return s.client.Do(req, nil)
+}
+
+// PublishDraftNote publishes a single draft note for a merge request.
+//
+// Gitlab API docs:
+// https://docs.gitlab.com/ee/api/draft_notes.html#publish-a-draft-note
+func (s *DraftNotesService) PublishDraftNote(pid interface{}, mergeRequest int, note int, options ...RequestOptionFunc) (*Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, err
+	}
+	u := fmt.Sprintf("projects/%s/merge_requests/%d/draft_notes/%d/publish", PathEscape(project), mergeRequest, note)
+
+	req, err := s.client.NewRequest(http.MethodPut, u, nil, options)
+	if err != nil {
+		return nil, err
+	}
+
+	return s.client.Do(req, nil)
+}
+
+// PublishAllDraftNotes publishes all draft notes for a merge request that belong to the user.
+//
+// Gitlab API docs:
+// https://docs.gitlab.com/ee/api/draft_notes.html#publish-a-draft-note
+func (s *DraftNotesService) PublishAllDraftNotes(pid interface{}, mergeRequest int, options ...RequestOptionFunc) (*Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, err
+	}
+	u := fmt.Sprintf("projects/%s/merge_requests/%d/draft_notes/bulk_publish", PathEscape(project), mergeRequest)
+
+	req, err := s.client.NewRequest(http.MethodPost, u, nil, options)
+	if err != nil {
+		return nil, err
+	}
+
+	return s.client.Do(req, nil)
+}
diff --git a/gitlab.go b/gitlab.go
index c773ef5..e492050 100644
--- a/gitlab.go
+++ b/gitlab.go
@@ -124,6 +124,7 @@ type Client struct {
 	Deployments                  *DeploymentsService
 	Discussions                  *DiscussionsService
 	DockerfileTemplate           *DockerfileTemplatesService
+	DraftNotes                   *DraftNotesService
 	Environments                 *EnvironmentsService
 	EpicIssues                   *EpicIssuesService
 	Epics                        *EpicsService
@@ -357,6 +358,7 @@ func newClient(options ...ClientOptionFunc) (*Client, error) {
 	c.Deployments = &DeploymentsService{client: c}
 	c.Discussions = &DiscussionsService{client: c}
 	c.DockerfileTemplate = &DockerfileTemplatesService{client: c}
+	c.DraftNotes = &DraftNotesService{client: c}
 	c.Environments = &EnvironmentsService{client: c}
 	c.EpicIssues = &EpicIssuesService{client: c}
 	c.Epics = &EpicsService{client: c}


Test patch:
diff --git a/draft_notes_test.go b/draft_notes_test.go
new file mode 100644
index 0000000..266efaa
--- /dev/null
+++ b/draft_notes_test.go
@@ -0,0 +1,212 @@
+//
+// Copyright 2021, Sander van Harmelen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package gitlab
+
+import (
+	"net/http"
+	"reflect"
+	"testing"
+)
+
+func TestGetDraftNote(t *testing.T) {
+	mux, client := setup(t)
+	mux.HandleFunc("/api/v4/projects/1/merge_requests/4329/draft_notes/3", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodGet)
+		mustWriteHTTPResponse(t, w, "testdata/get_draft_note.json")
+	})
+
+	note, _, err := client.DraftNotes.GetDraftNote("1", 4329, 3)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	want := &DraftNote{
+		ID:                37349978,
+		AuthorID:          10271899,
+		MergeRequestID:    291473309,
+		ResolveDiscussion: false,
+		DiscussionID:      "",
+		Note:              "Some draft note",
+		CommitID:          "",
+		LineCode:          "",
+		Position:          nil,
+	}
+
+	if !reflect.DeepEqual(note, want) {
+		t.Errorf("DraftNotes.GetDraftNote want %#v, got %#v", note, want)
+	}
+}
+
+func TestListDraftNotes(t *testing.T) {
+	mux, client := setup(t)
+	mux.HandleFunc("/api/v4/projects/1/merge_requests/4329/draft_notes", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodGet)
+		mustWriteHTTPResponse(t, w, "testdata/list_draft_notes.json")
+	})
+
+	notes, _, err := client.DraftNotes.ListDraftNotes("1", 4329, nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	want := []*DraftNote{
+		{
+			ID:                37349978,
+			AuthorID:          10271899,
+			MergeRequestID:    291473309,
+			ResolveDiscussion: false,
+			DiscussionID:      "",
+			Note:              "Some draft note",
+			CommitID:          "",
+			LineCode:          "",
+			Position:          nil,
+		},
+		{
+			ID:                37349979,
+			AuthorID:          10271899,
+			MergeRequestID:    291473309,
+			ResolveDiscussion: false,
+			DiscussionID:      "",
+			Note:              "Some draft note 2",
+			CommitID:          "",
+			LineCode:          "3dacf79e0d779e2baa1c700cf56510e42f55cf85_10_9",
+			Position: &NotePosition{
+				BaseSHA:      "64581c4ee41beb44d943d7801f82d9038e25e453",
+				StartSHA:     "87bffbff93bf334889780f54ae1922355661f867",
+				HeadSHA:      "2c972dbf9094c380f5f00dcd8112d2c69b24c859",
+				OldPath:      "src/some-dir/some-file.js",
+				NewPath:      "src/some-dir/some-file.js",
+				PositionType: "text",
+				NewLine:      9,
+				LineRange: &LineRange{
+					StartRange: &LinePosition{
+						LineCode: "3dacf79e0d779e2baa1c700cf56510e42f55cf85_10_9",
+						Type:     "new",
+						NewLine:  9,
+					},
+					EndRange: &LinePosition{
+						LineCode: "3dacf79e0d779e2baa1c700cf56510e42f55cf85_10_9",
+						Type:     "new",
+						NewLine:  9,
+					},
+				},
+			},
+		},
+	}
+
+	if !reflect.DeepEqual(notes, want) {
+		t.Errorf("DraftNotes.GetDraftNote want %#v, got %#v", notes, want)
+	}
+}
+
+func TestCreateDraftNote(t *testing.T) {
+	mux, client := setup(t)
+	mux.HandleFunc("/api/v4/projects/1/merge_requests/4329/draft_notes", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodPost)
+		mustWriteHTTPResponse(t, w, "testdata/create_draft_note.json")
+	})
+
+	note, _, err := client.DraftNotes.CreateDraftNote("1", 4329, &CreateDraftNoteOptions{
+		Note: Ptr("Some new draft note"),
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	want := &DraftNote{
+		ID:                37349980,
+		AuthorID:          10271899,
+		MergeRequestID:    291473309,
+		ResolveDiscussion: false,
+		DiscussionID:      "",
+		Note:              "Some new draft note",
+		CommitID:          "",
+		LineCode:          "",
+		Position:          nil,
+	}
+
+	if !reflect.DeepEqual(note, want) {
+		t.Errorf("DraftNotes.GetDraftNote want %#v, got %#v", note, want)
+	}
+}
+
+func TestUpdateDraftNote(t *testing.T) {
+	mux, client := setup(t)
+	mux.HandleFunc("/api/v4/projects/1/merge_requests/4329/draft_notes/3", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodPut)
+		mustWriteHTTPResponse(t, w, "testdata/update_draft_note.json")
+	})
+
+	note, _, err := client.DraftNotes.UpdateDraftNote("1", 4329, 3, &UpdateDraftNoteOptions{
+		Note: Ptr("Some changed draft note"),
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	want := &DraftNote{
+		ID:                37349980,
+		AuthorID:          10271899,
+		MergeRequestID:    291473309,
+		ResolveDiscussion: false,
+		DiscussionID:      "",
+		Note:              "Some changed draft note",
+		CommitID:          "",
+		LineCode:          "",
+		Position:          nil,
+	}
+
+	if !reflect.DeepEqual(note, want) {
+		t.Errorf("DraftNotes.UpdateDraftNote want %#v, got %#v", note, want)
+	}
+}
+
+func TestDeleteDraftNote(t *testing.T) {
+	mux, client := setup(t)
+	mux.HandleFunc("/api/v4/projects/1/merge_requests/4329/draft_notes/3", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodDelete)
+	})
+
+	_, err := client.DraftNotes.DeleteDraftNote("1", 4329, 3)
+	if err != nil {
+		t.Errorf("DraftNotes.DeleteDraftNote returned error: %v", err)
+	}
+}
+
+func TestPublishDraftNote(t *testing.T) {
+	mux, client := setup(t)
+	mux.HandleFunc("/api/v4/projects/1/merge_requests/4329/draft_notes/3/publish", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodPut)
+	})
+
+	_, err := client.DraftNotes.PublishDraftNote("1", 4329, 3)
+	if err != nil {
+		t.Errorf("DraftNotes.PublishDraftNote returned error: %v", err)
+	}
+}
+
+func TestPublishAllDraftNotes(t *testing.T) {
+	mux, client := setup(t)
+	mux.HandleFunc("/api/v4/projects/1/merge_requests/4329/draft_notes/bulk_publish", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodPost)
+	})
+
+	_, err := client.DraftNotes.PublishAllDraftNotes("1", 4329)
+	if err != nil {
+		t.Errorf("DraftNotes.PublishAllDraftNotes returned error: %v", err)
+	}
+}
diff --git a/groups_test.go b/groups_test.go
index d848e74..7fc6c3f 100644
--- a/groups_test.go
+++ b/groups_test.go
@@ -179,7 +179,6 @@ func TestDeleteGroup_WithPermanentDelete(t *testing.T) {
 		PermanentlyRemove: Ptr(true),
 		FullPath:          Ptr("testPath"),
 	})
-
 	if err != nil {
 		t.Errorf("Groups.DeleteGroup returned error: %v", err)
 	}
diff --git a/testdata/create_draft_note.json b/testdata/create_draft_note.json
new file mode 100644
index 0000000..bc0dc68
--- /dev/null
+++ b/testdata/create_draft_note.json
@@ -0,0 +1,11 @@
+{
+  "id": 37349980,
+  "author_id": 10271899,
+  "merge_request_id": 291473309,
+  "resolve_discussion": false,
+  "discussion_id": null,
+  "note": "Some new draft note",
+  "commit_id": null,
+  "position": null,
+  "line_code": null
+}
diff --git a/testdata/get_draft_note.json b/testdata/get_draft_note.json
new file mode 100644
index 0000000..a1e9cd4
--- /dev/null
+++ b/testdata/get_draft_note.json
@@ -0,0 +1,11 @@
+{
+  "id": 37349978,
+  "author_id": 10271899,
+  "merge_request_id": 291473309,
+  "resolve_discussion": false,
+  "discussion_id": null,
+  "note": "Some draft note",
+  "commit_id": null,
+  "position": null,
+  "line_code": null
+}
diff --git a/testdata/list_draft_notes.json b/testdata/list_draft_notes.json
new file mode 100644
index 0000000..15e235b
--- /dev/null
+++ b/testdata/list_draft_notes.json
@@ -0,0 +1,47 @@
+[
+  {
+    "id": 37349978,
+    "author_id": 10271899,
+    "merge_request_id": 291473309,
+    "resolve_discussion": false,
+    "discussion_id": null,
+    "note": "Some draft note",
+    "commit_id": null,
+    "position": null,
+    "line_code": null
+  },
+  {
+    "id": 37349979,
+    "author_id": 10271899,
+    "merge_request_id": 291473309,
+    "resolve_discussion": false,
+    "discussion_id": null,
+    "note": "Some draft note 2",
+    "commit_id": null,
+    "line_code": "3dacf79e0d779e2baa1c700cf56510e42f55cf85_10_9",
+    "position": {
+      "base_sha": "64581c4ee41beb44d943d7801f82d9038e25e453",
+      "start_sha": "87bffbff93bf334889780f54ae1922355661f867",
+      "head_sha": "2c972dbf9094c380f5f00dcd8112d2c69b24c859",
+      "old_path": "src/some-dir/some-file.js",
+      "new_path": "src/some-dir/some-file.js",
+      "position_type": "text",
+      "old_line": null,
+      "new_line": 9,
+      "line_range": {
+        "start": {
+          "line_code": "3dacf79e0d779e2baa1c700cf56510e42f55cf85_10_9",
+          "type": "new",
+          "old_line": null,
+          "new_line": 9
+        },
+        "end": {
+          "line_code": "3dacf79e0d779e2baa1c700cf56510e42f55cf85_10_9",
+          "type": "new",
+          "old_line": null,
+          "new_line": 9
+        }
+      }
+    }
+  }
+]
diff --git a/testdata/update_draft_note.json b/testdata/update_draft_note.json
new file mode 100644
index 0000000..98cea1a
--- /dev/null
+++ b/testdata/update_draft_note.json
@@ -0,0 +1,11 @@
+{
+  "id": 37349980,
+  "author_id": 10271899,
+  "merge_request_id": 291473309,
+  "resolve_discussion": false,
+  "discussion_id": null,
+  "note": "Some changed draft note",
+  "commit_id": null,
+  "position": null,
+  "line_code": null
+}