diff --git a/modules/structs/repo.go b/modules/structs/repo.go index 832ffa8bcc..fb784bd8b3 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -278,6 +278,16 @@ type CreateBranchRepoOption struct { OldRefName string `json:"old_ref_name" binding:"GitRefName;MaxSize(100)"` } +// UpdateBranchRepoOption options when updating a branch in a repository +// swagger:model +type UpdateBranchRepoOption struct { + // New branch name + // + // required: true + // unique: true + Name string `json:"name" binding:"Required;GitRefName;MaxSize(100)"` +} + // TransferRepoOption options when transfer a repository's ownership // swagger:model type TransferRepoOption struct { diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index f28ee980e1..96365e7c14 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1195,6 +1195,7 @@ func Routes() *web.Router { m.Get("/*", repo.GetBranch) m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteBranch) m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateBranchRepoOption{}), repo.CreateBranch) + m.Patch("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.UpdateBranchRepoOption{}), repo.UpdateBranch) }, context.ReferencesGitRepo(), reqRepoReader(unit.TypeCode)) m.Group("/branch_protections", func() { m.Get("", repo.ListBranchProtections) diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index 53f3b4648a..946203e97e 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -386,6 +386,77 @@ func ListBranches(ctx *context.APIContext) { ctx.JSON(http.StatusOK, apiBranches) } +// UpdateBranch updates a repository's branch. +func UpdateBranch(ctx *context.APIContext) { + // swagger:operation PATCH /repos/{owner}/{repo}/branches/{branch} repository repoUpdateBranch + // --- + // summary: Update a branch + // consumes: + // - application/json + // 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: branch + // in: path + // description: name of the branch + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/UpdateBranchRepoOption" + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" + + opt := web.GetForm(ctx).(*api.UpdateBranchRepoOption) + + oldName := ctx.PathParam("*") + repo := ctx.Repo.Repository + + if repo.IsEmpty { + ctx.Error(http.StatusNotFound, "", "Git Repository is empty.") + return + } + + if repo.IsMirror { + ctx.Error(http.StatusForbidden, "", "Git Repository is a mirror.") + return + } + + msg, err := repo_service.RenameBranch(ctx, repo, ctx.Doer, ctx.Repo.GitRepo, oldName, opt.Name) + if err != nil { + ctx.Error(http.StatusInternalServerError, "RenameBranch", err) + return + } + if msg == "target_exist" { + ctx.Error(http.StatusUnprocessableEntity, "", "Cannot rename a branch using the same name or rename to a branch that already exists.") + return + } + if msg == "from_not_exist" { + ctx.Error(http.StatusNotFound, "", "Branch doesn't exist.") + return + } + + ctx.Status(http.StatusNoContent) +} + // GetBranchProtection gets a branch protection func GetBranchProtection(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/branch_protections/{name} repository repoGetBranchProtection diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index 39c98c666e..125605d98f 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -90,6 +90,8 @@ type swaggerParameterBodies struct { // in:body EditRepoOption api.EditRepoOption // in:body + UpdateBranchRepoOption api.UpdateBranchRepoOption + // in:body TransferRepoOption api.TransferRepoOption // in:body CreateForkOption api.CreateForkOption diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index c06c0ad154..82a301da2f 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -5045,6 +5045,63 @@ "$ref": "#/responses/repoArchivedError" } } + }, + "patch": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Update a branch", + "operationId": "repoUpdateBranch", + "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 branch", + "name": "branch", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/UpdateBranchRepoOption" + } + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/validationError" + } + } } }, "/repos/{owner}/{repo}/collaborators": { @@ -24968,6 +25025,22 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "UpdateBranchRepoOption": { + "description": "UpdateBranchRepoOption options when updating a branch in a repository", + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "description": "New branch name", + "type": "string", + "uniqueItems": true, + "x-go-name": "Name" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "UpdateFileOptions": { "description": "UpdateFileOptions options for updating files\nNote: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)", "type": "object", diff --git a/tests/integration/api_branch_test.go b/tests/integration/api_branch_test.go index 8e49516aa7..24a041de17 100644 --- a/tests/integration/api_branch_test.go +++ b/tests/integration/api_branch_test.go @@ -5,6 +5,7 @@ package integration import ( "net/http" + "net/http/httptest" "net/url" "testing" @@ -186,6 +187,37 @@ func testAPICreateBranch(t testing.TB, session *TestSession, user, repo, oldBran return resp.Result().StatusCode == status } +func TestAPIUpdateBranch(t *testing.T) { + onGiteaRun(t, func(t *testing.T, _ *url.URL) { + t.Run("UpdateBranchWithEmptyRepo", func(t *testing.T) { + testAPIUpdateBranch(t, "user10", "repo6", "master", "test", http.StatusNotFound) + }) + t.Run("UpdateBranchWithSameBranchNames", func(t *testing.T) { + resp := testAPIUpdateBranch(t, "user2", "repo1", "master", "master", http.StatusUnprocessableEntity) + assert.Contains(t, resp.Body.String(), "Cannot rename a branch using the same name or rename to a branch that already exists.") + }) + t.Run("UpdateBranchThatAlreadyExists", func(t *testing.T) { + resp := testAPIUpdateBranch(t, "user2", "repo1", "master", "branch2", http.StatusUnprocessableEntity) + assert.Contains(t, resp.Body.String(), "Cannot rename a branch using the same name or rename to a branch that already exists.") + }) + t.Run("UpdateBranchWithNonExistentBranch", func(t *testing.T) { + resp := testAPIUpdateBranch(t, "user2", "repo1", "i-dont-exist", "new-branch-name", http.StatusNotFound) + assert.Contains(t, resp.Body.String(), "Branch doesn't exist.") + }) + t.Run("RenameBranchNormalScenario", func(t *testing.T) { + testAPIUpdateBranch(t, "user2", "repo1", "branch2", "new-branch-name", http.StatusNoContent) + }) + }) +} + +func testAPIUpdateBranch(t *testing.T, ownerName, repoName, from, to string, expectedHTTPStatus int) *httptest.ResponseRecorder { + token := getUserToken(t, ownerName, auth_model.AccessTokenScopeWriteRepository) + req := NewRequestWithJSON(t, "PATCH", "api/v1/repos/"+ownerName+"/"+repoName+"/branches/"+from, &api.UpdateBranchRepoOption{ + Name: to, + }).AddTokenAuth(token) + return MakeRequest(t, req, expectedHTTPStatus) +} + func TestAPIBranchProtection(t *testing.T) { defer tests.PrepareTestEnv(t)()