From b3fbd37e992cf3f9f42f49818087c67d464000eb Mon Sep 17 00:00:00 2001
From: 6543 <6543@obermui.de>
Date: Thu, 17 Jun 2021 16:02:34 +0200
Subject: [PATCH] [API] expose repo.GetReviewers() & repo.GetAssignees()
 (#16168)

* API: expose repo.GetReviewers() & repo.GetAssignees()

* Add tests

* fix unrelated swagger query type
---
 integrations/api_repo_test.go        | 28 +++++++++++
 modules/convert/user.go              |  9 ++++
 routers/api/v1/api.go                |  2 +
 routers/api/v1/notify/repo.go        |  2 +-
 routers/api/v1/notify/user.go        |  2 +-
 routers/api/v1/repo/collaborators.go | 60 ++++++++++++++++++++++++
 routers/api/v1/user/user.go          |  8 +---
 templates/swagger/v1_json.tmpl       | 70 +++++++++++++++++++++++++++-
 8 files changed, 170 insertions(+), 11 deletions(-)

diff --git a/integrations/api_repo_test.go b/integrations/api_repo_test.go
index 1ca4575508..98c9fb6ec7 100644
--- a/integrations/api_repo_test.go
+++ b/integrations/api_repo_test.go
@@ -494,3 +494,31 @@ func TestAPIRepoTransfer(t *testing.T) {
 	repo = models.AssertExistsAndLoadBean(t, &models.Repository{ID: repo.ID}).(*models.Repository)
 	_ = models.DeleteRepository(user, repo.OwnerID, repo.ID)
 }
+
+func TestAPIRepoGetReviewers(t *testing.T) {
+	defer prepareTestEnv(t)()
+	user := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
+	session := loginUser(t, user.Name)
+	token := getTokenForLoggedInUser(t, session)
+	repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
+
+	req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/reviewers?token=%s", user.Name, repo.Name, token)
+	resp := session.MakeRequest(t, req, http.StatusOK)
+	var reviewers []*api.User
+	DecodeJSON(t, resp, &reviewers)
+	assert.Len(t, reviewers, 4)
+}
+
+func TestAPIRepoGetAssignees(t *testing.T) {
+	defer prepareTestEnv(t)()
+	user := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
+	session := loginUser(t, user.Name)
+	token := getTokenForLoggedInUser(t, session)
+	repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
+
+	req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/assignees?token=%s", user.Name, repo.Name, token)
+	resp := session.MakeRequest(t, req, http.StatusOK)
+	var assignees []*api.User
+	DecodeJSON(t, resp, &assignees)
+	assert.Len(t, assignees, 1)
+}
diff --git a/modules/convert/user.go b/modules/convert/user.go
index c588f5f2f0..07a4efd41a 100644
--- a/modules/convert/user.go
+++ b/modules/convert/user.go
@@ -25,6 +25,15 @@ func ToUser(user, doer *models.User) *api.User {
 	return toUser(user, signed, authed)
 }
 
+// ToUsers convert list of models.User to list of api.User
+func ToUsers(doer *models.User, users []*models.User) []*api.User {
+	result := make([]*api.User, len(users))
+	for i := range users {
+		result[i] = ToUser(users[i], doer)
+	}
+	return result
+}
+
 // ToUserWithAccessMode convert models.User to api.User
 // AccessMode is not none show add some more information
 func ToUserWithAccessMode(user *models.User, accessMode models.AccessMode) *api.User {
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index acee6329af..0b47953e58 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -746,6 +746,8 @@ func Routes() *web.Route {
 						Put(reqAdmin(), bind(api.AddCollaboratorOption{}), repo.AddCollaborator).
 						Delete(reqAdmin(), repo.DeleteCollaborator)
 				}, reqToken())
+				m.Get("/assignees", reqToken(), reqAnyRepoReader(), repo.GetAssignees)
+				m.Get("/reviewers", reqToken(), reqAnyRepoReader(), repo.GetReviewers)
 				m.Group("/teams", func() {
 					m.Get("", reqAnyRepoReader(), repo.ListTeams)
 					m.Combo("/{team}").Get(reqAnyRepoReader(), repo.IsTeam).
diff --git a/routers/api/v1/notify/repo.go b/routers/api/v1/notify/repo.go
index 4deb16a227..af55d1d49c 100644
--- a/routers/api/v1/notify/repo.go
+++ b/routers/api/v1/notify/repo.go
@@ -65,7 +65,7 @@ func ListRepoNotifications(ctx *context.APIContext) {
 	// - name: all
 	//   in: query
 	//   description: If true, show notifications marked as read. Default value is false
-	//   type: string
+	//   type: boolean
 	// - name: status-types
 	//   in: query
 	//   description: "Show notifications with the provided status types. Options are: unread, read and/or pinned. Defaults to unread & pinned"
diff --git a/routers/api/v1/notify/user.go b/routers/api/v1/notify/user.go
index 1ff62622b0..475a541bdc 100644
--- a/routers/api/v1/notify/user.go
+++ b/routers/api/v1/notify/user.go
@@ -27,7 +27,7 @@ func ListNotifications(ctx *context.APIContext) {
 	// - name: all
 	//   in: query
 	//   description: If true, show notifications marked as read. Default value is false
-	//   type: string
+	//   type: boolean
 	// - name: status-types
 	//   in: query
 	//   description: "Show notifications with the provided status types. Options are: unread, read and/or pinned. Defaults to unread & pinned."
diff --git a/routers/api/v1/repo/collaborators.go b/routers/api/v1/repo/collaborators.go
index d0936019fa..078af1f6ff 100644
--- a/routers/api/v1/repo/collaborators.go
+++ b/routers/api/v1/repo/collaborators.go
@@ -221,3 +221,63 @@ func DeleteCollaborator(ctx *context.APIContext) {
 	}
 	ctx.Status(http.StatusNoContent)
 }
+
+// GetReviewers return all users that can be requested to review in this repo
+func GetReviewers(ctx *context.APIContext) {
+	// swagger:operation GET /repos/{owner}/{repo}/reviewers repository repoGetReviewers
+	// ---
+	// summary: Return all users that can be requested to review in this repo
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: owner
+	//   in: path
+	//   description: owner of the repo
+	//   type: string
+	//   required: true
+	// - name: repo
+	//   in: path
+	//   description: name of the repo
+	//   type: string
+	//   required: true
+	// responses:
+	//   "200":
+	//     "$ref": "#/responses/UserList"
+
+	reviewers, err := ctx.Repo.Repository.GetReviewers(ctx.User.ID, 0)
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "ListCollaborators", err)
+		return
+	}
+	ctx.JSON(http.StatusOK, convert.ToUsers(ctx.User, reviewers))
+}
+
+// GetAssignees return all users that have write access and can be assigned to issues
+func GetAssignees(ctx *context.APIContext) {
+	// swagger:operation GET /repos/{owner}/{repo}/assignees repository repoGetAssignees
+	// ---
+	// summary: Return all users that have write access and can be assigned to issues
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: owner
+	//   in: path
+	//   description: owner of the repo
+	//   type: string
+	//   required: true
+	// - name: repo
+	//   in: path
+	//   description: name of the repo
+	//   type: string
+	//   required: true
+	// responses:
+	//   "200":
+	//     "$ref": "#/responses/UserList"
+
+	assignees, err := ctx.Repo.Repository.GetAssignees()
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "ListCollaborators", err)
+		return
+	}
+	ctx.JSON(http.StatusOK, convert.ToUsers(ctx.User, assignees))
+}
diff --git a/routers/api/v1/user/user.go b/routers/api/v1/user/user.go
index 6e811bf0f8..4adae532fd 100644
--- a/routers/api/v1/user/user.go
+++ b/routers/api/v1/user/user.go
@@ -13,7 +13,6 @@ import (
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/convert"
-	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/routers/api/v1/utils"
 )
 
@@ -73,18 +72,13 @@ func Search(ctx *context.APIContext) {
 		return
 	}
 
-	results := make([]*api.User, len(users))
-	for i := range users {
-		results[i] = convert.ToUser(users[i], ctx.User)
-	}
-
 	ctx.SetLinkHeader(int(maxResults), listOptions.PageSize)
 	ctx.Header().Set("X-Total-Count", fmt.Sprintf("%d", maxResults))
 	ctx.Header().Set("Access-Control-Expose-Headers", "X-Total-Count, Link")
 
 	ctx.JSON(http.StatusOK, map[string]interface{}{
 		"ok":   true,
-		"data": results,
+		"data": convert.ToUsers(ctx.User, users),
 	})
 }
 
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 18b870517e..9b0d07ebfc 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -630,7 +630,7 @@
         "operationId": "notifyGetList",
         "parameters": [
           {
-            "type": "string",
+            "type": "boolean",
             "description": "If true, show notifications marked as read. Default value is false",
             "name": "all",
             "in": "query"
@@ -2277,6 +2277,39 @@
         }
       }
     },
+    "/repos/{owner}/{repo}/assignees": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "repository"
+        ],
+        "summary": "Return all users that have write access and can be assigned to issues",
+        "operationId": "repoGetAssignees",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "owner of the repo",
+            "name": "owner",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the repo",
+            "name": "repo",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/UserList"
+          }
+        }
+      }
+    },
     "/repos/{owner}/{repo}/branch_protections": {
       "get": {
         "produces": [
@@ -6844,7 +6877,7 @@
             "required": true
           },
           {
-            "type": "string",
+            "type": "boolean",
             "description": "If true, show notifications marked as read. Default value is false",
             "name": "all",
             "in": "query"
@@ -8629,6 +8662,39 @@
         }
       }
     },
+    "/repos/{owner}/{repo}/reviewers": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "repository"
+        ],
+        "summary": "Return all users that can be requested to review in this repo",
+        "operationId": "repoGetReviewers",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "owner of the repo",
+            "name": "owner",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the repo",
+            "name": "repo",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/UserList"
+          }
+        }
+      }
+    },
     "/repos/{owner}/{repo}/signing-key.gpg": {
       "get": {
         "produces": [