From 34d9cb335c08da785b90acb2c1001b93218f6ac2 Mon Sep 17 00:00:00 2001
From: Johan Van de Wauw <johan@gisky.be>
Date: Fri, 25 Sep 2020 21:11:43 +0200
Subject: [PATCH] API: Get release by tags endpoint (#12932)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Get a release based on a tag name (for which a release exists).
Based on:
https://developer.github.com/v3/repos/releases/#get-a-release-by-tag-name

Co-authored-by: 赵智超 <1012112796@qq.com>
Co-authored-by: 6543 <6543@obermui.de>
---
 integrations/api_releases_test.go   | 34 ++++++++++++++++
 routers/api/v1/api.go               |  3 ++
 routers/api/v1/repo/release_tags.go | 60 +++++++++++++++++++++++++++++
 templates/swagger/v1_json.tmpl      | 43 +++++++++++++++++++++
 4 files changed, 140 insertions(+)
 create mode 100644 routers/api/v1/repo/release_tags.go

diff --git a/integrations/api_releases_test.go b/integrations/api_releases_test.go
index 9aef33d06..58c2e3544 100644
--- a/integrations/api_releases_test.go
+++ b/integrations/api_releases_test.go
@@ -7,6 +7,7 @@ package integrations
 import (
 	"fmt"
 	"net/http"
+	"strings"
 	"testing"
 
 	"code.gitea.io/gitea/models"
@@ -120,3 +121,36 @@ func TestAPICreateReleaseToDefaultBranchOnExistingTag(t *testing.T) {
 
 	createNewReleaseUsingAPI(t, session, token, owner, repo, "v0.0.1", "", "v0.0.1", "test")
 }
+
+func TestAPIGetReleaseByTag(t *testing.T) {
+	defer prepareTestEnv(t)()
+
+	repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
+	owner := models.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User)
+	session := loginUser(t, owner.LowerName)
+
+	tag := "v1.1"
+
+	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s/",
+		owner.Name, repo.Name, tag)
+
+	req := NewRequestf(t, "GET", urlStr)
+	resp := session.MakeRequest(t, req, http.StatusOK)
+
+	var release *api.Release
+	DecodeJSON(t, resp, &release)
+
+	assert.Equal(t, "testing-release", release.Title)
+
+	nonexistingtag := "nonexistingtag"
+
+	urlStr = fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s/",
+		owner.Name, repo.Name, nonexistingtag)
+
+	req = NewRequestf(t, "GET", urlStr)
+	resp = session.MakeRequest(t, req, http.StatusNotFound)
+
+	var err *api.APIError
+	DecodeJSON(t, resp, &err)
+	assert.True(t, strings.HasPrefix(err.Message, "release tag does not exist"))
+}
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index 3b6f8dbba..7b2d567e3 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -797,6 +797,9 @@ func RegisterRoutes(m *macaron.Macaron) {
 								Delete(reqToken(), reqRepoWriter(models.UnitTypeReleases), repo.DeleteReleaseAttachment)
 						})
 					})
+					m.Group("/tags", func() {
+						m.Get("/:tag", repo.GetReleaseTag)
+					})
 				}, reqRepoReader(models.UnitTypeReleases))
 				m.Post("/mirror-sync", reqToken(), reqRepoWriter(models.UnitTypeCode), repo.MirrorSync)
 				m.Get("/editorconfig/:filename", context.RepoRef(), reqRepoReader(models.UnitTypeCode), repo.GetEditorconfig)
diff --git a/routers/api/v1/repo/release_tags.go b/routers/api/v1/repo/release_tags.go
new file mode 100644
index 000000000..bde3251ba
--- /dev/null
+++ b/routers/api/v1/repo/release_tags.go
@@ -0,0 +1,60 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package repo
+
+import (
+	"net/http"
+
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/context"
+)
+
+// GetReleaseTag get a single release of a repository by its tagname
+func GetReleaseTag(ctx *context.APIContext) {
+	// swagger:operation GET /repos/{owner}/{repo}/releases/tags/{tag} repository repoGetReleaseTag
+	// ---
+	// summary: Get a release by tag name
+	// 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
+	// - name: tag
+	//   in: path
+	//   description: tagname of the release to get
+	//   type: string
+	//   required: true
+	// responses:
+	//   "200":
+	//     "$ref": "#/responses/Release"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	tag := ctx.Params(":tag")
+
+	release, err := models.GetRelease(ctx.Repo.Repository.ID, tag)
+	if err != nil {
+		if models.IsErrReleaseNotExist(err) {
+			ctx.Error(http.StatusNotFound, "GetRelease", err)
+			return
+		}
+		ctx.Error(http.StatusInternalServerError, "GetRelease", err)
+		return
+	}
+
+	if err := release.LoadAttributes(); err != nil {
+		ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
+		return
+	}
+	ctx.JSON(http.StatusOK, release.APIFormat())
+}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 51a618ae4..e54f40d67 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -7685,6 +7685,49 @@
         }
       }
     },
+    "/repos/{owner}/{repo}/releases/tags/{tag}": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "repository"
+        ],
+        "summary": "Get a release by tag name",
+        "operationId": "repoGetReleaseTag",
+        "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
+          },
+          {
+            "type": "string",
+            "description": "tagname of the release to get",
+            "name": "tag",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/Release"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      }
+    },
     "/repos/{owner}/{repo}/releases/{id}": {
       "get": {
         "produces": [