Repository: xanzy/go-gitlab
Pull request #1149 (2021-05-21 09:38:24)

Add some missing todos fields
Add some of:
* Type `TodoTargetType`
* Type `IssueTasksCompletionStatus`
* Type `BasicProject`

Remove from todo.project: (not found in json & doc)
* `HTTPURLToRepo`
* `WebURL`

Not reproduced, but present [in doc](https://docs.gitlab.com/ce/api/todos.html#get-a-list-of-to-dos)
`TodoTargetAlertManagement  TodoTargetType = "AlertManagement::Alert"`


Patch:
diff --git a/client_options.go b/client_options.go
index 9f74046..f1d39ad 100644
--- a/client_options.go
+++ b/client_options.go
@@ -40,7 +40,8 @@ func WithCustomBackoff(backoff retryablehttp.Backoff) ClientOptionFunc {
 	}
 }
 
-// WithCustomLogger can be used to configure a custom retryablehttp leveled logger
+// WithCustomLeveledLogger can be used to configure a custom retryablehttp
+// leveled logger.
 func WithCustomLeveledLogger(leveledLogger retryablehttp.LeveledLogger) ClientOptionFunc {
 	return func(c *Client) error {
 		c.client.Logger = leveledLogger
@@ -58,7 +59,7 @@ func WithCustomLimiter(limiter RateLimiter) ClientOptionFunc {
 	}
 }
 
-// WithCustomLogger can be used to configure a custom retryablehttp logger
+// WithCustomLogger can be used to configure a custom retryablehttp logger.
 func WithCustomLogger(logger retryablehttp.Logger) ClientOptionFunc {
 	return func(c *Client) error {
 		c.client.Logger = logger
diff --git a/issues.go b/issues.go
index 397e8c2..19c28d9 100644
--- a/issues.go
+++ b/issues.go
@@ -85,44 +85,41 @@ type IssueLinks struct {
 //
 // GitLab API docs: https://docs.gitlab.com/ce/api/issues.html
 type Issue struct {
-	ID                   int              `json:"id"`
-	IID                  int              `json:"iid"`
-	ExternalID           string           `json:"external_id"`
-	State                string           `json:"state"`
-	Description          string           `json:"description"`
-	Author               *IssueAuthor     `json:"author"`
-	Milestone            *Milestone       `json:"milestone"`
-	ProjectID            int              `json:"project_id"`
-	Assignees            []*IssueAssignee `json:"assignees"`
-	Assignee             *IssueAssignee   `json:"assignee"`
-	UpdatedAt            *time.Time       `json:"updated_at"`
-	ClosedAt             *time.Time       `json:"closed_at"`
-	ClosedBy             *IssueCloser     `json:"closed_by"`
-	Title                string           `json:"title"`
-	CreatedAt            *time.Time       `json:"created_at"`
-	MovedToID            int              `json:"moved_to_id"`
-	Labels               Labels           `json:"labels"`
-	LabelDetails         []*LabelDetails  `json:"label_details"`
-	Upvotes              int              `json:"upvotes"`
-	Downvotes            int              `json:"downvotes"`
-	DueDate              *ISOTime         `json:"due_date"`
-	WebURL               string           `json:"web_url"`
-	References           *IssueReferences `json:"references"`
-	TimeStats            *TimeStats       `json:"time_stats"`
-	Confidential         bool             `json:"confidential"`
-	Weight               int              `json:"weight"`
-	DiscussionLocked     bool             `json:"discussion_locked"`
-	Subscribed           bool             `json:"subscribed"`
-	UserNotesCount       int              `json:"user_notes_count"`
-	Links                *IssueLinks      `json:"_links"`
-	IssueLinkID          int              `json:"issue_link_id"`
-	MergeRequestCount    int              `json:"merge_requests_count"`
-	EpicIssueID          int              `json:"epic_issue_id"`
-	Epic                 *Epic            `json:"epic"`
-	TaskCompletionStatus struct {
-		Count          int `json:"count"`
-		CompletedCount int `json:"completed_count"`
-	} `json:"task_completion_status"`
+	ID                   int                    `json:"id"`
+	IID                  int                    `json:"iid"`
+	ExternalID           string                 `json:"external_id"`
+	State                string                 `json:"state"`
+	Description          string                 `json:"description"`
+	Author               *IssueAuthor           `json:"author"`
+	Milestone            *Milestone             `json:"milestone"`
+	ProjectID            int                    `json:"project_id"`
+	Assignees            []*IssueAssignee       `json:"assignees"`
+	Assignee             *IssueAssignee         `json:"assignee"`
+	UpdatedAt            *time.Time             `json:"updated_at"`
+	ClosedAt             *time.Time             `json:"closed_at"`
+	ClosedBy             *IssueCloser           `json:"closed_by"`
+	Title                string                 `json:"title"`
+	CreatedAt            *time.Time             `json:"created_at"`
+	MovedToID            int                    `json:"moved_to_id"`
+	Labels               Labels                 `json:"labels"`
+	LabelDetails         []*LabelDetails        `json:"label_details"`
+	Upvotes              int                    `json:"upvotes"`
+	Downvotes            int                    `json:"downvotes"`
+	DueDate              *ISOTime               `json:"due_date"`
+	WebURL               string                 `json:"web_url"`
+	References           *IssueReferences       `json:"references"`
+	TimeStats            *TimeStats             `json:"time_stats"`
+	Confidential         bool                   `json:"confidential"`
+	Weight               int                    `json:"weight"`
+	DiscussionLocked     bool                   `json:"discussion_locked"`
+	Subscribed           bool                   `json:"subscribed"`
+	UserNotesCount       int                    `json:"user_notes_count"`
+	Links                *IssueLinks            `json:"_links"`
+	IssueLinkID          int                    `json:"issue_link_id"`
+	MergeRequestCount    int                    `json:"merge_requests_count"`
+	EpicIssueID          int                    `json:"epic_issue_id"`
+	Epic                 *Epic                  `json:"epic"`
+	TaskCompletionStatus *TasksCompletionStatus `json:"task_completion_status"`
 }
 
 func (i Issue) String() string {
diff --git a/merge_requests.go b/merge_requests.go
index 3ec2a7e..72a383b 100644
--- a/merge_requests.go
+++ b/merge_requests.go
@@ -96,18 +96,15 @@ type MergeRequest struct {
 		HeadSha  string `json:"head_sha"`
 		StartSha string `json:"start_sha"`
 	} `json:"diff_refs"`
-	DivergedCommitsCount int    `json:"diverged_commits_count"`
-	RebaseInProgress     bool   `json:"rebase_in_progress"`
-	ApprovalsBeforeMerge int    `json:"approvals_before_merge"`
-	Reference            string `json:"reference"`
-	FirstContribution    bool   `json:"first_contribution"`
-	TaskCompletionStatus struct {
-		Count          int `json:"count"`
-		CompletedCount int `json:"completed_count"`
-	} `json:"task_completion_status"`
-	HasConflicts                bool `json:"has_conflicts"`
-	BlockingDiscussionsResolved bool `json:"blocking_discussions_resolved"`
-	Overflow                    bool `json:"overflow"`
+	DivergedCommitsCount        int                    `json:"diverged_commits_count"`
+	RebaseInProgress            bool                   `json:"rebase_in_progress"`
+	ApprovalsBeforeMerge        int                    `json:"approvals_before_merge"`
+	Reference                   string                 `json:"reference"`
+	FirstContribution           bool                   `json:"first_contribution"`
+	TaskCompletionStatus        *TasksCompletionStatus `json:"task_completion_status"`
+	HasConflicts                bool                   `json:"has_conflicts"`
+	BlockingDiscussionsResolved bool                   `json:"blocking_discussions_resolved"`
+	Overflow                    bool                   `json:"overflow"`
 }
 
 func (m MergeRequest) String() string {
diff --git a/projects.go b/projects.go
index c58907c..795d08d 100644
--- a/projects.go
+++ b/projects.go
@@ -126,6 +126,17 @@ type Project struct {
 	MergeRequestsTemplate string             `json:"merge_requests_template"`
 }
 
+// BasicProject included in other service responses (such as todos).
+type BasicProject struct {
+	ID                int        `json:"id"`
+	Description       string     `json:"description"`
+	Name              string     `json:"name"`
+	NameWithNamespace string     `json:"name_with_namespace"`
+	Path              string     `json:"path"`
+	PathWithNamespace string     `json:"path_with_namespace"`
+	CreatedAt         *time.Time `json:"created_at"`
+}
+
 // ContainerExpirationPolicy represents the container expiration policy.
 type ContainerExpirationPolicy struct {
 	Cadence         string     `json:"cadence"`
diff --git a/todos.go b/todos.go
index 7c8cdd0..7ffb7fc 100644
--- a/todos.go
+++ b/todos.go
@@ -30,110 +30,77 @@ type TodosService struct {
 	client *Client
 }
 
-// TodoAction represents the available actions that can be performed on a todo.
+// Todo represents a GitLab todo.
 //
 // GitLab API docs: https://docs.gitlab.com/ce/api/todos.html
-type TodoAction string
-
-// The available todo actions.
-const (
-	TodoAssigned          TodoAction = "assigned"
-	TodoMentioned         TodoAction = "mentioned"
-	TodoBuildFailed       TodoAction = "build_failed"
-	TodoMarked            TodoAction = "marked"
-	TodoApprovalRequired  TodoAction = "approval_required"
-	TodoDirectlyAddressed TodoAction = "directly_addressed"
-)
+type Todo struct {
+	ID         int            `json:"id"`
+	Project    *BasicProject  `json:"project"`
+	Author     *BasicUser     `json:"author"`
+	ActionName TodoAction     `json:"action_name"`
+	TargetType TodoTargetType `json:"target_type"`
+	Target     *TodoTarget    `json:"target"`
+	TargetURL  string         `json:"target_url"`
+	Body       string         `json:"body"`
+	State      string         `json:"state"`
+	CreatedAt  *time.Time     `json:"created_at"`
+}
+
+func (t Todo) String() string {
+	return Stringify(t)
+}
 
 // TodoTarget represents a todo target of type Issue or MergeRequest
 type TodoTarget struct {
-	// TODO: replace both Assignee and Author structs with v4 User struct
-	Assignee struct {
-		Name      string `json:"name"`
-		Username  string `json:"username"`
-		ID        int    `json:"id"`
-		State     string `json:"state"`
-		AvatarURL string `json:"avatar_url"`
-		WebURL    string `json:"web_url"`
-	} `json:"assignee"`
-	Author struct {
-		Name      string `json:"name"`
-		Username  string `json:"username"`
-		ID        int    `json:"id"`
-		State     string `json:"state"`
-		AvatarURL string `json:"avatar_url"`
-		WebURL    string `json:"web_url"`
-	} `json:"author"`
-	CreatedAt      *time.Time `json:"created_at"`
-	Description    string     `json:"description"`
-	Downvotes      int        `json:"downvotes"`
-	ID             int        `json:"id"`
-	IID            int        `json:"iid"`
-	Labels         []string   `json:"labels"`
-	Milestone      Milestone  `json:"milestone"`
-	ProjectID      int        `json:"project_id"`
-	State          string     `json:"state"`
-	Subscribed     bool       `json:"subscribed"`
-	Title          string     `json:"title"`
-	UpdatedAt      *time.Time `json:"updated_at"`
-	Upvotes        int        `json:"upvotes"`
-	UserNotesCount int        `json:"user_notes_count"`
-	WebURL         string     `json:"web_url"`
+	Assignees            []*BasicUser           `json:"assignees"`
+	Assignee             *BasicUser             `json:"assignee"`
+	Author               *BasicUser             `json:"author"`
+	CreatedAt            *time.Time             `json:"created_at"`
+	Description          string                 `json:"description"`
+	Downvotes            int                    `json:"downvotes"`
+	ID                   int                    `json:"id"`
+	IID                  int                    `json:"iid"`
+	Labels               []string               `json:"labels"`
+	Milestone            *Milestone             `json:"milestone"`
+	ProjectID            int                    `json:"project_id"`
+	State                string                 `json:"state"`
+	Subscribed           bool                   `json:"subscribed"`
+	TaskCompletionStatus *TasksCompletionStatus `json:"task_completion_status"`
+	Title                string                 `json:"title"`
+	UpdatedAt            *time.Time             `json:"updated_at"`
+	Upvotes              int                    `json:"upvotes"`
+	UserNotesCount       int                    `json:"user_notes_count"`
+	WebURL               string                 `json:"web_url"`
 
 	// Only available for type Issue
-	Confidential bool   `json:"confidential"`
-	DueDate      string `json:"due_date"`
-	Weight       int    `json:"weight"`
+	Confidential bool        `json:"confidential"`
+	DueDate      string      `json:"due_date"`
+	HasTasks     bool        `json:"has_tasks"`
+	Links        *IssueLinks `json:"_links"`
+	MovedToID    int         `json:"moved_to_id"`
+	TimeStats    *TimeStats  `json:"time_stats"`
+	Weight       int         `json:"weight"`
 
 	// Only available for type MergeRequest
-	ApprovalsBeforeMerge      int    `json:"approvals_before_merge"`
-	ForceRemoveSourceBranch   bool   `json:"force_remove_source_branch"`
-	MergeCommitSHA            string `json:"merge_commit_sha"`
-	MergeWhenPipelineSucceeds bool   `json:"merge_when_pipeline_succeeds"`
-	MergeStatus               string `json:"merge_status"`
-	SHA                       string `json:"sha"`
-	ShouldRemoveSourceBranch  bool   `json:"should_remove_source_branch"`
-	SourceBranch              string `json:"source_branch"`
-	SourceProjectID           int    `json:"source_project_id"`
-	Squash                    bool   `json:"squash"`
-	TargetBranch              string `json:"target_branch"`
-	TargetProjectID           int    `json:"target_project_id"`
-	WorkInProgress            bool   `json:"work_in_progress"`
-}
-
-// Todo represents a GitLab todo.
-//
-// GitLab API docs: https://docs.gitlab.com/ce/api/todos.html
-type Todo struct {
-	ID      int `json:"id"`
-	Project struct {
-		ID                int    `json:"id"`
-		HTTPURLToRepo     string `json:"http_url_to_repo"`
-		WebURL            string `json:"web_url"`
-		Name              string `json:"name"`
-		NameWithNamespace string `json:"name_with_namespace"`
-		Path              string `json:"path"`
-		PathWithNamespace string `json:"path_with_namespace"`
-	} `json:"project"`
-	Author struct {
-		ID        int    `json:"id"`
-		Name      string `json:"name"`
-		Username  string `json:"username"`
-		State     string `json:"state"`
-		AvatarURL string `json:"avatar_url"`
-		WebURL    string `json:"web_url"`
-	} `json:"author"`
-	ActionName TodoAction `json:"action_name"`
-	TargetType string     `json:"target_type"`
-	Target     TodoTarget `json:"target"`
-	TargetURL  string     `json:"target_url"`
-	Body       string     `json:"body"`
-	State      string     `json:"state"`
-	CreatedAt  *time.Time `json:"created_at"`
-}
-
-func (t Todo) String() string {
-	return Stringify(t)
+	ApprovalsBeforeMerge      int          `json:"approvals_before_merge"`
+	ForceRemoveSourceBranch   bool         `json:"force_remove_source_branch"`
+	MergeCommitSHA            string       `json:"merge_commit_sha"`
+	MergeWhenPipelineSucceeds bool         `json:"merge_when_pipeline_succeeds"`
+	MergeStatus               string       `json:"merge_status"`
+	Reference                 string       `json:"reference"`
+	Reviewers                 []*BasicUser `json:"reviewers"`
+	SHA                       string       `json:"sha"`
+	ShouldRemoveSourceBranch  bool         `json:"should_remove_source_branch"`
+	SourceBranch              string       `json:"source_branch"`
+	SourceProjectID           int          `json:"source_project_id"`
+	Squash                    bool         `json:"squash"`
+	TargetBranch              string       `json:"target_branch"`
+	TargetProjectID           int          `json:"target_project_id"`
+	WorkInProgress            bool         `json:"work_in_progress"`
+
+	// Only available for type DesignManagement::Design
+	FileName string `json:"filename"`
+	ImageURL string `json:"image_url"`
 }
 
 // ListTodosOptions represents the available ListTodos() options.
diff --git a/types.go b/types.go
index 805cd5b..44187b2 100644
--- a/types.go
+++ b/types.go
@@ -403,6 +403,39 @@ func SubGroupCreationLevel(v SubGroupCreationLevelValue) *SubGroupCreationLevelV
 	return p
 }
 
+// TasksCompletionStatus represents tasks of the issue/merge request.
+type TasksCompletionStatus struct {
+	Count          int `json:"count"`
+	CompletedCount int `json:"completed_count"`
+}
+
+// TodoAction represents the available actions that can be performed on a todo.
+//
+// GitLab API docs: https://docs.gitlab.com/ce/api/todos.html
+type TodoAction string
+
+// The available todo actions.
+const (
+	TodoAssigned          TodoAction = "assigned"
+	TodoMentioned         TodoAction = "mentioned"
+	TodoBuildFailed       TodoAction = "build_failed"
+	TodoMarked            TodoAction = "marked"
+	TodoApprovalRequired  TodoAction = "approval_required"
+	TodoDirectlyAddressed TodoAction = "directly_addressed"
+)
+
+// TodoTargetType represents the available target that can be linked to a todo.
+//
+// GitLab API docs: https://docs.gitlab.com/ce/api/todos.html
+type TodoTargetType string
+
+const (
+	TodoTargetAlertManagement  TodoTargetType = "AlertManagement::Alert"
+	TodoTargetDesignManagement TodoTargetType = "DesignManagement::Design"
+	TodoTargetIssue            TodoTargetType = "Issue"
+	TodoTargetMergeRequest     TodoTargetType = "MergeRequest"
+)
+
 // VariableTypeValue represents a variable type within GitLab.
 //
 // GitLab API docs: https://docs.gitlab.com/ce/api/


Test patch:
diff --git a/todos_test.go b/todos_test.go
index 425d32a..401136c 100644
--- a/todos_test.go
+++ b/todos_test.go
@@ -37,7 +37,7 @@ func TestListTodos(t *testing.T) {
 
 	require.NoError(t, err)
 
-	want := []*Todo{{ID: 1, State: "pending", Target: TodoTarget{ID: 1, ApprovalsBeforeMerge: 2}}, {ID: 2, State: "pending", Target: TodoTarget{ID: 2}}}
+	want := []*Todo{{ID: 1, State: "pending", Target: &TodoTarget{ID: 1, ApprovalsBeforeMerge: 2}}, {ID: 2, State: "pending", Target: &TodoTarget{ID: 2}}}
 	require.Equal(t, want, todos)
 }