diff --git a/integrations/api_releases_test.go b/integrations/api_releases_test.go index 58c2e3544..8328b014d 100644 --- a/integrations/api_releases_test.go +++ b/integrations/api_releases_test.go @@ -154,3 +154,26 @@ func TestAPIGetReleaseByTag(t *testing.T) { DecodeJSON(t, resp, &err) assert.True(t, strings.HasPrefix(err.Message, "release tag does not exist")) } + +func TestAPIDeleteTagByName(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) + token := getTokenForLoggedInUser(t, session) + + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/delete-tag/?token=%s", + owner.Name, repo.Name, token) + + req := NewRequestf(t, http.MethodDelete, urlStr) + _ = session.MakeRequest(t, req, http.StatusNoContent) + + // Make sure that actual releases can't be deleted outright + createNewReleaseUsingAPI(t, session, token, owner, repo, "release-tag", "", "Release Tag", "test") + urlStr = fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/release-tag/?token=%s", + owner.Name, repo.Name, token) + + req = NewRequestf(t, http.MethodDelete, urlStr) + _ = session.MakeRequest(t, req, http.StatusConflict) +} diff --git a/integrations/api_repo_test.go b/integrations/api_repo_test.go index c8afa73ae..fed63a959 100644 --- a/integrations/api_repo_test.go +++ b/integrations/api_repo_test.go @@ -223,7 +223,7 @@ func TestAPIViewRepo(t *testing.T) { DecodeJSON(t, resp, &repo) assert.EqualValues(t, 1, repo.ID) assert.EqualValues(t, "repo1", repo.Name) - assert.EqualValues(t, 1, repo.Releases) + assert.EqualValues(t, 2, repo.Releases) assert.EqualValues(t, 1, repo.OpenIssues) assert.EqualValues(t, 3, repo.OpenPulls) diff --git a/integrations/release_test.go b/integrations/release_test.go index 4d2260d88..c817dcaec 100644 --- a/integrations/release_test.go +++ b/integrations/release_test.go @@ -83,7 +83,7 @@ func TestCreateRelease(t *testing.T) { session := loginUser(t, "user2") createNewRelease(t, session, "/user2/repo1", "v0.0.1", "v0.0.1", false, false) - checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", i18n.Tr("en", "repo.release.stable"), 2) + checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", i18n.Tr("en", "repo.release.stable"), 3) } func TestCreateReleasePreRelease(t *testing.T) { @@ -92,7 +92,7 @@ func TestCreateReleasePreRelease(t *testing.T) { session := loginUser(t, "user2") createNewRelease(t, session, "/user2/repo1", "v0.0.1", "v0.0.1", true, false) - checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", i18n.Tr("en", "repo.release.prerelease"), 2) + checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", i18n.Tr("en", "repo.release.prerelease"), 3) } func TestCreateReleaseDraft(t *testing.T) { @@ -101,7 +101,7 @@ func TestCreateReleaseDraft(t *testing.T) { session := loginUser(t, "user2") createNewRelease(t, session, "/user2/repo1", "v0.0.1", "v0.0.1", false, true) - checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", i18n.Tr("en", "repo.release.draft"), 2) + checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", i18n.Tr("en", "repo.release.draft"), 3) } func TestCreateReleasePaging(t *testing.T) { diff --git a/models/fixtures/release.yml b/models/fixtures/release.yml index f95eb048b..8d3f5840e 100644 --- a/models/fixtures/release.yml +++ b/models/fixtures/release.yml @@ -27,3 +27,19 @@ is_prerelease: false is_tag: false created_unix: 946684800 + +- + id: 3 + repo_id: 1 + publisher_id: 2 + tag_name: "delete-tag" + lower_tag_name: "delete-tag" + target: "master" + title: "delete-tag" + sha1: "65f1bf27bc3bf70f64657658635e66094edbcb4d" + num_commits: 10 + is_draft: false + is_prerelease: false + is_tag: true + created_unix: 946684800 + diff --git a/modules/context/api.go b/modules/context/api.go index 772e1f8f5..9dad588c7 100644 --- a/modules/context/api.go +++ b/modules/context/api.go @@ -61,6 +61,10 @@ type APIForbiddenError struct { // swagger:response notFound type APINotFound struct{} +//APIConflict is a conflict empty response +// swagger:response conflict +type APIConflict struct{} + //APIRedirect is a redirect response // swagger:response redirect type APIRedirect struct{} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 147cb8e27..42489cd4a 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -798,7 +798,9 @@ func RegisterRoutes(m *macaron.Macaron) { }) }) m.Group("/tags", func() { - m.Get("/:tag", repo.GetReleaseTag) + m.Combo("/:tag"). + Get(repo.GetReleaseTag). + Delete(reqToken(), reqRepoWriter(models.UnitTypeReleases), repo.DeleteReleaseTag) }) }, reqRepoReader(models.UnitTypeReleases)) m.Post("/mirror-sync", reqToken(), reqRepoWriter(models.UnitTypeCode), repo.MirrorSync) diff --git a/routers/api/v1/repo/release_tags.go b/routers/api/v1/repo/release_tags.go index 2a72e0000..ef07ce5e1 100644 --- a/routers/api/v1/repo/release_tags.go +++ b/routers/api/v1/repo/release_tags.go @@ -5,11 +5,13 @@ package repo import ( + "errors" "net/http" "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/convert" + releaseservice "code.gitea.io/gitea/services/release" ) // GetReleaseTag get a single release of a repository by its tagname @@ -59,3 +61,56 @@ func GetReleaseTag(ctx *context.APIContext) { } ctx.JSON(http.StatusOK, convert.ToRelease(release)) } + +// DeleteReleaseTag delete a tag from a repository +func DeleteReleaseTag(ctx *context.APIContext) { + // swagger:operation DELETE /repos/{owner}/{repo}/releases/tags/{tag} repository repoDeleteReleaseTag + // --- + // summary: Delete a release tag + // 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: name of the tag to delete + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + // "409": + // "$ref": "#/responses/conflict" + + 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 !release.IsTag { + ctx.Error(http.StatusConflict, "IsTag", errors.New("a tag attached to a release cannot be deleted directly")) + return + } + + if err := releaseservice.DeleteReleaseByID(release.ID, ctx.User, true); err != nil { + ctx.Error(http.StatusInternalServerError, "DeleteReleaseByID", err) + } + + ctx.Status(http.StatusNoContent) +} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 90a76643d..b8f81bb8f 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -7834,6 +7834,47 @@ "$ref": "#/responses/notFound" } } + }, + "delete": { + "tags": [ + "repository" + ], + "summary": "Delete a release tag", + "operationId": "repoDeleteReleaseTag", + "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": "name of the tag to delete", + "name": "tag", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "409": { + "$ref": "#/responses/conflict" + } + } } }, "/repos/{owner}/{repo}/releases/{id}": { @@ -16249,6 +16290,9 @@ "$ref": "#/definitions/WatchInfo" } }, + "conflict": { + "description": "APIConflict is a conflict empty response" + }, "empty": { "description": "APIEmpty is an empty response" },