diff --git a/integrations/api_branch_test.go b/integrations/api_branch_test.go index acf7525f8..26d8fb4b4 100644 --- a/integrations/api_branch_test.go +++ b/integrations/api_branch_test.go @@ -6,6 +6,7 @@ package integrations import ( "net/http" + "net/url" "testing" api "code.gitea.io/gitea/modules/structs" @@ -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) + session := ctx.Session + 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)() diff --git a/integrations/integration_test.go b/integrations/integration_test.go index c6a416975..3c0125af6 100644 --- a/integrations/integration_test.go +++ b/integrations/integration_test.go @@ -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" @@ -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) { + 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)) +} diff --git a/models/error.go b/models/error.go index 3b05a7152..e9343cbe7 100644 --- a/models/error.go +++ b/models/error.go @@ -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 diff --git a/modules/repository/branch.go b/modules/repository/branch.go index 418ba25c8..94be6f0f5 100644 --- a/modules/repository/branch.go +++ b/modules/repository/branch.go @@ -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{ + BranchName: oldBranchName, + } } basePath, err := models.CreateTemporaryPath("branch-maker") diff --git a/modules/structs/repo.go b/modules/structs/repo.go index 70de9b746..832d330e7 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -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 + // 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 { diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 0d62b751c..2eb39f607 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -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) diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index 57c74d7da..90db597ef 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -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) + + 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 diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index f13dc6386..d9ef05c33 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -129,6 +129,9 @@ type swaggerParameterBodies struct { // in:body EditReactionOption api.EditReactionOption + // in:body + CreateBranchRepoOption api.CreateBranchRepoOption + // in:body CreateBranchProtectionOption api.CreateBranchProtectionOption diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 0cbe33bd2..70f12b083 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -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}": { @@ -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",