Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add API Endpoint for Branch Creation #11607

Merged
merged 8 commits into from
May 29, 2020
67 changes: 67 additions & 0 deletions integrations/api_branch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package integrations

import (
"net/http"
"net/url"
"testing"

api "code.gitea.io/gitea/modules/structs"
tle-huu marked this conversation as resolved.
Show resolved Hide resolved
Expand Down Expand Up @@ -100,6 +101,72 @@ func TestAPIGetBranch(t *testing.T) {
}
}

func TestAPICreateBranch(t *testing.T) {
onGiteaRun(t, testAPICreateBranches)
}

func testAPICreateBranches(t *testing.T, giteaURL *url.URL) {

username := "user2"
ctx := NewAPITestContext(t, username, "my-noo-repo")
giteaURL.Path = ctx.GitPath()

t.Run("CreateRepo", doAPICreateRepository(ctx, false))
tests := []struct {
OldBranch string
NewBranch string
ExpectedHTTPStatus int
}{
// Creating branch from default branch
{
OldBranch: "",
NewBranch: "new_branch_from_default_branch",
ExpectedHTTPStatus: http.StatusCreated,
},
// Creating branch from master
{
OldBranch: "master",
NewBranch: "new_branch_from_master_1",
ExpectedHTTPStatus: http.StatusCreated,
},
// Trying to create from master but already exists
{
OldBranch: "master",
NewBranch: "new_branch_from_master_1",
ExpectedHTTPStatus: http.StatusConflict,
},
// Trying to create from other branch (not default branch)
{
OldBranch: "new_branch_from_master_1",
NewBranch: "branch_2",
ExpectedHTTPStatus: http.StatusCreated,
},
// Trying to create from a branch which does not exist
{
OldBranch: "does_not_exist",
NewBranch: "new_branch_from_non_existent",
ExpectedHTTPStatus: http.StatusNotFound,
},
}
for _, test := range tests {
defer resetFixtures(t)
zeripath marked this conversation as resolved.
Show resolved Hide resolved
session := ctx.Session
tle-huu marked this conversation as resolved.
Show resolved Hide resolved
token := getTokenForLoggedInUser(t, session)
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/my-noo-repo/branches?token="+token, &api.CreateBranchRepoOption{
BranchName: test.NewBranch,
OldBranchName: test.OldBranch,
})
resp := session.MakeRequest(t, req, test.ExpectedHTTPStatus)

var branch api.Branch
DecodeJSON(t, resp, &branch)

if test.ExpectedHTTPStatus == http.StatusCreated {
assert.EqualValues(t, test.NewBranch, branch.Name)
}
}
}

func TestAPIBranchProtection(t *testing.T) {
defer prepareTestEnv(t)()

Expand Down
12 changes: 12 additions & 0 deletions integrations/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/queue"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/routers"
"code.gitea.io/gitea/routers/routes"
Expand Down Expand Up @@ -459,3 +460,14 @@ func GetCSRF(t testing.TB, session *TestSession, urlStr string) string {
doc := NewHTMLParser(t, resp.Body)
return doc.GetCSRF()
}

// resetFixtures flushes queues, reloads fixtures and resets test repositories within a single test.
// Most tests should call defer prepareTestEnv(t)() (or have onGiteaRun do that for them) but sometimes
// within a single test this is required
func resetFixtures(t *testing.T) {
zeripath marked this conversation as resolved.
Show resolved Hide resolved
assert.NoError(t, queue.GetManager().FlushAll(context.Background(), -1))
assert.NoError(t, models.LoadFixtures())
assert.NoError(t, os.RemoveAll(setting.RepoRootPath))
assert.NoError(t, com.CopyDir(path.Join(filepath.Dir(setting.AppPath), "integrations/gitea-repositories-meta"),
setting.RepoRootPath))
}
15 changes: 15 additions & 0 deletions models/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -995,6 +995,21 @@ func IsErrWontSign(err error) bool {
// |______ / |__| (____ /___| /\___ >___| /
// \/ \/ \/ \/ \/

// ErrBranchDoesNotExist represents an error that branch with such name does not exist.
type ErrBranchDoesNotExist struct {
BranchName string
}

// IsErrBranchDoesNotExist checks if an error is an ErrBranchDoesNotExist.
func IsErrBranchDoesNotExist(err error) bool {
_, ok := err.(ErrBranchDoesNotExist)
return ok
}

func (err ErrBranchDoesNotExist) Error() string {
return fmt.Sprintf("branch does not exist [name: %s]", err.BranchName)
}

// ErrBranchAlreadyExists represents an error that branch with such name already exists.
type ErrBranchAlreadyExists struct {
BranchName string
Expand Down
4 changes: 3 additions & 1 deletion modules/repository/branch.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ func CreateNewBranch(doer *models.User, repo *models.Repository, oldBranchName,
}

if !git.IsBranchExist(repo.RepoPath(), oldBranchName) {
return fmt.Errorf("OldBranch: %s does not exist. Cannot create new branch from this", oldBranchName)
return models.ErrBranchDoesNotExist{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you checked every call to CreateNewBranch and made sure they handle your new error correctly?

Copy link
Contributor Author

@tle-huu tle-huu May 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other place where CreateNewBranch is used is in routers/repo/branch.go CreateBranch function and uses the ctx.Repo.BranchName as the "OldBranchName".

It does not "models.isErr" it but treats it as a ServerErr. Should we add a check there as well ? If so, should we do it in this PR or in another CL ?

BranchName: oldBranchName,
}
}

basePath, err := models.CreateTemporaryPath("branch-maker")
Expand Down
16 changes: 16 additions & 0 deletions modules/structs/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,22 @@ type EditRepoOption struct {
Archived *bool `json:"archived,omitempty"`
}

// CreateBranchRepoOption options when creating a branch in a repository
// swagger:model
type CreateBranchRepoOption struct {

// Name of the branch to create
//
// required: true
tle-huu marked this conversation as resolved.
Show resolved Hide resolved
// unique: true
BranchName string `json:"new_branch_name" binding:"Required;GitRefName;MaxSize(100)"`

// Name of the old branch to create from
//
// unique: true
OldBranchName string `json:"old_branch_name" binding:"GitRefName;MaxSize(100)"`
}

// TransferRepoOption options when transfer a repository's ownership
// swagger:model
type TransferRepoOption struct {
Expand Down
1 change: 1 addition & 0 deletions routers/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,7 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Get("", repo.ListBranches)
m.Get("/*", context.RepoRefByType(context.RepoRefBranch), repo.GetBranch)
m.Delete("/*", reqRepoWriter(models.UnitTypeCode), context.RepoRefByType(context.RepoRefBranch), repo.DeleteBranch)
m.Post("", reqRepoWriter(models.UnitTypeCode), bind(api.CreateBranchRepoOption{}), repo.CreateBranch)
}, reqRepoReader(models.UnitTypeCode))
m.Group("/branch_protections", func() {
m.Get("", repo.ListBranchProtections)
Expand Down
90 changes: 90 additions & 0 deletions routers/api/v1/repo/branch.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,96 @@ func DeleteBranch(ctx *context.APIContext) {
ctx.Status(http.StatusNoContent)
}

// CreateBranch creates a branch for a user's repository
func CreateBranch(ctx *context.APIContext, opt api.CreateBranchRepoOption) {
// swagger:operation POST /repos/{owner}/{repo}/branches repository repoCreateBranch
// ---
// summary: Create 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: body
// in: body
// schema:
// "$ref": "#/definitions/CreateBranchRepoOption"
// responses:
// "201":
// "$ref": "#/responses/Branch"
// "404":
// description: The old branch does not exist.
// "409":
// description: The branch with the same name already exists.

if ctx.Repo.Repository.IsEmpty {
ctx.Error(http.StatusNotFound, "", "Git Repository is empty.")
return
}

if len(opt.OldBranchName) == 0 {
opt.OldBranchName = ctx.Repo.Repository.DefaultBranch
}

err := repo_module.CreateNewBranch(ctx.User, ctx.Repo.Repository, opt.OldBranchName, opt.BranchName)
zeripath marked this conversation as resolved.
Show resolved Hide resolved
tle-huu marked this conversation as resolved.
Show resolved Hide resolved

if err != nil {
if models.IsErrBranchDoesNotExist(err) {
ctx.Error(http.StatusNotFound, "", "The old branch does not exist")
}
if models.IsErrTagAlreadyExists(err) {
ctx.Error(http.StatusConflict, "", "The branch with the same tag already exists.")

} else if models.IsErrBranchAlreadyExists(err) || git.IsErrPushOutOfDate(err) {
ctx.Error(http.StatusConflict, "", "The branch already exists.")

} else if models.IsErrBranchNameConflict(err) {
ctx.Error(http.StatusConflict, "", "The branch with the same name already exists.")

} else {
ctx.Error(http.StatusInternalServerError, "CreateRepoBranch", err)

}
return
}

branch, err := repo_module.GetBranch(ctx.Repo.Repository, opt.BranchName)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetBranch", err)
return
}

commit, err := branch.GetCommit()
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetCommit", err)
return
}

branchProtection, err := ctx.Repo.Repository.GetBranchProtection(branch.Name)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetBranchProtection", err)
return
}

br, err := convert.ToBranch(ctx.Repo.Repository, branch, commit, branchProtection, ctx.User, ctx.Repo.IsAdmin())
if err != nil {
ctx.Error(http.StatusInternalServerError, "convert.ToBranch", err)
return
}

ctx.JSON(http.StatusCreated, br)
}

// ListBranches list all the branches of a repository
func ListBranches(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/branches repository repoListBranches
Expand Down
3 changes: 3 additions & 0 deletions routers/api/v1/swagger/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ type swaggerParameterBodies struct {
// in:body
EditReactionOption api.EditReactionOption

// in:body
CreateBranchRepoOption api.CreateBranchRepoOption

// in:body
CreateBranchProtectionOption api.CreateBranchProtectionOption

Expand Down
69 changes: 69 additions & 0 deletions templates/swagger/v1_json.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -2241,6 +2241,53 @@
"$ref": "#/responses/BranchList"
}
}
},
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Create a branch",
"operationId": "repoCreateBranch",
"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
},
{
"name": "body",
"in": "body",
"schema": {
"$ref": "#/definitions/CreateBranchRepoOption"
}
}
],
"responses": {
"201": {
"$ref": "#/responses/Branch"
},
"404": {
"description": "The old branch does not exist."
},
"409": {
"description": "The branch with the same name already exists."
}
}
}
},
"/repos/{owner}/{repo}/branches/{branch}": {
Expand Down Expand Up @@ -10886,6 +10933,28 @@
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"CreateBranchRepoOption": {
"description": "CreateBranchRepoOption options when creating a branch in a repository",
"type": "object",
"required": [
"new_branch_name"
],
"properties": {
"new_branch_name": {
"description": "Name of the branch to create",
"type": "string",
"uniqueItems": true,
"x-go-name": "BranchName"
},
"old_branch_name": {
"description": "Name of the old branch to create from",
"type": "string",
"uniqueItems": true,
"x-go-name": "OldBranchName"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"CreateEmailOption": {
"description": "CreateEmailOption options when creating email addresses",
"type": "object",
Expand Down