Skip to content

Commit 1beebeb

Browse files
kemzebsillyguodong
andcommitted
Implement rename branch API
Co-authored-by: sillyguodong <[email protected]>
1 parent 41b4ef8 commit 1beebeb

File tree

6 files changed

+205
-0
lines changed

6 files changed

+205
-0
lines changed

modules/structs/repo.go

+10
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,16 @@ type CreateBranchRepoOption struct {
278278
OldRefName string `json:"old_ref_name" binding:"GitRefName;MaxSize(100)"`
279279
}
280280

281+
// RenameBranchOption options when rename a branch in a repository
282+
// swagger:model
283+
type RenameBranchRepoOption struct {
284+
// New branch name
285+
//
286+
// required: true
287+
// unique: true
288+
NewName string `json:"new_name" binding:"Required;GitRefName;MaxSize(100)"`
289+
}
290+
281291
// TransferRepoOption options when transfer a repository's ownership
282292
// swagger:model
283293
type TransferRepoOption struct {

routers/api/v1/api.go

+1
Original file line numberDiff line numberDiff line change
@@ -1195,6 +1195,7 @@ func Routes() *web.Router {
11951195
m.Get("/*", repo.GetBranch)
11961196
m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteBranch)
11971197
m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateBranchRepoOption{}), repo.CreateBranch)
1198+
m.Post("/{name}/rename", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), reqRepoWriter(unit.TypeCode), bind(api.RenameBranchRepoOption{}), repo.RenameBranch)
11981199
}, context.ReferencesGitRepo(), reqRepoReader(unit.TypeCode))
11991200
m.Group("/branch_protections", func() {
12001201
m.Get("", repo.ListBranchProtections)

routers/api/v1/repo/branch.go

+89
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,95 @@ func ListBranches(ctx *context.APIContext) {
396396
ctx.JSON(http.StatusOK, apiBranches)
397397
}
398398

399+
// RenameBranch renames a repository's branch.
400+
func RenameBranch(ctx *context.APIContext) {
401+
// swagger:operation POST /repos/{owner}/{repo}/branches/{name}/rename repository repoRenameBranch
402+
// ---
403+
// summary: Rename a branch
404+
// consumes:
405+
// - application/json
406+
// produces:
407+
// - application/json
408+
// parameters:
409+
// - name: owner
410+
// in: path
411+
// description: owner of the repo
412+
// type: string
413+
// required: true
414+
// - name: repo
415+
// in: path
416+
// description: name of the repo
417+
// type: string
418+
// required: true
419+
// - name: name
420+
// in: path
421+
// description: original name of the branch
422+
// type: string
423+
// required: true
424+
// - name: body
425+
// in: body
426+
// schema:
427+
// "$ref": "#/definitions/RenameBranchRepoOption"
428+
// responses:
429+
// "201":
430+
// "$ref": "#/responses/Branch"
431+
// "403":
432+
// "$ref": "#/responses/forbidden"
433+
// "404":
434+
// "$ref": "#/responses/notFound"
435+
// "422":
436+
// "$ref": "#/responses/validationError"
437+
438+
opt := web.GetForm(ctx).(*api.RenameBranchRepoOption)
439+
repo := ctx.Repo.Repository
440+
441+
if repo.IsEmpty {
442+
ctx.Error(http.StatusNotFound, "", "Git Repository is empty.")
443+
return
444+
}
445+
446+
if repo.IsMirror {
447+
ctx.Error(http.StatusForbidden, "", "Git Repository is a mirror.")
448+
return
449+
}
450+
451+
msg, err := repo_service.RenameBranch(ctx, repo, ctx.Doer, ctx.Repo.GitRepo, ctx.PathParam("name"), opt.NewName)
452+
if err != nil {
453+
ctx.Error(http.StatusInternalServerError, "RenameBranch", err)
454+
return
455+
}
456+
if msg != "" {
457+
ctx.Error(http.StatusUnprocessableEntity, "", msg)
458+
return
459+
}
460+
461+
branch, err := ctx.Repo.GitRepo.GetBranch(opt.NewName)
462+
if err != nil {
463+
ctx.Error(http.StatusInternalServerError, "GetBranch", err)
464+
return
465+
}
466+
467+
commit, err := branch.GetCommit()
468+
if err != nil {
469+
ctx.Error(http.StatusInternalServerError, "GetCommit", err)
470+
return
471+
}
472+
473+
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, branch.Name)
474+
if err != nil {
475+
ctx.Error(http.StatusInternalServerError, "GetFirstMatchProtectedBranchRule", err)
476+
return
477+
}
478+
479+
br, err := convert.ToBranch(ctx, repo, opt.NewName, commit, pb, ctx.Doer, ctx.Repo.IsAdmin())
480+
if err != nil {
481+
ctx.Error(http.StatusInternalServerError, "convert.ToBranch", err)
482+
return
483+
}
484+
485+
ctx.JSON(http.StatusCreated, br)
486+
}
487+
399488
// GetBranchProtection gets a branch protection
400489
func GetBranchProtection(ctx *context.APIContext) {
401490
// swagger:operation GET /repos/{owner}/{repo}/branch_protections/{name} repository repoGetBranchProtection

routers/api/v1/swagger/options.go

+2
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ type swaggerParameterBodies struct {
9090
// in:body
9191
EditRepoOption api.EditRepoOption
9292
// in:body
93+
RenameBranchReopOption api.RenameBranchRepoOption
94+
// in:body
9395
TransferRepoOption api.TransferRepoOption
9496
// in:body
9597
CreateForkOption api.CreateForkOption

templates/swagger/v1_json.tmpl

+75
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/integration/api_branch_test.go

+28
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,34 @@ func testAPICreateBranch(t testing.TB, session *TestSession, user, repo, oldBran
185185
return resp.Result().StatusCode == status
186186
}
187187

188+
func TestAPIRenameBranch(t *testing.T) {
189+
onGiteaRun(t, func(t *testing.T, _ *url.URL) {
190+
t.Run("RenameBranchWithEmptyRepo", func(t *testing.T) {
191+
testAPIRenameBranch(t, "user10", "repo6", "master", "test", http.StatusNotFound)
192+
})
193+
t.Run("RenameBranchWithSameBranchNames", func(t *testing.T) {
194+
testAPIRenameBranch(t, "user2", "repo1", "master", "master", http.StatusUnprocessableEntity)
195+
})
196+
t.Run("RenameBranchWithBranchThatAlreadyExists", func(t *testing.T) {
197+
testAPIRenameBranch(t, "user2", "repo1", "master", "branch2", http.StatusUnprocessableEntity)
198+
})
199+
t.Run("RenameBranchWithNonExistentBranch", func(t *testing.T) {
200+
testAPIRenameBranch(t, "user2", "repo1", "i-dont-exist", "branch2", http.StatusUnprocessableEntity)
201+
})
202+
t.Run("RenameBranchNormalScenario", func(t *testing.T) {
203+
testAPIRenameBranch(t, "user2", "repo1", "branch2", "new-branch-name", http.StatusCreated)
204+
})
205+
})
206+
}
207+
208+
func testAPIRenameBranch(t *testing.T, ownerName, repoName, from, to string, expectedHTTPStatus int) {
209+
token := getUserToken(t, ownerName, auth_model.AccessTokenScopeWriteRepository)
210+
req := NewRequestWithJSON(t, "POST", "api/v1/repos/"+ownerName+"/"+repoName+"/branches/"+from+"/rename", &api.RenameBranchRepoOption{
211+
NewName: to,
212+
}).AddTokenAuth(token)
213+
MakeRequest(t, req, expectedHTTPStatus)
214+
}
215+
188216
func TestAPIBranchProtection(t *testing.T) {
189217
defer tests.PrepareTestEnv(t)()
190218

0 commit comments

Comments
 (0)