Implement update branch API (#32433)

Resolves #22526.

Builds upon #23061.

---------

Co-authored-by: sillyguodong <33891828+sillyguodong@users.noreply.github.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
(cherry picked from commit 01b1896bf5eacfd7f4f64d9ebb0ad165e3e60a5c)

Conflicts:
	routers/api/v1/api.go
  context conflict + s/PathParam/Params/
	templates/swagger/v1_json.tmpl
  make generate-swagger
This commit is contained in:
Kemal Zebari 2024-12-11 21:02:35 -08:00 committed by Earl Warren
parent c3d37894aa
commit b0d6a7f07b
No known key found for this signature in database
GPG key ID: 0579CB2928A78A00
6 changed files with 189 additions and 0 deletions

View file

@ -290,6 +290,16 @@ type CreateBranchRepoOption struct {
OldRefName string `json:"old_ref_name" binding:"GitRefName;MaxSize(100)"` 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 // TransferRepoOption options when transfer a repository's ownership
// swagger:model // swagger:model
type TransferRepoOption struct { type TransferRepoOption struct {

View file

@ -1153,6 +1153,7 @@ func Routes() *web.Route {
m.Get("/*", repo.GetBranch) m.Get("/*", repo.GetBranch)
m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteBranch) m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteBranch)
m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateBranchRepoOption{}), context.EnforceQuotaAPI(quota_model.LimitSubjectSizeGitAll, context.QuotaTargetRepo), repo.CreateBranch) m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateBranchRepoOption{}), context.EnforceQuotaAPI(quota_model.LimitSubjectSizeGitAll, context.QuotaTargetRepo), repo.CreateBranch)
m.Patch("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.UpdateBranchRepoOption{}), repo.UpdateBranch)
}, context.ReferencesGitRepo(), reqRepoReader(unit.TypeCode)) }, context.ReferencesGitRepo(), reqRepoReader(unit.TypeCode))
m.Group("/branch_protections", func() { m.Group("/branch_protections", func() {
m.Get("", repo.ListBranchProtections) m.Get("", repo.ListBranchProtections)

View file

@ -393,6 +393,77 @@ func ListBranches(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, apiBranches) 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.Params("*")
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 // GetBranchProtection gets a branch protection
func GetBranchProtection(ctx *context.APIContext) { func GetBranchProtection(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/branch_protections/{name} repository repoGetBranchProtection // swagger:operation GET /repos/{owner}/{repo}/branch_protections/{name} repository repoGetBranchProtection

View file

@ -101,6 +101,8 @@ type swaggerParameterBodies struct {
// in:body // in:body
EditRepoOption api.EditRepoOption EditRepoOption api.EditRepoOption
// in:body // in:body
UpdateBranchRepoOption api.UpdateBranchRepoOption
// in:body
TransferRepoOption api.TransferRepoOption TransferRepoOption api.TransferRepoOption
// in:body // in:body
CreateForkOption api.CreateForkOption CreateForkOption api.CreateForkOption

View file

@ -5806,6 +5806,63 @@
"$ref": "#/responses/repoArchivedError" "$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": { "/repos/{owner}/{repo}/collaborators": {
@ -27288,6 +27345,22 @@
}, },
"x-go-package": "code.gitea.io/gitea/modules/structs" "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": { "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)", "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", "type": "object",

View file

@ -5,6 +5,7 @@ package integration
import ( import (
"net/http" "net/http"
"net/http/httptest"
"net/url" "net/url"
"testing" "testing"
@ -187,6 +188,37 @@ func testAPICreateBranch(t testing.TB, session *TestSession, user, repo, oldBran
return resp.Result().StatusCode == status 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) { func TestAPIBranchProtection(t *testing.T) {
defer tests.PrepareTestEnv(t)() defer tests.PrepareTestEnv(t)()